From c91cafbb26a9b4c0377f3367f1d366e12b46dbbb Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:38:19 +0200 Subject: [PATCH 1/7] refactor(core): update class implementations to implement options interfaces - Updated .oxlintrc.json to enable @typescript-eslint/no-unsafe-declaration-merging rule - Updated Api, Field, Operation, and Resource classes to implement their respective options interfaces. - Declare all the fields in the classes instead of relying on interface declaration merging - Update openapi3 test to take new fields order Signed-off-by: J3m5 <5523410+J3m5@users.noreply.github.com> --- .oxlintrc.json | 2 +- src/core/Api.ts | 7 +++++-- src/core/Field.ts | 19 +++++++++++++++--- src/core/Operation.ts | 10 ++++++++-- src/core/Resource.ts | 19 ++++++++++++++---- src/openapi3/handleJson.test.ts | 34 +++++++++++++++++---------------- 6 files changed, 63 insertions(+), 28 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index b9b85fe..c15f8b7 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -30,7 +30,7 @@ } ], "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unsafe-declaration-merging": "off", + "@typescript-eslint/no-unsafe-declaration-merging": "error", "eslint/arrow-body-style": ["error", "as-needed"], "eslint/curly": "error", "eslint/id-length": "off", diff --git a/src/core/Api.ts b/src/core/Api.ts index 8b693a7..585668b 100644 --- a/src/core/Api.ts +++ b/src/core/Api.ts @@ -8,9 +8,12 @@ export interface ApiOptions resources?: Resource[]; }> {} -export interface Api extends ApiOptions {} -export class Api { +export class Api implements ApiOptions { entrypoint: string; + + title?: string | null; + resources?: Resource[] | null; + constructor(entrypoint: string, options: ApiOptions = {}) { this.entrypoint = entrypoint; assignSealed(this, options); diff --git a/src/core/Field.ts b/src/core/Field.ts index b28c927..20ee986 100644 --- a/src/core/Field.ts +++ b/src/core/Field.ts @@ -40,16 +40,29 @@ export interface FieldOptions enum?: { [key: string | number]: string | number }; reference?: string | Resource; embedded?: Resource; - required?: boolean; nullable?: boolean; + required?: boolean; description?: string; maxCardinality?: number; deprecated?: boolean; }> {} -export interface Field extends FieldOptions {} -export class Field { +export class Field implements FieldOptions { name: string; + + id?: string | null; + range?: string | null; + type?: FieldType | null; + arrayType?: FieldType | null; + enum?: { [key: string | number]: string | number } | null; + reference?: string | Resource | null; + embedded?: Resource | null; + nullable?: boolean | null; + required?: boolean | null; + description?: string | null; + maxCardinality?: number | null; + deprecated?: boolean | null; + constructor(name: string, options: FieldOptions = {}) { this.name = name; assignSealed(this, options); diff --git a/src/core/Operation.ts b/src/core/Operation.ts index 456b0da..e60e3a1 100644 --- a/src/core/Operation.ts +++ b/src/core/Operation.ts @@ -12,10 +12,16 @@ export interface OperationOptions deprecated?: boolean; }> {} -export interface Operation extends OperationOptions {} -export class Operation { +export class Operation implements OperationOptions { name: string; type: OperationType; + + method?: string | null; + expects?: any | null; + returns?: string | null; + types?: string[] | null; + deprecated?: boolean | null; + constructor( name: string, type: OperationType, diff --git a/src/core/Resource.ts b/src/core/Resource.ts index 84addfd..1befb01 100644 --- a/src/core/Resource.ts +++ b/src/core/Resource.ts @@ -9,19 +9,30 @@ export interface ResourceOptions id?: string; title?: string; description?: string; - deprecated?: boolean; fields?: Field[]; readableFields?: Field[]; writableFields?: Field[]; - parameters?: Parameter[]; getParameters?: () => Promise; operations?: Operation[]; + deprecated?: boolean; + parameters?: Parameter[]; }> {} -export interface Resource extends ResourceOptions {} -export class Resource { +export class Resource implements ResourceOptions { name: string; url: string; + + id?: string | null; + title?: string | null; + description?: string | null; + fields?: Field[] | null; + readableFields?: Field[] | null; + writableFields?: Field[] | null; + getParameters?: (() => Promise) | null; + operations?: Operation[] | null; + deprecated?: boolean | null; + parameters?: Parameter[] | null; + constructor(name: string, url: string, options: ResourceOptions = {}) { this.name = name; this.url = url; diff --git a/src/openapi3/handleJson.test.ts b/src/openapi3/handleJson.test.ts index 25cf654..9d17344 100644 --- a/src/openapi3/handleJson.test.ts +++ b/src/openapi3/handleJson.test.ts @@ -994,14 +994,7 @@ const parsed = [ description: "", }, ], - parameters: [ - { - variable: "page", - range: "integer", - required: false, - description: "The collection page number", - }, - ], + operations: [ { name: "Retrieves a Book resource.", @@ -1034,6 +1027,14 @@ const parsed = [ deprecated: false, }, ], + parameters: [ + { + variable: "page", + range: "integer", + required: false, + description: "The collection page number", + }, + ], }, { name: "reviews", @@ -1277,14 +1278,7 @@ const parsed = [ description: "", }, ], - parameters: [ - { - variable: "page", - range: "integer", - required: false, - description: "The collection page number", - }, - ], + operations: [ { name: "Retrieves a Review resource.", @@ -1317,6 +1311,14 @@ const parsed = [ deprecated: false, }, ], + parameters: [ + { + variable: "page", + range: "integer", + required: false, + description: "The collection page number", + }, + ], }, ]; From 323858b3ac0b5353c74c586710ed8f6931e6968f Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:39:22 +0200 Subject: [PATCH 2/7] refactor(core): replace assignSealed with Object.assign - Removed assignSealed utility function. - Updated constructors in Api, Field, Operation, and Resource classes to use Object.assign for property assignment. Signed-off-by: J3m5 <5523410+J3m5@users.noreply.github.com> --- src/core/Api.ts | 3 +-- src/core/Field.ts | 3 +-- src/core/Operation.ts | 3 +-- src/core/Resource.ts | 3 +-- src/core/utils/assignSealed.ts | 13 ------------- src/core/utils/index.ts | 1 - 6 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 src/core/utils/assignSealed.ts diff --git a/src/core/Api.ts b/src/core/Api.ts index 585668b..f2f8086 100644 --- a/src/core/Api.ts +++ b/src/core/Api.ts @@ -1,6 +1,5 @@ import type { Resource } from "./Resource.js"; import type { Nullable } from "./types.js"; -import { assignSealed } from "./utils/index.js"; export interface ApiOptions extends Nullable<{ @@ -16,6 +15,6 @@ export class Api implements ApiOptions { constructor(entrypoint: string, options: ApiOptions = {}) { this.entrypoint = entrypoint; - assignSealed(this, options); + Object.assign(this, options); } } diff --git a/src/core/Field.ts b/src/core/Field.ts index 20ee986..2648e3f 100644 --- a/src/core/Field.ts +++ b/src/core/Field.ts @@ -1,6 +1,5 @@ import type { Resource } from "./Resource.js"; import type { Nullable } from "./types.js"; -import { assignSealed } from "./utils/index.js"; export type FieldType = | "string" @@ -65,6 +64,6 @@ export class Field implements FieldOptions { constructor(name: string, options: FieldOptions = {}) { this.name = name; - assignSealed(this, options); + Object.assign(this, options); } } diff --git a/src/core/Operation.ts b/src/core/Operation.ts index e60e3a1..2b26de1 100644 --- a/src/core/Operation.ts +++ b/src/core/Operation.ts @@ -1,5 +1,4 @@ import type { Nullable } from "./types.js"; -import { assignSealed } from "./utils/index.js"; export type OperationType = "show" | "edit" | "delete" | "list" | "create"; @@ -30,6 +29,6 @@ export class Operation implements OperationOptions { this.name = name; this.type = type; - assignSealed(this, options); + Object.assign(this, options); } } diff --git a/src/core/Resource.ts b/src/core/Resource.ts index 1befb01..65198cd 100644 --- a/src/core/Resource.ts +++ b/src/core/Resource.ts @@ -2,7 +2,6 @@ import type { Field } from "./Field.js"; import type { Operation } from "./Operation.js"; import type { Parameter } from "./Parameter.js"; import type { Nullable } from "./types.js"; -import { assignSealed } from "./utils/index.js"; export interface ResourceOptions extends Nullable<{ @@ -36,6 +35,6 @@ export class Resource implements ResourceOptions { constructor(name: string, url: string, options: ResourceOptions = {}) { this.name = name; this.url = url; - assignSealed(this, options); + Object.assign(this, options); } } diff --git a/src/core/utils/assignSealed.ts b/src/core/utils/assignSealed.ts deleted file mode 100644 index 9b52d7e..0000000 --- a/src/core/utils/assignSealed.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function assignSealed< - TSrc extends Record, - TTarget extends TSrc, ->(target: TTarget, src: TSrc): void { - for (const key of Object.keys(src)) { - Object.defineProperty(target, key, { - writable: true, - enumerable: true, - configurable: false, - value: src[key], - }); - } -} diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 2cdc456..521929a 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -1,4 +1,3 @@ -export { assignSealed } from "./assignSealed.js"; export { buildEnumObject } from "./buildEnumObject.js"; export { getResourcePaths } from "./getResourcePaths.js"; export { getType } from "./getType.js"; From ff8b61dec278653e7cbc1a8da4368eacf05d0a39 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:45:38 +0200 Subject: [PATCH 3/7] refactor(docs): improve JSDoc comments following oxlint jsdoc plugin - Edit .oxlintrc.json to enable jsdoc plugin - Added parameter and return type annotations in buildEnumObject, getType, fetchJsonLd, parseHydraDocumentation, and handleJson functions. Signed-off-by: J3m5 <5523410+J3m5@users.noreply.github.com> --- .oxlintrc.json | 3 ++- src/core/utils/buildEnumObject.ts | 4 ++-- src/core/utils/getType.ts | 6 ++--- src/hydra/fetchJsonLd.ts | 3 +++ src/hydra/parseHydraDocumentation.ts | 36 ++++++++++++++++++++++++++-- src/openapi3/handleJson.ts | 6 ++--- 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index c15f8b7..3609d95 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -20,7 +20,8 @@ "import", "eslint", "oxc", - "promise" + "promise", + "jsdoc" ], "rules": { "@typescript-eslint/no-empty-object-type": [ diff --git a/src/core/utils/buildEnumObject.ts b/src/core/utils/buildEnumObject.ts index ce422f4..d823ddf 100644 --- a/src/core/utils/buildEnumObject.ts +++ b/src/core/utils/buildEnumObject.ts @@ -5,8 +5,8 @@ import { humanize } from "inflection"; * The keys of the object are the humanized versions of the enum values, * and the values are the original enum values. * - * @param enumArray - An array of enum values. - * @returns An object mapping humanized enum names to their original values, or null if the input is empty. + * @param {any[] | undefined} enumArray - An array of enum values. + * @returns {Record | null} An object mapping humanized enum names to their original values, or null if the input is empty. */ export function buildEnumObject( enumArray: any[] | undefined, diff --git a/src/core/utils/getType.ts b/src/core/utils/getType.ts index e573d0d..246d5b7 100644 --- a/src/core/utils/getType.ts +++ b/src/core/utils/getType.ts @@ -7,9 +7,9 @@ import type { FieldType } from "../Field.js"; * If a format is provided, it will map certain formats (e.g., "int32", "int64") to "integer". * Otherwise, it will camelize the format string. If no format is provided, it returns the OpenAPI type. * - * @param openApiType - The OpenAPI type string. - * @param format - An optional format string. - * @returns The mapped FieldType. + * @param {string} openApiType - The OpenAPI type string. + * @param {string} [format] - An optional format string. + * @returns {FieldType} The mapped FieldType. */ export function getType(openApiType: string, format?: string): FieldType { if (format) { diff --git a/src/hydra/fetchJsonLd.ts b/src/hydra/fetchJsonLd.ts index 0419f86..bd6131f 100644 --- a/src/hydra/fetchJsonLd.ts +++ b/src/hydra/fetchJsonLd.ts @@ -19,6 +19,9 @@ interface ResponseDocument extends RemoteDocument { /** * Sends a JSON-LD request to the API. + * @param {string} url The URL to request. + * @param {RequestInitExtended} [options] Optional fetch options. + * @returns {Promise} The response document or an empty response document. */ export default async function fetchJsonLd( url: string, diff --git a/src/hydra/parseHydraDocumentation.ts b/src/hydra/parseHydraDocumentation.ts index 0b5508c..396cabe 100644 --- a/src/hydra/parseHydraDocumentation.ts +++ b/src/hydra/parseHydraDocumentation.ts @@ -16,11 +16,19 @@ import type { /** * Extracts the short name of a resource. + * @param {string} url The resource URL. + * @param {string} entrypointUrl The API entrypoint URL. + * @returns {string} The short name of the resource. */ function guessNameFromUrl(url: string, entrypointUrl: string): string { return url.slice(entrypointUrl.length + 1); } +/** + * Gets the title or label from an ExpandedOperation object. + * @param {ExpandedOperation} obj The operation object. + * @returns {string} The title or label. + */ function getTitleOrLabel(obj: ExpandedOperation): string { const a = obj["http://www.w3.org/2000/01/rdf-schema#label"] ?? @@ -36,6 +44,9 @@ function getTitleOrLabel(obj: ExpandedOperation): string { /** * Finds the description of the class with the given id. + * @param {ExpandedDoc[]} docs The expanded documentation array. + * @param {string} classToFind The class ID to find. + * @returns {ExpandedClass} The matching expanded class. */ function findSupportedClass( docs: ExpandedDoc[], @@ -87,6 +98,9 @@ export function getDocumentationUrlFromHeaders(headers: Headers): string { /** * Retrieves Hydra's entrypoint and API docs. + * @param {string} entrypointUrl The URL of the API entrypoint. + * @param {RequestInitExtended} [options] Optional fetch options. + * @returns {Promise<{ entrypointUrl: string; docsUrl: string; response: Response; entrypoint: Entrypoint[]; docs: ExpandedDoc[]; }>} An object containing entrypointUrl, docsUrl, response, entrypoint, and docs. */ async function fetchEntrypointAndDocs( entrypointUrl: string, @@ -98,6 +112,11 @@ async function fetchEntrypointAndDocs( entrypoint: Entrypoint[]; docs: ExpandedDoc[]; }> { + /** + * Loads a JSON-LD document from the given input. + * @param {string} input The URL or IRI to load. + * @returns {Promise} The fetched JSON-LD response. + */ async function documentLoader(input: string) { const response = await fetchJsonLd(input, options); if (!("body" in response)) { @@ -154,6 +173,12 @@ async function fetchEntrypointAndDocs( } } +/** + * Finds the related class for a property. + * @param {ExpandedDoc[]} docs The expanded documentation array. + * @param {ExpandedRdfProperty} property The property to find the related class for. + * @returns {ExpandedClass} The related expanded class. + */ function findRelatedClass( docs: ExpandedDoc[], property: ExpandedRdfProperty, @@ -213,6 +238,9 @@ function findRelatedClass( /** * Parses Hydra documentation and converts it to an intermediate representation. + * @param {string} entrypointUrl The API entrypoint URL. + * @param {RequestInitExtended} [options] Optional fetch options. + * @returns {Promise<{ api: Api; response: Response; status: number; }>} The parsed API, response, and status. */ export default async function parseHydraDocumentation( entrypointUrl: string, @@ -470,8 +498,12 @@ export default async function parseHydraDocumentation( }); resource.parameters = []; - resource.getParameters = (): Promise => - getParameters(resource, options); + resource.getParameters = + /** + * Gets the parameters for the resource. + * @returns {Promise} The parameters for the resource. + */ + (): Promise => getParameters(resource, options); resources.push(resource); } diff --git a/src/openapi3/handleJson.ts b/src/openapi3/handleJson.ts index 1ed60df..c45d427 100644 --- a/src/openapi3/handleJson.ts +++ b/src/openapi3/handleJson.ts @@ -20,10 +20,10 @@ import type { * Assigns relationships between resources based on their fields. * Sets the field's `embedded` or `reference` property depending on its type. * - * @param resources - Array of Resource objects to process. - * @returns The same array of resources with relationships assigned. + * @param {Resource[]} resources - Array of Resource objects to process. + * @returns {Resource[]} The same array of resources with relationships assigned. */ -function assignResourceRelationships(resources: Resource[]) { +function assignResourceRelationships(resources: Resource[]): Resource[] { for (const resource of resources) { for (const field of resource.fields ?? []) { const name = camelize(field.name).replace(/Ids?$/, ""); From 4d318b791022e9caa9d318cb1f2895e7682a8a7a Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:46:21 +0200 Subject: [PATCH 4/7] refactor(core): export types from types.js in core index file Signed-off-by: J3m5 <5523410+J3m5@users.noreply.github.com> --- src/core/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/index.ts b/src/core/index.ts index 2145921..f4f00da 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -3,3 +3,4 @@ export * from "./Field.js"; export * from "./Operation.js"; export * from "./Parameter.js"; export * from "./Resource.js"; +export * from "./types.js"; From 1787767e9a36d374c1ecb9cbf51c23bfcc8ba20d Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:46:53 +0200 Subject: [PATCH 5/7] refactor(package.json): add exports for core module types and implementation Signed-off-by: J3m5 <5523410+J3m5@users.noreply.github.com> --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 62f7585..892b7c5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,11 @@ "import": "./lib/index.js", "default": "./lib/index.js" }, + "./core": { + "types": "./lib/core/index.d.ts", + "import": "./lib/core/index.js", + "default": "./lib/core/index.js" + }, "./package.json": "./package.json" }, "main": "./lib/index.js", From 48bbe129dd46e431c0d3b95c4364f253bd66dcd1 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:47:24 +0200 Subject: [PATCH 6/7] refactor(package.json): update lint:fix command and runtime version format - Changed lint:fix command to include --fix-suggestions - Updated runtime version format to include a space Signed-off-by: J3m5 <5523410+J3m5@users.noreply.github.com> --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 892b7c5..02a4e79 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "knip": "knip", "knip:fix": "knip --fix", "lint": "oxlint", - "lint:fix": "oxlint --fix", + "lint:fix": "oxlint --fix --fix-suggestions", "test": "vitest run", "test:coverage": "vitest --coverage", "test:watch": "vitest --watch", @@ -91,7 +91,7 @@ }, "runtime": { "name": "node", - "version": ">=20" + "version": ">= 20" } } } From 40afd1625436236e3caf85eda3baaab4ba703f76 Mon Sep 17 00:00:00 2001 From: J3m5 <5523410+J3m5@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:47:42 +0200 Subject: [PATCH 7/7] refactor(oxlintrc): remove unused ESLint and Unicorn rules Signed-off-by: J3m5 <5523410+J3m5@users.noreply.github.com> --- .oxlintrc.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 3609d95..76de191 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -38,7 +38,6 @@ "eslint/max-depth": "off", "eslint/max-lines": "off", "eslint/max-lines-per-function": "off", - "eslint/max-nested-callbacks": "off", "eslint/no-duplicate-imports": "off", "func-style": [ "error", @@ -91,9 +90,7 @@ } } ], - "unicorn/no-nested-ternary": "off", "unicorn/no-null": "off", - "unicorn/number-literal-case": "off", "vitest/consistent-test-it": [ "error", {