From 01354c9aa2676060222256f6cc0355f45b6f9887 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:24:40 +0200 Subject: [PATCH] refactor: remove lodash.get --- package.json | 3 +- pnpm-lock.yaml | 9 -- src/hydra/parseHydraDocumentation.ts | 208 +++++++++++++-------------- src/openapi3/handleJson.ts | 82 ++++++++--- src/swagger/handleJson.ts | 25 ++-- 5 files changed, 166 insertions(+), 161 deletions(-) diff --git a/package.json b/package.json index 789991d..3fb06c6 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,7 @@ "graphql": "^16.0.0", "inflection": "^3.0.0", "jsonld": "^8.3.2", - "jsonref": "^9.0.0", - "lodash.get": "^4.4.0" + "jsonref": "^9.0.0" }, "devDependencies": { "@types/inflection": "^1.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e02052..34b4296 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: jsonref: specifier: ^9.0.0 version: 9.0.0 - lodash.get: - specifier: ^4.4.0 - version: 4.4.2 devDependencies: '@types/inflection': specifier: ^1.13.0 @@ -773,10 +770,6 @@ packages: resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} engines: {node: '>=14.16'} - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} @@ -1735,8 +1728,6 @@ snapshots: ky@0.33.3: {} - lodash.get@4.4.2: {} - loupe@3.1.3: {} lru-cache@10.4.3: {} diff --git a/src/hydra/parseHydraDocumentation.ts b/src/hydra/parseHydraDocumentation.ts index c7f4dc3..1aac6a7 100644 --- a/src/hydra/parseHydraDocumentation.ts +++ b/src/hydra/parseHydraDocumentation.ts @@ -1,5 +1,4 @@ import jsonld from "jsonld"; -import get from "lodash.get"; import { Api } from "../Api.js"; import { Field } from "../Field.js"; import { Resource } from "../Resource.js"; @@ -45,10 +44,8 @@ function findSupportedClass( docs: ExpandedDoc[], classToFind: string, ): ExpandedClass { - const supportedClasses = get( - docs, - '[0]["http://www.w3.org/ns/hydra/core#supportedClass"]', - ) as ExpandedClass[] | undefined; + const supportedClasses = + docs?.[0]?.["http://www.w3.org/ns/hydra/core#supportedClass"]; if (!Array.isArray(supportedClasses)) { throw new TypeError( 'The API documentation has no "http://www.w3.org/ns/hydra/core#supportedClass" key or its value is not an array.', @@ -155,7 +152,7 @@ async function fetchEntrypointAndDocs( api: new Api(entrypointUrl, { resources: [] }), error, response, - status: get(response, "status"), + status: response?.status, }; } } @@ -173,25 +170,30 @@ function findRelatedClass( property: ExpandedRdfProperty, ): ExpandedClass { // Use the entrypoint property's owl:equivalentClass if available - if (Array.isArray(property["http://www.w3.org/2000/01/rdf-schema#range"])) { - for (const range of property[ - "http://www.w3.org/2000/01/rdf-schema#range" - ]) { - const onProperty = get( - range, - '["http://www.w3.org/2002/07/owl#equivalentClass"][0]["http://www.w3.org/2002/07/owl#onProperty"][0]["@id"]', - ) as unknown as string; - const allValuesFrom = get( - range, - '["http://www.w3.org/2002/07/owl#equivalentClass"][0]["http://www.w3.org/2002/07/owl#allValuesFrom"][0]["@id"]', - ) as unknown as string; - if ( - allValuesFrom && - onProperty === "http://www.w3.org/ns/hydra/core#member" - ) { - return findSupportedClass(docs, allValuesFrom); - } + for (const range of property["http://www.w3.org/2000/01/rdf-schema#range"] ?? + []) { + const equivalentClass = + "http://www.w3.org/2002/07/owl#equivalentClass" in range + ? range?.["http://www.w3.org/2002/07/owl#equivalentClass"]?.[0] + : undefined; + + if (!equivalentClass) { + continue; + } + + const onProperty = + equivalentClass["http://www.w3.org/2002/07/owl#onProperty"]?.[0]?.["@id"]; + const allValuesFrom = + equivalentClass["http://www.w3.org/2002/07/owl#allValuesFrom"]?.[0]?.[ + "@id" + ]; + + if ( + allValuesFrom && + onProperty === "http://www.w3.org/ns/hydra/core#member" + ) { + return findSupportedClass(docs, allValuesFrom); } } @@ -205,10 +207,10 @@ function findRelatedClass( continue; } - const returns = get( - entrypointSupportedOperation, - '["http://www.w3.org/ns/hydra/core#returns"][0]["@id"]', - ) as string | undefined; + const returns = + entrypointSupportedOperation?.[ + "http://www.w3.org/ns/hydra/core#returns" + ]?.[0]?.["@id"]; if ( typeof returns === "string" && returns.indexOf("http://www.w3.org/ns/hydra/core") !== 0 @@ -241,15 +243,11 @@ export default async function parseHydraDocumentation( const resources = [], fields = [], operations = []; - const title = get( - docs, - '[0]["http://www.w3.org/ns/hydra/core#title"][0]["@value"]', - "API Platform", - ) as string; - - const entrypointType = get(entrypoint, '[0]["@type"][0]') as - | string - | undefined; + const title = + docs?.[0]?.["http://www.w3.org/ns/hydra/core#title"]?.[0]?.["@value"] ?? + "API Platform"; + + const entrypointType = entrypoint?.[0]?.["@type"]?.[0]; if (!entrypointType) { throw new Error('The API entrypoint has no "@type" key.'); } @@ -274,23 +272,25 @@ export default async function parseHydraDocumentation( writableFields = [], resourceOperations = []; - const property = get( - properties, - '["http://www.w3.org/ns/hydra/core#property"][0]', - ) as ExpandedRdfProperty | undefined; + const property = + properties?.["http://www.w3.org/ns/hydra/core#property"]?.[0]; + const propertyIri = property?.["@id"]; - if (!property) { + if (!property || !propertyIri) { continue; } - const url = get(entrypoint, `[0]["${property["@id"]}"][0]["@id"]`) as - | string - | undefined; + const resourceProperty = entrypoint?.[0]?.[propertyIri]?.[0]; + + const url = + typeof resourceProperty === "object" + ? resourceProperty["@id"] + : undefined; if (!url) { console.error( new Error( - `Unable to find the URL for "${property["@id"]}" in the entrypoint, make sure your API resource has at least one GET collection operation declared.`, + `Unable to find the URL for "${propertyIri}" in the entrypoint, make sure your API resource has at least one GET collection operation declared.`, ), ); continue; @@ -298,19 +298,16 @@ export default async function parseHydraDocumentation( // Add fields const relatedClass = findRelatedClass(docs, property); - for (const supportedProperties of relatedClass[ + for (const supportedProperties of relatedClass?.[ "http://www.w3.org/ns/hydra/core#supportedProperty" - ]) { - const supportedProperty = get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#property"][0]', - ) as unknown as ExpandedRdfProperty; + ] ?? []) { + const supportedProperty = + supportedProperties?.["http://www.w3.org/ns/hydra/core#property"]?.[0]; const id = supportedProperty?.["@id"]; - const range = get( - supportedProperty, - '["http://www.w3.org/2000/01/rdf-schema#range"][0]["@id"]', - null, - ) as unknown as string; + const range = + supportedProperty?.[ + "http://www.w3.org/2000/01/rdf-schema#range" + ]?.[0]?.["@id"] ?? null; const field = new Field( supportedProperties?.["http://www.w3.org/ns/hydra/core#title"]?.[0]?.[ @@ -324,35 +321,31 @@ export default async function parseHydraDocumentation( range, type: getType(id, range), reference: - get(supportedProperty, '["@type"][0]') === + supportedProperty?.["@type"]?.[0] === "http://www.w3.org/ns/hydra/core#Link" ? range // Will be updated in a subsequent pass : null, embedded: - get(supportedProperty, '["@type"][0]') === + supportedProperty?.["@type"]?.[0] === "http://www.w3.org/ns/hydra/core#Link" ? null : (range as unknown as Resource), // Will be updated in a subsequent pass - required: get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#required"][0]["@value"]', - false, - ) as boolean, - description: get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#description"][0]["@value"]', - "", - ) as string, - maxCardinality: get( - supportedProperty, - '["http://www.w3.org/2002/07/owl#maxCardinality"][0]["@value"]', - null, - ) as number | null, - deprecated: get( - supportedProperties, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + required: + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#required" + ]?.[0]?.["@value"] ?? false, + description: + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#description" + ]?.[0]?.["@value"] ?? "", + maxCardinality: + supportedProperty?.[ + "http://www.w3.org/2002/07/owl#maxCardinality" + ]?.[0]?.["@value"] ?? null, + deprecated: + supportedProperties?.[ + "http://www.w3.org/2002/07/owl#deprecated" + ]?.[0]?.["@value"] ?? false, }, ); @@ -360,23 +353,20 @@ export default async function parseHydraDocumentation( resourceFields.push(field); if ( - get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#readable"][0]["@value"]', - ) + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#readable" + ]?.[0]?.["@value"] ) { readableFields.push(field); } if ( - get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#writeable"][0]["@value"]', - ) || - get( - supportedProperties, - '["http://www.w3.org/ns/hydra/core#writable"][0]["@value"]', - ) + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#writeable" + ]?.[0]?.["@value"] || + supportedProperties?.[ + "http://www.w3.org/ns/hydra/core#writable" + ]?.[0]?.["@value"] ) { writableFields.push(field); } @@ -414,11 +404,10 @@ export default async function parseHydraDocumentation( ]?.[0]?.["@id"], returns: range, types: entrypointOperation["@type"], - deprecated: get( - entrypointOperation, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + deprecated: + entrypointOperation?.[ + "http://www.w3.org/2002/07/owl#deprecated" + ]?.[0]?.["@value"] ?? false, }, ); @@ -464,11 +453,10 @@ export default async function parseHydraDocumentation( ]?.[0]?.["@id"], returns: range, types: supportedOperation["@type"], - deprecated: get( - supportedOperation, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + deprecated: + supportedOperation?.[ + "http://www.w3.org/2002/07/owl#deprecated" + ]?.[0]?.["@value"] ?? false, }, ); @@ -478,20 +466,18 @@ export default async function parseHydraDocumentation( const resource = new Resource(guessNameFromUrl(url, entrypointUrl), url, { id: relatedClass["@id"], - title: get( - relatedClass, - '["http://www.w3.org/ns/hydra/core#title"][0]["@value"]', - "", - ) as string, + title: + relatedClass?.["http://www.w3.org/ns/hydra/core#title"]?.[0]?.[ + "@value" + ] ?? "", fields: resourceFields, readableFields, writableFields, operations: resourceOperations, - deprecated: get( - relatedClass, - '["http://www.w3.org/2002/07/owl#deprecated"][0]["@value"]', - false, - ) as boolean, + deprecated: + relatedClass?.["http://www.w3.org/2002/07/owl#deprecated"]?.[0]?.[ + "@value" + ] ?? false, }); resource.parameters = []; diff --git a/src/openapi3/handleJson.ts b/src/openapi3/handleJson.ts index 2dfdcbc..cb4f336 100644 --- a/src/openapi3/handleJson.ts +++ b/src/openapi3/handleJson.ts @@ -1,5 +1,4 @@ import { parse as dereference } from "jsonref"; -import get from "lodash.get"; import inflection from "inflection"; import { Field } from "../Field.js"; import { Operation } from "../Operation.js"; @@ -11,21 +10,66 @@ import type { OpenAPIV3 } from "openapi-types"; import type { OperationType } from "../Operation.js"; function isParameter( - maybeParameter: OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject, -): maybeParameter is OpenAPIV3.ParameterObject { - return ( - maybeParameter !== undefined && - "name" in maybeParameter && - "in" in maybeParameter - ); + maybeParameter: NonNullable[number], +) { + return maybeParameter !== undefined && "in" in maybeParameter; } function isSchema( - maybeSchema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined, + maybeSchema: OpenAPIV3.MediaTypeObject["schema"], ): maybeSchema is OpenAPIV3.SchemaObject { + // Type predicate can't be inferred because all properties of SchemaObject + // type are optional, so we need to check for absence of $ref, but negated + // `in` checks can't infer the type. return maybeSchema !== undefined && !("$ref" in maybeSchema); } +function isResponse( + maybeResponse: OpenAPIV3.ResponsesObject[string] | undefined, +) { + return maybeResponse !== undefined && "description" in maybeResponse; +} + +function isRequestBody( + maybeRequestBody: OpenAPIV3.OperationObject["requestBody"], +) { + return maybeRequestBody !== undefined && "content" in maybeRequestBody; +} + +function getSchemaFromEditOperation( + editOperation: OpenAPIV3.OperationObject | undefined, +) { + if ( + isRequestBody(editOperation?.requestBody) && + isSchema(editOperation.requestBody.content?.["application/json"]?.schema) + ) { + return editOperation.requestBody.content["application/json"].schema; + } + + return null; +} + +function getSchemaFromShowOperation( + showOperation: OpenAPIV3.OperationObject | undefined, + document: OpenAPIV3.Document, + title: string, +) { + if ( + isResponse(showOperation?.responses?.["200"]) && + isSchema( + showOperation.responses["200"]?.content?.["application/json"]?.schema, + ) + ) { + return showOperation.responses["200"].content["application/json"].schema; + } + + if (isSchema(document.components?.schemas?.[title])) { + return document.components.schemas[title]; + } + + return null; +} + export function removeTrailingSlash(url: string): string { if (url.endsWith("/")) { url = url.slice(0, -1); @@ -184,19 +228,13 @@ export default async function handleJson( continue; } - const showSchema = showOperation - ? (get( - showOperation, - "responses.200.content.application/json.schema", - get(document, `components.schemas[${title}]`), - ) as OpenAPIV3.SchemaObject) - : null; - const editSchema = editOperation - ? (get( - editOperation, - "requestBody.content.application/json.schema", - ) as unknown as OpenAPIV3.SchemaObject) - : null; + const showSchema = getSchemaFromShowOperation( + showOperation, + document, + title, + ); + + const editSchema = getSchemaFromEditOperation(editOperation); if (!showSchema && !editSchema) { continue; diff --git a/src/swagger/handleJson.ts b/src/swagger/handleJson.ts index 761219a..23b540d 100644 --- a/src/swagger/handleJson.ts +++ b/src/swagger/handleJson.ts @@ -1,4 +1,3 @@ -import get from "lodash.get"; import inflection from "inflection"; import { Field } from "../Field.js"; import { Resource } from "../Resource.js"; @@ -8,7 +7,7 @@ import type { OpenAPIV2 } from "openapi-types"; export function removeTrailingSlash(url: string): string { if (url.endsWith("/")) { - url = url.slice(0, -1); + return url.slice(0, -1); } return url; } @@ -41,31 +40,23 @@ export default function handleJson( throw new Error(); // @TODO } - const description = definition.description || ""; - const properties = definition.properties; + const { description = "", properties } = definition; if (!properties) { throw new Error(); // @TODO } - const fieldNames = Object.keys(properties); - const requiredFields = get( - response, - ["definitions", title, "required"], - [], - ) as string[]; - - const fields = fieldNames.map((fieldName) => { - const property = properties[fieldName]; + const requiredFields = response.definitions?.[title]?.required ?? []; + const fields = Object.entries(properties).map(([fieldName, property]) => { return new Field(fieldName, { id: null, range: null, type: getType( - get(property, "type", "") as string, - get(property, "format", "") as string, + typeof property?.type === "string" ? property.type : "", + property?.["format"] ?? "", ), - enum: property?.enum + enum: property.enum ? Object.fromEntries( property.enum.map((enumValue: string | number) => [ typeof enumValue === "string" @@ -78,7 +69,7 @@ export default function handleJson( reference: null, embedded: null, required: requiredFields.some((value) => value === fieldName), - description: property?.description || "", + description: property.description || "", }); });