diff --git a/.changeset/witty-teachers-smoke.md b/.changeset/witty-teachers-smoke.md new file mode 100644 index 0000000..748dff0 --- /dev/null +++ b/.changeset/witty-teachers-smoke.md @@ -0,0 +1,5 @@ +--- +"@nornir/rest": major +--- + +OpenAPI 3 spec generation diff --git a/eslint.config.js b/eslint.config.js index 4db1c0f..f0d7b41 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,21 +18,16 @@ const test = { extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", - "plugin:unicorn/recommended", "plugin:jest/recommended", - "plugin:jest/style", "plugin:workspaces/recommended", "plugin:eslint-comments/recommended", - "plugin:sonarjs/recommended", ], parser: "@typescript-eslint/parser", plugins: [ "@typescript-eslint", "eslint-plugin-workspaces", - "eslint-plugin-unicorn", "eslint-plugin-jest", "eslint-plugin-eslint-comments", - "eslint-plugin-sonarjs", "eslint-plugin-no-secrets", ], root: true, @@ -44,15 +39,8 @@ const test = { ], rules: { // Reduce is confusing, but it shouldn't be banned - "unicorn/no-array-reduce": ["off"], - "unicorn/filename-case": ["error", { - case: "kebabCase", - }], - "unicorn/no-empty-files": ["off"], "workspaces/require-dependency": ["off"], - "unicorn/prevent-abbreviations": ["off"], "no-secrets/no-secrets": ["warn", {"tolerance": 5.0}], - "unicorn/numeric-separators-style": ["off"], "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", @@ -72,8 +60,7 @@ const test = { "match": false } }, - ], - "sonarjs/no-duplicate-string": ["off"], + ] }, }; diff --git a/package.json b/package.json index 8776bca..ee86fea 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,6 @@ "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-jest": "^27.2.3", "eslint-plugin-no-secrets": "^0.8.9", - "eslint-plugin-sonarjs": "^0.19.0", - "eslint-plugin-unicorn": "^48.0.0", "eslint-plugin-workspaces": "^0.9.0", "husky": "^8.0.3", "jest": "^29.5.0", @@ -29,9 +27,9 @@ "plop": "^3.1.2", "scripts": "workspace:^", "syncpack": "^9.8.4", - "ts-patch": "^3.0.2", + "ts-patch": "^3.1.1", "turbo": "^1.9.2", - "typescript": "^5.2.2" + "typescript": "^5.3.3" }, "engines": { "node": ">=18.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index cd7bead..0521159 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,14 +5,14 @@ "author": "John Conley", "devDependencies": { "@jest/globals": "^29.5.0", - "@nrfcloud/ts-json-schema-transformer": "^1.2.4", + "@nrfcloud/ts-json-schema-transformer": "^1.3.0", "@types/jest": "^29.4.0", "@types/node": "^18.15.11", "esbuild": "^0.17.18", "eslint": "^8.45.0", "jest": "^29.5.0", - "ts-patch": "^3.0.2", - "typescript": "^5.2.2" + "ts-patch": "^3.1.1", + "typescript": "^5.3.3" }, "engines": { "node": ">=18.0.0", diff --git a/packages/rest/__tests__/src/routing.spec.mts b/packages/rest/__tests__/src/routing.spec.mts index bb18976..cfa1993 100644 --- a/packages/rest/__tests__/src/routing.spec.mts +++ b/packages/rest/__tests__/src/routing.spec.mts @@ -1,21 +1,21 @@ import { - AnyMimeType, Controller, GetChain, HttpEvent, HttpRequest, - HttpStatusCode, - MimeType, + HttpRequestEmpty, normalizeEventHeaders, NornirRestRequestValidationError, PostChain, - router + router, + MimeType, + HttpStatusCode } from "../../dist/runtime/index.mjs"; import {nornir, Nornir} from "@nornir/core"; import {describe} from "@jest/globals"; import {NornirRouteNotFoundError} from "../../dist/runtime/router.mjs"; -interface RouteGetInput extends HttpRequest { +interface RouteGetInput extends HttpRequestEmpty { } @@ -80,42 +80,41 @@ class TestController { * @summary Cool Route */ @GetChain("/route") - public getRoute(chain: Nornir) { + public getRoute(chain: Nornir): Nornir { return chain .use(console.log) .use(() => ({ statusCode: HttpStatusCode.Ok, body: `cool`, headers: { - // eslint-disable-next-line sonarjs/no-duplicate-string - "content-type": MimeType.TextPlain, + "content-type": MimeType.TextPlain }, })); } @GetChain("/route2") - public getEmptyRoute(chain: Nornir) { + public getEmptyRoute(chain: Nornir): Nornir}> { return chain - .use(() => ({ - statusCode: HttpStatusCode.Ok, - body: undefined, - headers: { - "content-type": AnyMimeType - }, - })); + .use(() => { + return { + statusCode: HttpStatusCode.Ok, + body: undefined, + headers: {}, + } + }); } @PostChain("/route") - public postRoute(chain: Nornir) { + public postRoute(chain: Nornir): Nornir { return chain .use(input => input.headers["content-type"]) .use(contentType => ({ statusCode: HttpStatusCode.Ok, body: `Content-Type: ${contentType}`, headers: { - "content-type": MimeType.TextPlain, + "content-type": MimeType.TextPlain }, - })); + } as const)); } } @@ -134,7 +133,7 @@ describe("REST tests", () => { headers: {}, query: {} }); - expect(response.statusCode).toEqual(HttpStatusCode.Ok); + expect(response.statusCode).toEqual("200"); expect(response.body).toBe("cool"); expect(response.headers["content-type"]).toBe("text/plain"); }) @@ -154,7 +153,7 @@ describe("REST tests", () => { } }); expect(response).toEqual({ - statusCode: HttpStatusCode.Ok, + statusCode: "200", body: "Content-Type: application/json", headers: { "content-type": "text/plain" @@ -170,10 +169,9 @@ describe("REST tests", () => { query: {} }); expect(response).toEqual({ - statusCode: HttpStatusCode.Ok, + statusCode: "200", body: undefined, headers: { - "content-type": AnyMimeType } }) }) diff --git a/packages/rest/package.json b/packages/rest/package.json index 9ab072d..4650620 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -2,25 +2,39 @@ "name": "@nornir/rest", "description": "A nornir library", "version": "1.5.2", + "bin": { + "nornir-oas": "./dist/cli/cli.js" + }, "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.1.0", "@nornir/core": "workspace:^", - "@nrfcloud/ts-json-schema-transformer": "^1.2.4", + "@nrfcloud/ts-json-schema-transformer": "^1.3.0", "@types/aws-lambda": "^8.10.115", "ajv": "^8.12.0", + "atlassian-openapi": "^1.0.18", + "glob": "^10.3.10", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.21", + "openapi-diff": "^0.23.6", "openapi-types": "^12.1.0", "trouter": "^3.2.1", - "ts-json-schema-generator": "^1.4.0", - "ts-morph": "^19.0.0", - "tsutils": "^3.21.0" + "ts-is-present": "^1.2.2", + "ts-json-schema-generator": "^1.5.0", + "ts-morph": "^21.0.1", + "tsutils": "^3.21.0", + "yargs": "^17.7.2" }, "devDependencies": { "@jest/globals": "^29.5.0", "@types/jest": "^29.4.0", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.14.202", "@types/node": "^18.15.11", + "@types/yargs": "^17.0.32", "eslint": "^8.45.0", "jest": "^29.5.0", - "ts-patch": "^3.0.2", - "typescript": "^5.2.2" + "ts-patch": "^3.1.1", + "typescript": "^5.3.3" }, "engines": { "node": ">=18.0.0", @@ -68,5 +82,12 @@ "prepare": "patch-typescript", "prepublish": "pnpm build:clean", "test": "pnpm tests" + }, + "tsp": { + "name": "@nornir/rest", + "transform": "./dist/transform/transform.js", + "tscOptions": { + "parseAllJsDoc": true + } } } diff --git a/packages/rest/src/cli/cli.ts b/packages/rest/src/cli/cli.ts new file mode 100644 index 0000000..392e937 --- /dev/null +++ b/packages/rest/src/cli/cli.ts @@ -0,0 +1,42 @@ +import { readFileSync, writeFileSync } from "fs"; +import { merge } from "lodash"; +import path from "path"; +import yargs from "yargs"; +import { isErrorResult } from "../transform/openapi-merge"; +import { getMergedSpec } from "./lib/collect"; +import { resolveTsConfigOutdir } from "./lib/ts-utils"; + +yargs + .scriptName("nornir-oas") + .command("collect", "Build OpenAPI spec from collected spec files", args => + args + .option("output", { + default: path.join(process.cwd(), "openapi.json"), + alias: "o", + type: "string", + description: "Output file for the generated OpenAPI spec", + }) + .option("scanDirectory", { + default: resolveTsConfigOutdir() ?? path.join(process.cwd(), "dist"), + type: "string", + alias: "s", + description: + "Directory to scan for spec files. This is probably the directory where your compiled typescript files are.", + }) + .option("overrideSpec", { + type: "string", + alias: "b", + description: "Path to an openapi JSON file to deeply merge with collected specification", + }), args => { + const mergedSpec = getMergedSpec(args.scanDirectory); + if (isErrorResult(mergedSpec)) { + throw new Error("Failed to merge spec files"); + } + let spec = mergedSpec.output; + if (args.overrideSpec) { + const overrideSpec = JSON.parse(readFileSync(args.overrideSpec, "utf-8")); + spec = merge(spec, overrideSpec); + } + + writeFileSync(args.output, JSON.stringify(spec, null, 2)); + }).strictCommands().demandCommand().parse(); diff --git a/packages/rest/src/cli/lib/collect.ts b/packages/rest/src/cli/lib/collect.ts new file mode 100644 index 0000000..af5c176 --- /dev/null +++ b/packages/rest/src/cli/lib/collect.ts @@ -0,0 +1,30 @@ +import { Swagger } from "atlassian-openapi"; +import { readFileSync } from "fs"; +import { sync } from "glob"; +import { OpenAPIV3_1 } from "openapi-types"; +import { merge } from "../../transform/openapi-merge/index.js"; +import SwaggerV3 = Swagger.SwaggerV3; + +export function getSpecFiles(scanDir: string) { + return sync(`${scanDir}/**/*.nornir.oas.json`); +} + +export function readSpecFiles(paths: string[]) { + return paths.map(path => { + return JSON.parse(readFileSync(path, "utf-8")) as OpenAPIV3_1.Document; + }); +} + +export function getMergedSpec(scanDir: string) { + const files = getSpecFiles(scanDir); + const specs = readSpecFiles(files); + return merge(specs.map(spec => ({ + dispute: { + // mergeDispute: true, + // alwaysApply: true, + // prefix: "", + // suffix: "" + }, + oas: spec as SwaggerV3, + }))); +} diff --git a/packages/rest/src/cli/lib/ts-utils.ts b/packages/rest/src/cli/lib/ts-utils.ts new file mode 100644 index 0000000..f743775 --- /dev/null +++ b/packages/rest/src/cli/lib/ts-utils.ts @@ -0,0 +1,24 @@ +import { dirname, resolve } from "node:path"; +import ts from "typescript"; + +export function resolveTsConfigOutdir(searchPath = process.cwd(), configName = "tsconfig.json") { + const path = ts.findConfigFile(searchPath, ts.sys.fileExists, configName); + if (path == undefined) { + return; + } + const config = ts.readConfigFile(path, ts.sys.readFile); + if (config.error) { + return; + } + const compilerOptions = config.config as { compilerOptions: ts.CompilerOptions }; + + const pathDir = dirname(path); + + const outDir = compilerOptions.compilerOptions.outDir; + + if (outDir == undefined) { + return undefined; + } + + return resolve(pathDir, outDir); +} diff --git a/packages/rest/src/runtime/decorators.mts b/packages/rest/src/runtime/decorators.mts index 82fdc8a..480d84c 100644 --- a/packages/rest/src/runtime/decorators.mts +++ b/packages/rest/src/runtime/decorators.mts @@ -1,5 +1,5 @@ import {Nornir} from "@nornir/core"; -import {HttpRequest, HttpResponse} from "./http-event.mjs"; +import {HttpRequest, HttpResponse, HttpStatusCode, MimeType} from "./http-event.mjs"; import {InstanceOf} from "ts-morph"; const UNTRANSFORMED_ERROR = new Error("nornir/rest decorators have not been transformed. Have you setup ts-patch/ttypescript and added the originator to your tsconfig.json?"); @@ -15,18 +15,41 @@ export function Controller( - _target: (chain: Nornir) => Nornir, +const routeChainDecorator = ( + _target: (chain: Nornir>) => Nornir, ValidateResponseType>, _propertyKey: ClassMethodDecoratorContext, ): never => {throw UNTRANSFORMED_ERROR}; +export type ValidateRequestType = RequestResponseWithBodyHasContentType extends true ? T : "Request type with a body must have a content-type header"; +export type ValidateResponseType = RequestResponseWithBodyHasContentType extends true ? + OutputHasSpecifiedStatusCode extends true + ? T : "Response type must have a status code specified" : "Response type with a body must have a content-type header"; + +type OutputHasSpecifiedStatusCode = IfEquals; + +type RequestResponseWithBodyHasContentType = + // No body spec is valid + HasBody extends false ? true : + // Empty body is valid + T extends { body?: undefined | null } ? true : + T["headers"]["content-type"] extends string ? + IfEquals + : false; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type HasBody = T extends { body: any } ? true : false + +type Test = { statusCode: HttpStatusCode.Ok, headers: NonNullable, body: string} + /** * Use to mark a method as a GET route * * @originator nornir/rest + * */ -export function GetChain(_path: string) { - return routeChainDecorator; +export function GetChain(_path: Path) + { + return routeChainDecorator as IfEquals; } /** @@ -34,8 +57,8 @@ export function GetChain(_path: string) { * * @originator nornir/rest */ -export function PostChain(_path: string) { - return routeChainDecorator; +export function PostChain(_path: Path) { + return routeChainDecorator as IfEquals; } /** @@ -43,8 +66,8 @@ export function PostChain(_path: string) { * * @originator nornir/rest */ -export function PutChain(_path: string) { - return routeChainDecorator; +export function PutChain(_path: Path) { + return routeChainDecorator as IfEquals; } /** @@ -52,8 +75,8 @@ export function PutChain(_path: string) { * * @originator nornir/rest */ -export function PatchChain(_path: string) { - return routeChainDecorator; +export function PatchChain(_path: Path) { + return routeChainDecorator as IfEquals; } /** @@ -61,8 +84,8 @@ export function PatchChain(_path: string) { * * @originator nornir/rest */ -export function DeleteChain(_path: string) { - return routeChainDecorator; +export function DeleteChain(_path: Path) { + return routeChainDecorator as IfEquals; } /** @@ -70,8 +93,8 @@ export function DeleteChain(_path: string) { * * @originator nornir/rest */ -export function HeadChain(_path: string) { - return routeChainDecorator; +export function HeadChain(_path: Path) { + return routeChainDecorator as IfEquals; } /** @@ -79,8 +102,8 @@ export function HeadChain(_path: string) { * * @originator nornir/rest */ -export function OptionsChain(_path: string) { - return routeChainDecorator; +export function OptionsChain(_path: Path) { + return routeChainDecorator as IfEquals; } /** @@ -93,3 +116,6 @@ export function Provider() { throw UNTRANSFORMED_ERROR } } + +type IfEquals = (() => G extends T ? 1 : 2) extends (() => G extends U ? 1 : 2) ? Y + : N; diff --git a/packages/rest/src/runtime/http-event.mts b/packages/rest/src/runtime/http-event.mts index 198bb17..16f17d8 100644 --- a/packages/rest/src/runtime/http-event.mts +++ b/packages/rest/src/runtime/http-event.mts @@ -1,5 +1,3 @@ -import {Nominal} from "./utils.mjs"; - export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"; export type HttpEvent = Omit & { @@ -13,32 +11,34 @@ export type UnparsedHttpEvent = Omit & { rawQuery: string } -// export type HttpHeaders = { -// readonly "content-type": MimeType; -// } & Record - export type HttpHeadersWithContentType = { - readonly "content-type": MimeType | AnyMimeType + readonly "content-type": MimeType } & HttpHeaders; +export type HttpHeadersWithoutContentType = { + readonly "content-type"?: undefined +} & HttpHeaders -export type HttpHeaders = Record; + +export type HttpHeaders = Record; export interface HttpRequest { - readonly headers: HttpHeadersWithContentType; + readonly headers: HttpHeadersWithoutContentType | HttpHeadersWithContentType; - readonly query: Record | Array>; + readonly query: QueryParams; readonly body?: unknown; - readonly pathParams: Record; + readonly pathParams: PathParams; } +type QueryParams = Record | Array>; + +type PathParams = Record; + export interface HttpRequestEmpty extends HttpRequest { - headers: { - "content-type": AnyMimeType; - } - // body?: undefined; + headers: HttpHeadersWithoutContentType + body?: undefined } export interface HttpRequestJSON extends HttpRequest { @@ -50,7 +50,7 @@ export interface HttpRequestJSON extends HttpRequest { export interface HttpResponse { readonly statusCode: HttpStatusCode; - readonly headers: HttpHeadersWithContentType; + readonly headers: HttpHeadersWithContentType | HttpHeadersWithoutContentType; readonly body?: unknown; } @@ -59,12 +59,12 @@ export interface SerializedHttpResponse extends Omit { } export interface HttpResponseEmpty extends HttpResponse { - headers: { - "content-type": AnyMimeType; - }, - body?: undefined; + readonly body?: undefined } +/** + * @ignore + */ export enum HttpStatusCode { Continue = "100", SwitchingProtocols = "101", @@ -127,10 +127,13 @@ export enum HttpStatusCode { NotExtended = "510", } -export type AnyMimeType = Nominal -export const AnyMimeType = "*/*" as AnyMimeType; - +/** + * @ignore + */ export enum MimeType { + /** + * @ignore + */ None = "", ApplicationJson = "application/json", ApplicationOctetStream = "application/octet-stream", diff --git a/packages/rest/src/runtime/index.mts b/packages/rest/src/runtime/index.mts index 5539600..04d5afa 100644 --- a/packages/rest/src/runtime/index.mts +++ b/packages/rest/src/runtime/index.mts @@ -1,13 +1,20 @@ import {nornir} from "@nornir/core"; +import {UnparsedHttpEvent} from './http-event.mjs'; +import {normalizeEventHeaders} from "./utils.mjs" +import {httpEventParser} from './parse.mjs' +import {httpResponseSerializer} from './serialize.mjs' + +import {Router} from './router.mjs'; +import {httpErrorHandler} from "./error.mjs"; export { GetChain, Controller, PostChain, DeleteChain, HeadChain, OptionsChain, PatchChain, PutChain, - Provider + Provider, ValidateRequestType, ValidateResponseType } from './decorators.mjs'; export { HttpResponse, HttpRequest, HttpEvent, HttpMethod, HttpRequestEmpty, HttpResponseEmpty, HttpStatusCode, HttpRequestJSON, HttpHeaders, MimeType, - UnparsedHttpEvent, SerializedHttpResponse, AnyMimeType + UnparsedHttpEvent, SerializedHttpResponse } from './http-event.mjs'; export {RouteHolder, NornirRestRequestValidationError} from './route-holder.mjs' export {NornirRestRequestError, NornirRestError, httpErrorHandler, mapError, mapErrorClass} from './error.mjs' @@ -17,13 +24,6 @@ export {httpResponseSerializer, HttpBodySerializer, HttpBodySerializerMap} from export {normalizeEventHeaders, normalizeHeaders, getContentType} from "./utils.mjs" export {Router} from "./router.mjs" -import {UnparsedHttpEvent} from './http-event.mjs'; -import {normalizeEventHeaders} from "./utils.mjs" -import {httpEventParser} from './parse.mjs' -import {httpResponseSerializer} from './serialize.mjs' - -import {Router} from './router.mjs'; -import {httpErrorHandler} from "./error.mjs"; export const router = Router.build export function restChain() { diff --git a/packages/rest/src/runtime/parse.mts b/packages/rest/src/runtime/parse.mts index de0c646..8832f2d 100644 --- a/packages/rest/src/runtime/parse.mts +++ b/packages/rest/src/runtime/parse.mts @@ -3,7 +3,6 @@ import querystring from "node:querystring"; import {getContentType} from "./utils.mjs"; import {NornirRestError} from "./error.mjs"; -import {AttachmentRegistry} from "@nornir/core"; export type HttpBodyParser = (body: Buffer) => unknown @@ -19,7 +18,7 @@ export class NornirRestParseError extends NornirRestError { return { statusCode: HttpStatusCode.UnprocessableEntity, headers: { - "content-type": MimeType.TextPlain, + "content-type": MimeType.ApplicationJson, }, body: this.message } diff --git a/packages/rest/src/runtime/route-holder.mts b/packages/rest/src/runtime/route-holder.mts index dd43827..cafcc9e 100644 --- a/packages/rest/src/runtime/route-holder.mts +++ b/packages/rest/src/runtime/route-holder.mts @@ -53,7 +53,7 @@ export class NornirRestRequestValidationError exten statusCode: HttpStatusCode.UnprocessableEntity, body: {errors: this.errors}, headers: { - 'content-type': MimeType.ApplicationJson, + 'content-type': MimeType.ApplicationJson }, } } diff --git a/packages/rest/src/runtime/router.mts b/packages/rest/src/runtime/router.mts index 2be596c..b7e0be9 100644 --- a/packages/rest/src/runtime/router.mts +++ b/packages/rest/src/runtime/router.mts @@ -63,7 +63,6 @@ export class Router { return async (event, registry): Promise => { - // eslint-disable-next-line unicorn/no-array-method-this-argument, unicorn/no-array-callback-reference const {params, handlers: [handler]} = this.router.find(event.method, event.path); const request: HttpRequest = { ...event, diff --git a/packages/rest/src/runtime/serialize.mts b/packages/rest/src/runtime/serialize.mts index d123731..d5dce68 100644 --- a/packages/rest/src/runtime/serialize.mts +++ b/packages/rest/src/runtime/serialize.mts @@ -2,14 +2,14 @@ import {HttpResponse, MimeType, SerializedHttpResponse} from "./http-event.mjs"; import {getContentType} from "./utils.mjs"; -export type HttpBodySerializer = (body: T | undefined) => Buffer +export type HttpBodySerializer = (body: unknown | undefined) => Buffer -export type HttpBodySerializerMap = Partial>> +export type HttpBodySerializerMap = Partial> -const DEFAULT_BODY_SERIALIZERS: HttpBodySerializerMap & {default: HttpBodySerializer} = { - "application/json": (body?: object) => Buffer.from(JSON.stringify(body) || "", "utf8"), - "text/plain": (body?: string) => Buffer.from(body?.toString() || "", "utf8"), - "default": (body?: never) => Buffer.from(JSON.stringify(body) || "", "utf8") +const DEFAULT_BODY_SERIALIZERS: HttpBodySerializerMap & {default: HttpBodySerializer} = { + "application/json": (body) => Buffer.from(JSON.stringify(body) || "", "utf8"), + "text/plain": (body) => Buffer.from(body?.toString() || "", "utf8"), + "default": (body) => Buffer.from(JSON.stringify(body) || "", "utf8") } export function httpResponseSerializer(bodySerializerMap?: HttpBodySerializerMap) { diff --git a/packages/rest/src/transform/controller-meta.ts b/packages/rest/src/transform/controller-meta.ts index 0cba723..ae7a192 100644 --- a/packages/rest/src/transform/controller-meta.ts +++ b/packages/rest/src/transform/controller-meta.ts @@ -1,7 +1,58 @@ +import { JSONSchema7 } from "json-schema"; +import { OpenAPIV3, OpenAPIV3_1 } from "openapi-types"; import ts from "typescript"; +import { TransformationError } from "./error"; +import { + dereferenceSchema, + getFirstExample, + getSchemaOrAllOf, + getUnifiedPropertySchemas, + joinSchemas, + moveExamplesToExample, + moveRefsToAllOf, + resolveDiscriminantProperty, + rewriteRefsForOpenApi, + unresolveRefs, +} from "./json-schema-utils"; +import { isErrorResult, merge, MergeInput } from "./openapi-merge"; import { Project } from "./project"; import { FileTransformer } from "./transformers/file-transformer"; +export abstract class OpenApiSpecHolder { + private static specFileMap = new Map(); + + public static addSpecForFile(file: ts.SourceFile, spec: OpenAPIV3_1.Document) { + const fileSpecs = this.specFileMap.get(file.fileName) || []; + fileSpecs.push(spec); + this.specFileMap.set(file.fileName, fileSpecs); + } + + public static getSpecForFile(file: ts.SourceFile): OpenAPIV3_1.Document { + const mergeInputs: MergeInput = (this.specFileMap.get(file.fileName) || []) + .map((spec) => ({ + oas: spec, + dispute: {}, + })) as MergeInput; + + if (mergeInputs.length === 0) { + return { + openapi: "3.0.3", + info: { + title: "Nornir API", + version: "1.0.0", + }, + components: {}, + }; + } + const merged = merge(mergeInputs); + if (isErrorResult(merged)) { + throw new Error(merged.message); + } + + return merged.output as OpenAPIV3_1.Document; + } +} + export class ControllerMeta { private static cache = new Map(); private static routes = new Map>(); @@ -11,20 +62,9 @@ export class ControllerMeta { public readonly initializationStatements: ts.Statement[] = []; private instanceProviderExpression: ts.Expression; - // public static getRoutes(): RouteInfo[] { - // const methods = ControllerMeta.routes.values(); - // const routes = Array.from(methods).map((method) => Array.from(method.values())); - // return routes.flat(); - // } - - // public static getAndClearRoutes(): RouteInfo[] { - // const routes = this.getRoutes(); - // this.routes.clear(); - // return routes; - // } - public static clearCache() { this.cache.clear(); + this.routes.clear(); } public static create( @@ -54,14 +94,6 @@ export class ControllerMeta { return this.cache.get(name); } - public static getAssert(route: ts.ClassDeclaration): ControllerMeta { - const meta = this.get(route); - if (!meta) { - throw new Error("Route not found: " + route.getText()); - } - return meta; - } - private constructor( private readonly project: Project, public readonly source: ts.SourceFile, @@ -159,26 +191,18 @@ export class ControllerMeta { return ts.factory.createExpressionStatement(callExpression); } - private getRouteIndex(info: RouteIndex) { + public getRouteIndex(method: string, path: string) { return { - method: info.method, - path: this.basePath + deparameterizePath(info.path).toLowerCase(), + method, + path: normalizeHttpPath(this.basePath + path.toLowerCase()), }; } - public registerRoute(node: ts.Node, routeInfo: { - method: string; - path: string; - description?: string; - summary?: string; - input: ts.Type; - output: ts.Type; - filePath: string; - }) { + public registerRoute(method: string, path: string, info: Omit) { if (this.project.transformOnly) { return; } - const index = this.getRouteIndex(routeInfo); + const index = this.getRouteIndex(method, path); const methods = ControllerMeta.routes.get(index.path) || new Map(); ControllerMeta.routes.set(index.path, methods); const route = methods.get(index.method); @@ -186,208 +210,214 @@ export class ControllerMeta { throw new Error(`Route already registered: ${index.method} ${index.path}`); } - methods.set(index.method, { - method: routeInfo.method, - path: this.basePath + routeInfo.path.toLowerCase(), - description: routeInfo.description, - // requestInfo: this.buildRequestInfo(index, routeInfo.input), - // responseInfo: this.buildResponseInfo(index, routeInfo.output), - filePath: routeInfo.filePath, - summary: routeInfo.summary, + const modifiedRouteInfo = { + index, + description: info.description, + outputSchema: info.outputSchema, + inputSchema: info.inputSchema, + filePath: info.filePath, + summary: info.summary, + deprecated: info.deprecated, + operationId: info.operationId, + tags: info.tags, + input: info.input, + }; + + try { + OpenApiSpecHolder.addSpecForFile(this.source, this.generateRouteSpec(modifiedRouteInfo)); + } catch (e) { + if (e instanceof TransformationError) { + throw e; + } + console.error(e); + throw new TransformationError("Could not generate OpenAPI spec for route", index); + } + methods.set(index.method, modifiedRouteInfo); + } + + private generateRouteSpec(route: RouteInfo): OpenAPIV3_1.Document { + const inputSchema = moveRefsToAllOf(route.inputSchema); + const dereferencedInputSchema = dereferenceSchema(inputSchema); + const outputSchema = moveRefsToAllOf(route.outputSchema); + const dereferencedOutputSchema = dereferenceSchema(outputSchema); + return { + openapi: "3.0.3", + info: { + title: "Nornir API", + version: "1.0.0", + }, + paths: { + [route.index.path]: { + [route.index.method.toLowerCase()]: { + deprecated: route.deprecated, + tags: route.tags, + operationId: route.operationId, + summary: route.summary, + description: route.description, + responses: this.generateOutputType(route.index, dereferencedOutputSchema), + requestBody: this.generateRequestBody(route.index, dereferencedInputSchema), + parameters: [ + ...this.generateParametersForSchemaPath(dereferencedInputSchema, "/pathParams", "path"), + ...this.generateParametersForSchemaPath(dereferencedInputSchema, "/query", "query"), + ...this.generateParametersForSchemaPath(dereferencedInputSchema, "/headers", "header"), + ], + }, + }, + }, + components: { + schemas: { + ...rewriteRefsForOpenApi(moveExamplesToExample(inputSchema)).definitions, + ...rewriteRefsForOpenApi(moveExamplesToExample(outputSchema)).definitions, + }, + parameters: {}, + }, + } as OpenAPIV3_1.Document; + } + + private generateParametersForSchemaPath(inputSchema: JSONSchema7, schemaPath: string, paramType: string) { + const propertySchemas = getUnifiedPropertySchemas(inputSchema, schemaPath); + + return Object.entries(propertySchemas).map(([name, schema]) => { + // Just take the first provided description and example for now + const description = schema.schemaSet.find((schema) => schema.description)?.description; + let example = (schema.schemaSet.find((schema) => schema.examples))?.examples; + if (Array.isArray(example)) { + example = example[0]; + } + + // If every schema is deprecated, then the parameter is deprecated + const deprecated = schema.schemaSet.every((schema) => + (schema as { + deprecated?: boolean; + }).deprecated + ); + + const mergedSchema = schema?.schemaSet.length === 1 + ? schema.schemaSet[0] + : { + anyOf: schema.schemaSet, + }; + + const paramObject: OpenAPIV3_1.ParameterObject = { + name, + in: paramType, + required: schema.required, + description, + example, + deprecated, + schema: rewriteRefsForOpenApi(unresolveRefs(mergedSchema)) as OpenAPIV3.NonArraySchemaObject, + }; + + return paramObject; }); } - // private buildRequestInfo(routeIndex: RouteIndex, inputType: ts.Type): RequestInfo { - // const paramterData: { [key in ParameterType]: { [name: string]: ParameterMeta } } = { - // path: {}, - // header: {}, - // query: {}, - // }; - // const body: { [contentType: string]: Metadata } = {}; - // - // const meta = MetadataFactory.generate( - // this.project.checker, - // ControllerMeta.metadataCollection, - // inputType, - // { resolve: false, constant: true }, - // ); - // for (const object of meta.objects) { - // for (const property of object.properties) { - // const key = MetadataUtils.getSoleLiteral(property.key); - // if (key != null && !isRequestTypeField(key)) { - // throw new Error(`Invalid request field: ${key}`); - // } - // switch (key) { - // case "query": - // this.buildParameterInfo(property.value, "query", paramterData); - // break; - // case "pathParams": - // this.buildParameterInfo(property.value, "path", paramterData); - // break; - // case "headers": - // this.buildParameterInfo(property.value, "header", paramterData); - // break; - // } - // } - // this.buildBodyInfo(routeIndex, object, body); - // } - // - // return { - // body, - // parameters: [ - // ...Object.values(paramterData.path), - // ...Object.values(paramterData.header), - // ...Object.values(paramterData.query), - // ], - // }; - // } - - // private buildResponseInfo(routeIndex: RouteIndex, outputType: ts.Type): ResponseInfo { - // const responses: ResponseInfo = {}; - // const meta = MetadataFactory.generate( - // this.project.checker, - // ControllerMeta.metadataCollection, - // outputType, - // { resolve: false, constant: true }, - // ); - // for (const object of meta.objects) { - // const statusCodeProp = MetadataUtils.getPropertyByStringIndex(object, "statusCode"); - // if (statusCodeProp == null) { - // throw new Error("Response must have a statusCode property"); - // } - // let statusCodes = statusCodeProp.constants.map((c) => c.values).flat().map(v => v.toString()); - // if (HttpStatusCodes.every((c) => statusCodes.includes(c))) { - // strictError( - // this.project, - // new StrictTransformationError( - // "Response status codes must be literal values", - // "Defaulting response status code to 200", - // routeIndex, - // ), - // ); - // statusCodes = ["200"]; - // } - // - // if (statusCodes.length === 0) { - // throw new Error("Literal status codes must be specified"); - // } - // - // for (const statusCode of statusCodes) { - // if (responses[statusCode] != null) { - // throw new Error(`Response already defined for status code ${statusCode}`); - // } - // const headerParamHolder: { [key in ParameterType]: { [name: string]: ParameterMeta } } = { - // path: {}, - // header: {}, - // query: {}, - // }; - // const headerProp = MetadataUtils.getPropertyByStringIndex(object, "headers"); - // if (headerProp != null) { - // this.buildParameterInfo(headerProp, "header", headerParamHolder); - // } - // const result: ResponseInfo[string] = { - // body: {}, - // headers: Object.values(headerParamHolder.header), - // }; - // this.buildBodyInfo(routeIndex, object, result.body); - // responses[statusCode] = result; - // } - // } - // return responses; - // } - - // private buildBodyInfo( - // routeIndex: RouteIndex, - // object: MetadataObject, - // bodyTypes: { [contentType: string]: Metadata }, - // ) { - // let contentType = this.getContentTypeFromObject(object); - // const bodyType = MetadataUtils.getPropertyByStringIndex(object, "body"); - // if (bodyType == null || (bodyType.empty() && !bodyType.nullable)) { - // return; - // } - // if (contentType == null) { - // strictError( - // this.project, - // new StrictTransformationError( - // "No content type specified for body", - // "No content type specified, defaulting to application/json", - // routeIndex, - // ), - // ); - // } - // contentType = contentType || DEFAULT_CONTENT_TYPE; - // const existingBody = bodyTypes[contentType]; - // if (existingBody != null) { - // if (MetadataUtils.equal(existingBody, bodyType)) { - // return; - // } else { - // throw new Error(`Content type ${contentType} already defined`); - // } - // } else { - // bodyTypes[contentType] = bodyType; - // } - // } - - // private getContentTypeFromObject(metaObject: MetadataObject): string | null { - // const headers = MetadataUtils.getPropertyByStringIndex(metaObject, "headers"); - // if (headers == null) { - // return null; - // } - // if (headers.objects.length !== 1) { - // return null; - // } - // const headerObject = headers.objects[0]; - // const contentType = MetadataUtils.getPropertyByStringIndex(headerObject, "content-type"); - // if (contentType == null) { - // return null; - // } - // return MetadataUtils.getSoleLiteral(contentType); - // } - - // private buildParameterInfo( - // inputMeta: Metadata, - // parameterType: ParameterType, - // parameterData: { [key in ParameterType]: { [name: string]: ParameterMeta } }, - // ) { - // for (const object of inputMeta.objects) { - // for (const property of object.properties) { - // const key = MetadataUtils.getSoleLiteral(property.key); - // if (key == null) { - // continue; - // } - // const meta = property.value; - // const existingParameter = parameterData[parameterType][key]; - // if (existingParameter != null) { - // parameterData[parameterType][key] = { - // name: key, - // meta: Metadata.merge(existingParameter.meta, meta), - // type: parameterType, - // }; - // } else { - // parameterData[parameterType][key] = { - // name: key, - // meta, - // type: parameterType, - // }; - // } - // } - // } - // } + private generateOutputType(routeIndex: RouteIndex, outputSchema: JSONSchema7): OpenAPIV3_1.ResponsesObject { + const responses: OpenAPIV3_1.ResponsesObject = {}; + const statusCodeDiscriminatedSchemas = resolveDiscriminantProperty(outputSchema, "/statusCode"); + + if (statusCodeDiscriminatedSchemas == null) { + throw new TransformationError("Could not resolve status codes for some responses", routeIndex); + } + + for (const [statusCode, schema] of Object.entries(statusCodeDiscriminatedSchemas)) { + const headers = this.generateParametersForSchemaPath(schema, "/headers", "header"); + const contentTypeDiscriminatedSchemas = resolveDiscriminantProperty(schema, "/headers/content-type"); + const bodySchema = getUnifiedPropertySchemas(schema, "/")["body"]; + if (contentTypeDiscriminatedSchemas == null && bodySchema != null) { + throw new TransformationError(`Could not resolve content types for "${statusCode}" responses`, routeIndex); + } + + const content = contentTypeDiscriminatedSchemas == null ? undefined : Object.fromEntries( + Object.entries(contentTypeDiscriminatedSchemas).map(([contentType, schema]) => { + const branchBodySchemaSet = getUnifiedPropertySchemas(schema, "/")["body"]; + const branchBodySchema = branchBodySchemaSet != null + ? rewriteRefsForOpenApi( + unresolveRefs(joinSchemas(branchBodySchemaSet.schemaSet)), + ) + : undefined; + const example = branchBodySchema != null ? getFirstExample(branchBodySchema) : undefined; + return [ + contentType, + { + ...(example == null ? {} : { example }), + schema: branchBodySchema as OpenAPIV3.NonArraySchemaObject, + }, + ]; + }), + ); + + responses[statusCode] = { + description: getSchemaOrAllOf(schema).description || "", + headers: Object.fromEntries(headers.map(header => [header.name, header])), + content, + }; + } + + return responses; + } + + private generateRequestBody(routeIndex: RouteIndex, inputSchema: JSONSchema7): OpenAPIV3_1.RequestBodyObject | void { + const bodySchema = getUnifiedPropertySchemas(inputSchema, "/")["body"]; + const contentTypeDiscriminatedSchemas = resolveDiscriminantProperty(inputSchema, "/headers/content-type"); + if (bodySchema == null || (bodySchema.schemaSet.length === 0)) { + return; + } + + if (contentTypeDiscriminatedSchemas == null) { + throw new TransformationError("Could not resolve content types for request body", routeIndex); + } + + const content = Object.fromEntries( + Object.entries(contentTypeDiscriminatedSchemas).map(([contentType, schema]) => { + const branchBodySchemaSet = getUnifiedPropertySchemas(schema, "/")["body"]; + const branchBodySchema = rewriteRefsForOpenApi(unresolveRefs(joinSchemas(branchBodySchemaSet.schemaSet))); + const example = getFirstExample(branchBodySchema); + + return [ + contentType, + { + ...(example == null ? {} : { example }), + schema: branchBodySchema, + }, + ]; + }), + ); + + // If there is exactly one branch, then we can use the description from that branch. + // Otherwise, don't use a description. + const description = Object.keys(content).length === 1 + ? getSchemaOrAllOf(Object.values(content)[0].schema).description + : undefined; + + return { + description, + required: bodySchema.required, + content, + } as OpenAPIV3_1.RequestBodyObject; + } } -function deparameterizePath(path: string) { - return path.replaceAll(/:[^/]+/g, ":param"); +function normalizeHttpPath(path: string) { + const doubleSlashRemoved = path.replace(/\/\//g, "/"); + const endingSlashRemoved = doubleSlashRemoved.endsWith("/") ? doubleSlashRemoved.slice(0, -1) : doubleSlashRemoved; + return endingSlashRemoved.trim(); } export interface RouteInfo { - method: string; - path: string; + index: RouteIndex; description?: string; summary?: string; - // requestInfo: RequestInfo; - // responseInfo: ResponseInfo; + input: ts.TypeNode; + inputSchema: JSONSchema7; + outputSchema: JSONSchema7; filePath: string; + tags?: string[]; + deprecated?: boolean; + operationId?: string; } -export type RouteIndex = Pick; +export interface RouteIndex { + method: string; + path: string; +} diff --git a/packages/rest/src/transform/error.ts b/packages/rest/src/transform/error.ts index f47814c..0245d29 100644 --- a/packages/rest/src/transform/error.ts +++ b/packages/rest/src/transform/error.ts @@ -1,18 +1,18 @@ import ts from "typescript"; import { RouteIndex } from "./controller-meta"; export class TransformationError extends Error { - constructor(message: string, public readonly routeIndex: RouteIndex, public readonly node?: ts.Node) { + constructor(message: string, routeIndex: RouteIndex, node?: ts.Node) { super(message); - this.message += this.getMessageAddendum(); + this.message += this.getMessageAddendum(routeIndex, node); } - public getMessageAddendum() { - const routeMessage = ` - ${this.routeIndex.method} ${this.routeIndex.path}`; + public getMessageAddendum(routeIndex: RouteIndex, node?: ts.Node) { + const routeMessage = ` - ${routeIndex.method} ${routeIndex.path}`; let message = routeMessage; - if (this.node) { - const file: ts.SourceFile = this.node.getSourceFile(); + if (node) { + const file: ts.SourceFile = node.getSourceFile(); const { line, character } = file.getLineAndCharacterOfPosition( - this.node.pos, + node.pos, ); message += `\n${file.fileName}:${line + 1}:${character + 1}`; } @@ -24,6 +24,6 @@ export class StrictTransformationError extends TransformationError { public readonly warningMessage: string; constructor(errorMessage: string, warningMessage: string, routeIndex: RouteIndex, node?: ts.Node) { super(errorMessage, routeIndex, node); - this.warningMessage = warningMessage + this.getMessageAddendum(); + this.warningMessage = warningMessage + this.getMessageAddendum(routeIndex, node); } } diff --git a/packages/rest/src/transform/json-schema-utils.ts b/packages/rest/src/transform/json-schema-utils.ts new file mode 100644 index 0000000..b190025 --- /dev/null +++ b/packages/rest/src/transform/json-schema-utils.ts @@ -0,0 +1,311 @@ +// Traverses the schema and extracts a unified definitions for the properties under the given path +import $RefParser from "@apidevtools/json-schema-ref-parser"; +import dereference from "@apidevtools/json-schema-ref-parser/dist/lib/dereference.js"; +import { JSONSchema7 } from "json-schema"; +import traverse from "json-schema-traverse"; +import { cloneDeep } from "lodash"; + +const refParser = new $RefParser(); + +export function dereferenceSchema(schema: JSONSchema7) { + const clonedSchema = cloneDeep(schema); + refParser.parse(clonedSchema); + refParser.schema = clonedSchema; + dereference(refParser, { + dereference: { + circular: "ignore", + onDereference(path: string, value: JSONSchema7) { + (value as { "x-resolved-ref": string })["x-resolved-ref"] = path; + }, + }, + }); + return clonedSchema; +} + +interface UnifiedPropertySchema { + schemaSet: JSONSchema7[]; + required: boolean; +} + +export function joinSchemas(schemaSet: JSONSchema7[]): JSONSchema7 { + if (schemaSet.length === 0) { + return {}; + } + if (schemaSet.length === 1) { + return schemaSet[0]; + } + + return { + allOf: schemaSet, + }; +} + +export function getUnifiedPropertySchemas( + schema: JSONSchema7, + parentPath: string, +): Record { + // Take a path from the json schema and convert it to a path in validated object + + const convertJsonSchemaPathIfPropertyPath = (path: string) => { + if (path.split("/").at(-2) !== "properties") { + return undefined; + } + + return path + // replace properties + .replace(/\/properties\//g, "/") + // replace items and index + .replace(/\/items\/(\d+)(\/|$)/g, "$2") + // replace anyOf and index + .replace(/\/anyOf\/(\d+)(\/|$)/g, "$2") + // replace oneOf and index + .replace(/\/oneOf\/(\d+)(\/|$)/g, "$2") + // replace allOf and index + .replace(/\/allOf\/(\d+)(\/|$)/g, "$2"); + }; + + const isUnifiedSchemaEmpty = (unifiedSchema: UnifiedPropertySchema) => { + return !unifiedSchema.required && ( + unifiedSchema.schemaSet.length === 0 + || unifiedSchema.schemaSet.every(schema => isSchemaEmpty(schema)) + ); + }; + + const isDirectChildPath = (childPath: string, parentPath: string) => { + const childPathParts = childPath.split("/").filter(part => part !== ""); + const parentPathParts = parentPath.split("/").filter(part => part !== ""); + + if (childPathParts.length !== parentPathParts.length + 1) { + return false; + } + + return parentPathParts.every((part, index) => part === childPathParts[index]); + }; + + const schemas: Record = {}; + let parentSchemas = 0; + + try { + traverse(schema, { + allKeys: false, + cb: { + pre(schema, jsonPtr, rootSchema, parentJsonPtr) { + const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr); + if (convertedPath === parentPath && parentJsonPtr != "") { + parentSchemas++; + } + }, + post: (schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) => { + const convertedPath = convertJsonSchemaPathIfPropertyPath(jsonPtr); + + if (convertedPath != null && isDirectChildPath(convertedPath, parentPath)) { + const schemaSet = schemas[keyIndex || ""] || { + schemaSet: [], + required: true, + }; + schemaSet.required = !schemaSet.required ? false : parentSchema?.required?.includes(keyIndex) ?? false; + schemaSet.schemaSet.push(schema); + schemas[keyIndex || ""] = schemaSet; + } + }, + }, + }); + } catch (e) { + if (e instanceof RangeError) { + throw new Error("Infinite loop detected in json schema"); + } + throw e; + } + + return Object.fromEntries( + Object.entries(schemas).map(([key, value]) => { + return [key, { + schemaSet: value.schemaSet, + required: value.required ? (parentSchemas) <= value.schemaSet.length : false, + }]; + }).filter(([, value]) => !isUnifiedSchemaEmpty(value as UnifiedPropertySchema)), + ); +} + +export function unresolveRefs(schema: JSONSchema7) { + const clonedSchema = cloneDeep(schema); + traverse(clonedSchema, { + cb: { + pre: (subSchema) => { + if (subSchema["x-resolved-ref"]) { + const ref = subSchema["x-resolved-ref"]; + Object.keys(subSchema).forEach(key => delete subSchema[key]); + subSchema.$ref = ref; + } + }, + }, + }); + + return clonedSchema; +} + +const MOVED_REF_MARKER = Symbol("moved-ref-marker"); +export function moveRefsToAllOf(schema: JSONSchema7) { + const clonedSchema = cloneDeep(schema); + traverse(clonedSchema, { + cb: { + pre: (schema) => { + if (schema.$ref && !(schema as { [MOVED_REF_MARKER]: unknown })[MOVED_REF_MARKER]) { + const allOf = schema.allOf || []; + const ref = schema.$ref; // schema.$ref.replace("#/definitions/", "#/components/schemas/"); + delete schema.$ref; + schema.allOf = [ + ...allOf, + { + $ref: ref, + [MOVED_REF_MARKER]: true, + }, + ]; + } + }, + }, + }); + + return clonedSchema; +} + +export function moveExamplesToExample(schema: JSONSchema7) { + const clonedSchema = cloneDeep(schema); + traverse(clonedSchema, { + cb: { + pre: (schema) => { + if (schema.example == null && schema.examples != null && schema.examples.length > 0) { + schema.example = schema.examples[0]; + delete schema.examples; + } + }, + }, + }); + + return clonedSchema; +} + +export function rewriteRefsForOpenApi(schema: JSONSchema7) { + const clonedSchema = cloneDeep(schema); + traverse(clonedSchema, { + cb: { + pre: (schema) => { + if (schema.$ref) { + schema.$ref = schema.$ref.replace("#/definitions/", "#/components/schemas/"); + } + }, + }, + }); + + return clonedSchema; +} + +export function getSchemaOrAllOf(schema: JSONSchema7): JSONSchema7 { + if (schema.allOf != null && schema.allOf.length === 1) { + return schema.allOf[0] as JSONSchema7; + } + + return schema; +} + +export function getFirstExample(schema: JSONSchema7): unknown { + schema = getSchemaOrAllOf(schema); + if ((schema as { example?: string }).example != null) { + return (schema as { example?: string }).example; + } + if (schema.examples != null) { + return Array.isArray(schema.examples) ? schema.examples[0] : schema.examples; + } + return undefined; +} + +export function resolveDiscriminantProperty(schema: JSONSchema7, propertyPath: string) { + type SchemaBranch = { discriminatorValue: string | number; branchSchema: JSONSchema7 }; + if (schema.allOf != null && Object.keys(schema.allOf).length === 1) { + return resolveDiscriminantProperty(schema.allOf[0] as JSONSchema7, propertyPath); + } + + const discriminatorMap: Record = {}; + const addToDiscriminatorMap = (branch: SchemaBranch): boolean => { + if (discriminatorMap[branch.discriminatorValue] != null) { + return false; + } + discriminatorMap[branch.discriminatorValue] = branch.branchSchema; + return true; + }; + + const addManyToDiscriminatorMap = (discriminatorValues: string[], schema: JSONSchema7): boolean => { + return discriminatorValues.every(value => + addToDiscriminatorMap({ discriminatorValue: value, branchSchema: schema }) + ); + }; + + const analyzeBranch = (branchSchema: JSONSchema7, propertyPath: string): void | string[] => { + const parentPath = propertyPath.split("/").slice(0, -1).join("/"); + const propertyName = propertyPath.split("/").at(-1); + + if (propertyName == null) { + return; + } + + const branchSchemaProperties = getUnifiedPropertySchemas(branchSchema as JSONSchema7, parentPath); + const discriminantProperty = branchSchemaProperties[propertyName]; + if (discriminantProperty == null) { + return; + } + if (!discriminantProperty.required) { + return; + } + const discriminatorValues: string[] = []; + + for (const schema of discriminantProperty.schemaSet) { + const resolvedAllOfSchema = getSchemaOrAllOf(schema); + if ( + !(resolvedAllOfSchema.type === "string" || resolvedAllOfSchema.type === "number") + || (resolvedAllOfSchema.const == null && resolvedAllOfSchema.enum == null) + ) { + return; + } + + if (resolvedAllOfSchema.const != null) { + discriminatorValues.push(resolvedAllOfSchema.const as string); + } + if (resolvedAllOfSchema.enum != null) { + discriminatorValues.push(...(resolvedAllOfSchema.enum as string[])); + } + } + + return discriminatorValues; + }; + + if (schema.anyOf == null) { + const result = analyzeBranch(schema, propertyPath); + if (result == null || !addManyToDiscriminatorMap(result, schema)) { + return; + } + return discriminatorMap; + } + + for (const branchSchema of schema.anyOf) { + const result = analyzeBranch(branchSchema as JSONSchema7, propertyPath); + if (result == null || !addManyToDiscriminatorMap(result, branchSchema as JSONSchema7)) { + return; + } + } + + return discriminatorMap; +} + +export function isSchemaEmpty(schema: JSONSchema7): boolean { + const keys = Object.keys(schema); + for (const key of keys) { + if (key.startsWith("x-") || key.startsWith("$")) { + continue; + } + return false; + } + return true; +} diff --git a/packages/rest/src/transform/lib.ts b/packages/rest/src/transform/lib.ts index 9f0814f..7dac363 100644 --- a/packages/rest/src/transform/lib.ts +++ b/packages/rest/src/transform/lib.ts @@ -29,7 +29,8 @@ export function getStringLiteralOrConst(project: Project, node: ts.Expression): export interface NornirDecoratorInfo { decorator: ts.Decorator; - signature: ts.Signature; + symbol: ts.Symbol; + // signature: ts.Signature; declaration: ts.Declaration; } @@ -42,19 +43,34 @@ export function separateNornirDecorators( } { const nornirDecorators: { decorator: ts.Decorator; - signature: ts.Signature; + symbol: ts.Symbol; + // signature: ts.Signature; declaration: ts.Declaration; }[] = []; const decorators: ts.Decorator[] = []; for (const decorator of originalDecorators) { - const signature = project.checker.getResolvedSignature(decorator); - const parentDeclaration = signature?.getDeclaration()?.parent; - if (parentDeclaration && signature && signature.declaration && isNornirRestNode(parentDeclaration)) { + const identifier = ts.isIdentifier(decorator.expression) + ? decorator.expression + : ts.isCallExpression(decorator.expression) + ? decorator.expression.expression as ts.Identifier + : undefined; + if (!identifier) continue; + const identifierSymbol = project.checker.getSymbolAtLocation(identifier); + if (!identifierSymbol) continue; + const symbol = project.checker.getAliasedSymbol(identifierSymbol); + const declaration = symbol?.declarations?.[0]; + if (!declaration) continue; + + // const signature = project.checker.getResolvedSignature(decorator); + // + // const parentDeclaration = signature?.getDeclaration()?.parent; + if (isNornirRestNode(declaration)) { nornirDecorators.push({ decorator, - signature, - declaration: signature.declaration, + symbol, + // signature, + declaration, }); } else { decorators.push(decorator); diff --git a/packages/rest/src/transform/openapi-generator.ts b/packages/rest/src/transform/openapi-generator.ts index 3a1aeef..5a29ac4 100644 --- a/packages/rest/src/transform/openapi-generator.ts +++ b/packages/rest/src/transform/openapi-generator.ts @@ -1,5 +1,3 @@ -/* eslint-disable eslint-comments/disable-enable-pair,unicorn/no-empty-file */ - // import type { OpenAPIV3 } from "openapi-types"; // import { Metadata } from "typia/lib/metadata/Metadata"; // import { ApplicationProgrammer } from "typia/lib/programmers/ApplicationProgrammer"; diff --git a/packages/rest/src/transform/openapi-merge/component-equivalence.ts b/packages/rest/src/transform/openapi-merge/component-equivalence.ts new file mode 100644 index 0000000..1a613f8 --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/component-equivalence.ts @@ -0,0 +1,108 @@ +import { Swagger, SwaggerLookup as Lookup, SwaggerTypeChecks as TC } from "atlassian-openapi"; +import _ from "lodash"; +import { Modify } from "./reference-walker"; + +export type ReferenceWalker = (component: A, modify: Modify) => void; + +function referenceCount(walker: ReferenceWalker, component: A): number { + let count = 0; + walker(component, ref => { + count++; + return ref; + }); + return count; +} + +export function shallowEquality( + referenceWalker: ReferenceWalker, +): (x: A | Swagger.Reference, y: A | Swagger.Reference) => boolean { + return (x: A | Swagger.Reference, y: A | Swagger.Reference): boolean => { + if (!_.isEqual(x, y)) { + return false; + } + + if (TC.isReference(x)) { + return false; + } + + return referenceCount(referenceWalker, x) === 0; + }; +} + +function isSchemaOrThrowError(ref: Swagger.Reference, s: Swagger.Schema | undefined): Swagger.Schema { + if (s === undefined) { + throw new Error(`Could not resolve reference: ${ref.$ref}`); + } + return s; +} + +function arraysEquivalent(leftOriginal: Array, rightOriginal: Array): boolean { + if (leftOriginal.length !== rightOriginal.length) { + return false; + } + + const left = [...leftOriginal].sort(); + const right = [...rightOriginal].sort(); + + for (let index = 0; index < left.length; index++) { + if (left[index] !== right[index]) { + return false; + } + } + + return true; +} + +// The idea is that, if you have made this comparison before, then don't do it again, just return true becauese you have a cycle +type SeenResult = "seen-before" | "new"; + +class ReferenceRecord { + private leftRightSeen: { [leftRef: string]: { [rightRef: string]: boolean } } = {}; + + public checkAndStore(left: Swagger.Reference, right: Swagger.Reference): SeenResult { + if (this.leftRightSeen[left.$ref] === undefined) { + this.leftRightSeen[left.$ref] = {}; + } + + const leftLookup = this.leftRightSeen[left.$ref]; + + const result: SeenResult = leftLookup[right.$ref] === true ? "seen-before" : "new"; + leftLookup[right.$ref] = true; + return result; + } +} + +export function deepEquality( + xLookup: Lookup.Lookup, + yLookup: Lookup.Lookup, +): (x: A | Swagger.Reference, y: A | Swagger.Reference) => boolean { + const refRecord = new ReferenceRecord(); + + function compare(x: T | Swagger.Reference, y: T | Swagger.Reference): boolean { + // If both are references then look up the references and compare them for equality + if (TC.isReference(x) && TC.isReference(y)) { + if (refRecord.checkAndStore(x, y) === "seen-before") { + return true; + } + + const xResult = isSchemaOrThrowError(x, xLookup.getSchema(x)); + const yResult = isSchemaOrThrowError(y, yLookup.getSchema(y)); + return compare(xResult, yResult); + } else if (TC.isReference(x) || TC.isReference(y)) { + return false; + } else if (typeof x === "object" && typeof y === "object") { + // If both are objects then they should have all of the same keys and the values of those keys should match + if (!arraysEquivalent(Object.keys(x as object), Object.keys(y as object))) { + return false; + } + + const xKeys = Object.keys(x as object) as Array; + return xKeys.every(key => compare(x?.[key], y?.[key])); + } + + // If they are not objects or references then you can just run a direct equality + return _.isEqual(x, y); + } + + return compare; +} diff --git a/packages/rest/src/transform/openapi-merge/data.ts b/packages/rest/src/transform/openapi-merge/data.ts new file mode 100644 index 0000000..91cb2ae --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/data.ts @@ -0,0 +1,148 @@ +import { Swagger } from "atlassian-openapi"; + +export type OperationSelection = { + /** + * Only Operations that have these tags will be taken from this OpenAPI file. If a single Operation contains + * an includeTag and an excludeTag then it will be excluded; exclusion takes precedence. + */ + includeTags?: string[]; + + /** + * Any Operation that has any one of these tags will be excluded from the final result. If a single Operation contains + * an includeTag and an excludeTag then it will be excluded; exclusion takes precedence. + */ + excludeTags?: string[]; +}; + +export interface DisputeBase { + /** + * If this is set to true, then this prefix will always be applied to every Schema, even if there is no dispute + * for that particular schema. This may prevent the deduplication of common schemas from different OpenApi files. + */ + alwaysApply?: boolean; + /** + * If this is set to true, then this well deep merge components, bringing all keys and values from identically + * named components in to the one object + */ + mergeDispute?: boolean; +} + +export interface DisputePrefix extends DisputeBase { + /** + * The prefix to use when a schema is in dispute. + */ + prefix: string; +} + +export interface DisputeSuffix extends DisputeBase { + /** + * The suffix to use when a schema is in dispute. + */ + suffix: string; +} + +export type Dispute = DisputePrefix | DisputeSuffix; + +export interface SingleMergeInputBase { + oas: Swagger.SwaggerV3; + + pathModification?: PathModification; + + /** + * Any Operation tagged with one of the paths in this definition will be excluded from the merge result. Any tag + * mentioned in this list will also be excluded from the top level list of tags. + */ + operationSelection?: OperationSelection; + + /** + * This configuration setting lets you configure how the info.description from this OpenAPI file will be merged + * into the final resulting OpenAPI file + */ + description?: DescriptionMergeBehaviour; +} + +/** + * The original SingelMergeInput, now deprecated. This is included for backwards compatibility, to prevent a breaking + * change and should be removed in the next major version. + * + * @deprecated + */ +export interface SingleMergeInputV1 extends SingleMergeInputBase { + /** + * The prefix to use in the event of a dispute. + * + * @deprecated + */ + disputePrefix?: string; +} + +/** + * The current expected format of the SingleMergeInput. + */ +export interface SingleMergeInputV2 extends SingleMergeInputBase { + /** + * This dictates how any disputes will be resolved between similar elements across multiple OpenAPI files. + */ + dispute?: Dispute; + /** + * When set to false, allows operation IDs to be non-uniqiue. Default behaviour is to force a unique suffix unless + * specifically set + */ + uniqueOperations?: boolean; +} + +export type SingleMergeInput = SingleMergeInputV1 | SingleMergeInputV2; + +export type PathModification = { + stripStart?: string; + prepend?: string; +}; + +export type DescriptionMergeBehaviour = { + /** + * Wether or not the description for this OpenAPI file will be merged into the description of the final file. + */ + append: boolean; + + /** + * You may optionally include a Markdown Title to demarcate this particular section of the merged description files. + */ + title?: DescriptionTitle; +}; + +export type DescriptionTitle = { + /** + * The value of the included title. + * + * @minLength 1 + */ + value: string; + + /** + * What heading level this heading will be at: from h1 through to h6. The default value is 1 and will create h1 elements + * in Markdown format. + * + * @minimum 1 + * @maximum 6 + */ + headingLevel?: number; +}; + +export type MergeInput = Array; + +export type SuccessfulMergeResult = { + output: Swagger.SwaggerV3; +}; + +export type ErrorType = "no-inputs" | "duplicate-paths" | "component-definition-conflict" | "operation-id-conflict"; + +export type ErrorMergeResult = { + type: ErrorType; + message: string; +}; + +export function isErrorResult(t: object): t is ErrorMergeResult { + return "type" in t && "message" in t; +} + +export type MergeResult = SuccessfulMergeResult | ErrorMergeResult; diff --git a/packages/rest/src/transform/openapi-merge/dispute.ts b/packages/rest/src/transform/openapi-merge/dispute.ts new file mode 100644 index 0000000..0df94e6 --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/dispute.ts @@ -0,0 +1,35 @@ +import { Dispute, DisputePrefix, SingleMergeInput } from "./data"; + +export function getDispute(input: SingleMergeInput): Dispute | undefined { + if ("disputePrefix" in input) { + if (input.disputePrefix !== undefined) { + return { + prefix: input.disputePrefix, + }; + } + + return undefined; + } else if ("dispute" in input) { + return input.dispute; + } + + return undefined; +} + +export type DisputeStatus = "disputed" | "undisputed"; + +function isDisputePrefix(dispute: Dispute): dispute is DisputePrefix { + return "prefix" in dispute; +} + +export function applyDispute(dispute: Dispute | undefined, input: string, status: DisputeStatus): string { + if (dispute === undefined) { + return input; + } + + if ((status === "disputed" && !dispute.mergeDispute) || dispute.alwaysApply) { + return isDisputePrefix(dispute) ? `${dispute.prefix}${input}` : `${input}${dispute.suffix}`; + } + + return input; +} diff --git a/packages/rest/src/transform/openapi-merge/extensions.ts b/packages/rest/src/transform/openapi-merge/extensions.ts new file mode 100644 index 0000000..015deaf --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/extensions.ts @@ -0,0 +1,51 @@ +import { Swagger } from "atlassian-openapi"; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +type Extensions = { [extensionKey: string]: any }; + +function extractExtensions(input: Swagger.SwaggerV3): Extensions { + const result: Extensions = {}; + + const plainObject: Extensions = input; + + for (const key in plainObject) { + /* eslint-disable-next-line no-prototype-builtins */ + if (key.startsWith("x-") && plainObject.hasOwnProperty(key)) { + result[key] = plainObject[key]; + } + } + + return result; +} + +function mergeExtensionsHelper(extensions: Extensions[]): Extensions { + if (extensions.length === 0) { + return {}; + } + + if (extensions.length === 1) { + return extensions[0]; + } + + const result = { ...extensions[0] }; + + for (let extensionIndex = 1; extensionIndex < extensions.length; extensionIndex++) { + const ext = extensions[extensionIndex]; + + for (const extensionKey in ext) { + /* eslint-disable-next-line no-prototype-builtins */ + if (result[extensionKey] === undefined && ext.hasOwnProperty(extensionKey)) { + result[extensionKey] = ext[extensionKey]; + } + } + } + + return result; +} + +export function mergeExtensions(mergeTarget: Swagger.SwaggerV3, oass: Swagger.SwaggerV3[]): Swagger.SwaggerV3 { + return { + ...mergeTarget, + ...mergeExtensionsHelper([extractExtensions(mergeTarget), ...oass.map(extractExtensions)]), + }; +} diff --git a/packages/rest/src/transform/openapi-merge/index.ts b/packages/rest/src/transform/openapi-merge/index.ts new file mode 100644 index 0000000..afb11c2 --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/index.ts @@ -0,0 +1,56 @@ +import { Swagger } from "atlassian-openapi"; +import { isPresent } from "ts-is-present"; +import { isErrorResult, MergeInput, MergeResult, OperationSelection, PathModification } from "./data"; +import { mergeExtensions } from "./extensions"; +import { mergeInfos } from "./info"; +import { mergePathsAndComponents } from "./paths-and-components"; +import { mergeTags } from "./tags"; + +export { isErrorResult, MergeInput, MergeResult, OperationSelection, PathModification }; + +function getFirst(inputs: Array): A | undefined { + if (inputs.length > 0) { + return inputs[0]; + } + + return undefined; +} + +function getFirstMatching(inputs: Array, extract: (input: A) => B | undefined): B | undefined { + return getFirst(inputs.map(extract).filter(isPresent)); +} + +/** + * Swagger Merge Tool + */ +export function merge(inputs: MergeInput): MergeResult { + if (inputs.length === 0) { + return { type: "no-inputs", message: "You must provide at least one OAS file as an input." }; + } + + const pathAndComponentResult = mergePathsAndComponents(inputs); + + if (isErrorResult(pathAndComponentResult)) { + return pathAndComponentResult; + } + + const { paths, components: retComponents } = pathAndComponentResult; + + const components = Object.keys(retComponents).length === 0 ? undefined : retComponents; + + const output: Swagger.SwaggerV3 = mergeExtensions( + { + openapi: "3.0.3", + info: mergeInfos(inputs), + servers: getFirstMatching(inputs, input => input.oas.servers), + externalDocs: getFirstMatching(inputs, input => input.oas.externalDocs), + security: getFirstMatching(inputs, input => input.oas.security), + tags: mergeTags(inputs), + paths, + components, + }, + inputs.map(input => input.oas), + ); + + return { output }; +} diff --git a/packages/rest/src/transform/openapi-merge/info.ts b/packages/rest/src/transform/openapi-merge/info.ts new file mode 100644 index 0000000..fa932a5 --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/info.ts @@ -0,0 +1,39 @@ +import { Swagger } from "atlassian-openapi"; +import _ from "lodash"; +import { isPresent } from "ts-is-present"; +import { MergeInput, SingleMergeInput } from "./data"; + +function getInfoDescriptionWithHeading(mergeInput: SingleMergeInput): string | undefined { + const { description } = mergeInput.oas.info; + + if (description === undefined) { + return undefined; + } + + const trimmedDescription = description.trimRight(); + + if (mergeInput.description === undefined || mergeInput.description.title === undefined) { + return trimmedDescription; + } + + const { title } = mergeInput.description; + + const headingLevel = title.headingLevel || 1; + + return `${"#".repeat(headingLevel)} ${title.value}\n\n${trimmedDescription}`; +} + +export function mergeInfos(mergeInput: MergeInput): Swagger.Info { + const finalInfo = _.cloneDeep(mergeInput[0].oas.info); + + const appendedDescriptions = mergeInput + .filter(i => i.description && i.description.append) + .map(getInfoDescriptionWithHeading) + .filter(isPresent); + + if (appendedDescriptions.length > 0) { + finalInfo.description = appendedDescriptions.join("\n\n"); + } + + return finalInfo; +} diff --git a/packages/rest/src/transform/openapi-merge/operation-selection.ts b/packages/rest/src/transform/openapi-merge/operation-selection.ts new file mode 100644 index 0000000..f1bd508 --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/operation-selection.ts @@ -0,0 +1,84 @@ +import { Swagger } from "atlassian-openapi"; +import _ from "lodash"; +import { OperationSelection } from "./data"; + +const allMethods: Swagger.Method[] = [ + "get", + "put", + "post", + "delete", + "options", + "head", + "patch", + "trace", +]; + +function operationContainsAnyTag(operation: Swagger.Operation, tags: string[]): boolean { + return operation.tags !== undefined && operation.tags.some(tag => tags.includes(tag)); +} + +function dropOperationsThatHaveTags(originalOas: Swagger.SwaggerV3, excludedTags: string[]): Swagger.SwaggerV3 { + if (excludedTags.length === 0) { + return originalOas; + } + + const oas = _.cloneDeep(originalOas); + + for (const path in oas.paths) { + /* eslint-disable-next-line no-prototype-builtins */ + if (oas.paths.hasOwnProperty(path)) { + const pathItem = oas.paths[path]; + + for (let i = 0; i < allMethods.length; i++) { + const method = allMethods[i]; + const operation = pathItem[method]; + + if (operation !== undefined && operationContainsAnyTag(operation, excludedTags)) { + delete pathItem[method]; + } + } + } + } + + return oas; +} + +function includeOperationsThatHaveTags(originalOas: Swagger.SwaggerV3, includeTags: string[]): Swagger.SwaggerV3 { + if (includeTags.length === 0) { + return originalOas; + } + + const oas = _.cloneDeep(originalOas); + + for (const path in oas.paths) { + /* eslint-disable-next-line no-prototype-builtins */ + if (oas.paths.hasOwnProperty(path)) { + const pathItem = oas.paths[path]; + + for (let i = 0; i < allMethods.length; i++) { + const method = allMethods[i]; + const operation = pathItem[method]; + + if (operation !== undefined && !operationContainsAnyTag(operation, includeTags)) { + delete pathItem[method]; + } + } + } + } + + return oas; +} + +export function runOperationSelection( + originalOas: Swagger.SwaggerV3, + operationSelection: OperationSelection | undefined, +): Swagger.SwaggerV3 { + if (operationSelection === undefined) { + return originalOas; + } + + return dropOperationsThatHaveTags( + includeOperationsThatHaveTags(originalOas, operationSelection.includeTags || []), + operationSelection.excludeTags || [], + ); +} diff --git a/packages/rest/src/transform/openapi-merge/paths-and-components.ts b/packages/rest/src/transform/openapi-merge/paths-and-components.ts new file mode 100644 index 0000000..b69ba7c --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/paths-and-components.ts @@ -0,0 +1,434 @@ +import { Swagger, SwaggerLookup } from "atlassian-openapi"; +import _ from "lodash"; +import { deepEquality } from "./component-equivalence"; +import { Dispute, ErrorMergeResult, MergeInput } from "./data"; +import { applyDispute, getDispute } from "./dispute"; +import { runOperationSelection } from "./operation-selection"; +import { walkAllReferences } from "./reference-walker"; + +export type PathAndComponents = { + paths: Swagger.Paths; + components: Swagger.Components; +}; + +function removeFromStart(input: string, trim: string): string { + if (input.startsWith(trim)) { + return input.substring(trim.length); + } + + return input; +} + +type Components = { [key: string]: A }; +type Equal = (x: A, y: A) => boolean; +type AddModRef = (from: string, to: string) => void; + +function processComponents( + results: Components, + components: Components, + areEqual: Equal, + dispute: Dispute | undefined, + addModifiedReference: AddModRef, +): ErrorMergeResult | undefined { + for (const key in components) { + /* eslint-disable-next-line no-prototype-builtins */ + if (components.hasOwnProperty(key)) { + const component = components[key]; + + const modifiedKey = applyDispute(dispute, key, "undisputed"); + if (modifiedKey !== key) { + addModifiedReference(key, modifiedKey); + } + + if (results[modifiedKey] === undefined || areEqual(results[modifiedKey], component)) { + // Add the schema + results[modifiedKey] = component; + } else { + // Distnguish the name and then add the element + let schemaPlaced = false; + + // Try and use the dispute prefix first + if (dispute !== undefined) { + const preferredSchemaKey = applyDispute(dispute, key, "disputed"); + if (results[preferredSchemaKey] === undefined || areEqual(results[preferredSchemaKey], component)) { + results[preferredSchemaKey] = component; + addModifiedReference(key, preferredSchemaKey); + schemaPlaced = true; + } // Merge deeply if the flag is set in the dispute object + else if (dispute.mergeDispute && Object.keys(results).includes(preferredSchemaKey)) { + results[preferredSchemaKey] = _.merge(results[preferredSchemaKey], component); + addModifiedReference(key, preferredSchemaKey); + schemaPlaced = true; + } + } + + // Incrementally find the right prefix + for (let antiConflict = 1; schemaPlaced === false && antiConflict < 1000; antiConflict++) { + const trySchemaKey = `${key}${antiConflict}`; + + if (results[trySchemaKey] === undefined) { + results[trySchemaKey] = component; + addModifiedReference(key, trySchemaKey); + schemaPlaced = true; + } + } + + // In the unlikely event that we can't find a duplicate, return an error + if (schemaPlaced === false) { + return { + type: "component-definition-conflict", + message: `The "${key}" definition had a duplicate in a previous input and could not be deduplicated.`, + }; + } + } + } + } +} + +function countOperationsInPathItem(pathItem: Swagger.PathItem): number { + let count = 0; + count += pathItem.get !== undefined ? 1 : 0; + count += pathItem.put !== undefined ? 1 : 0; + count += pathItem.post !== undefined ? 1 : 0; + count += pathItem.delete !== undefined ? 1 : 0; + count += pathItem.options !== undefined ? 1 : 0; + count += pathItem.head !== undefined ? 1 : 0; + count += pathItem.patch !== undefined ? 1 : 0; + count += pathItem.trace !== undefined ? 1 : 0; + return count; +} + +function dropPathItemsWithNoOperations(originalOas: Swagger.SwaggerV3): Swagger.SwaggerV3 { + const oas = _.cloneDeep(originalOas); + + for (const path in oas.paths) { + /* eslint-disable-next-line no-prototype-builtins */ + if (oas.paths.hasOwnProperty(path)) { + const pathItem = oas.paths[path]; + + if (countOperationsInPathItem(pathItem) === 0) { + delete oas.paths[path]; + } + } + } + + return oas; +} + +function findUniqueOperationId( + operationId: string, + seenOperationIds: Set, + dispute: Dispute | undefined, +): string | ErrorMergeResult { + if (!seenOperationIds.has(operationId)) { + return operationId; + } + + // Try the dispute prefix + if (dispute !== undefined) { + const disputeOpId = applyDispute(dispute, operationId, "disputed"); + if (!seenOperationIds.has(disputeOpId)) { + return disputeOpId; + } + } + + // Incrementally find the right prefix + for (let antiConflict = 1; antiConflict < 1000; antiConflict++) { + const tryOpId = `${operationId}${antiConflict}`; + if (!seenOperationIds.has(tryOpId)) { + return tryOpId; + } + } + + // Fail with an error + return { + type: "operation-id-conflict", + message: `Could not resolve a conflict for the operationId '${operationId}'`, + }; +} + +function ensureUniqueOperationId( + operation: Swagger.Operation, + seenOperationIds: Set, + dispute: Dispute | undefined, +): ErrorMergeResult | undefined { + if (operation.operationId !== undefined) { + const opId = findUniqueOperationId(operation.operationId, seenOperationIds, dispute); + if (typeof opId === "string") { + operation.operationId = opId; + seenOperationIds.add(opId); + } else { + return opId; + } + } +} + +function ensureUniqueOperationIds( + pathItem: Swagger.PathItem, + seenOperationIds: Set, + dispute: Dispute | undefined, +): ErrorMergeResult | undefined { + const operations = [ + pathItem.get, + pathItem.put, + pathItem.post, + pathItem.delete, + pathItem.patch, + pathItem.head, + pathItem.trace, + pathItem.options, + ]; + + for (let opIndex = 0; opIndex < operations.length; opIndex++) { + const operation = operations[opIndex]; + + if (operation !== undefined) { + const result = ensureUniqueOperationId(operation, seenOperationIds, dispute); + if (result !== undefined) { + return result; + } + } + } +} + +/** + * Merge algorithm: + * + * Generate reference mappings for the components. Eliminating duplicates. + * Generate reference mappings for the paths. + * Copy the elements into the new location. + * Update all of the paths and components to the new references. + * + * @param inputs + */ +export function mergePathsAndComponents(inputs: MergeInput): PathAndComponents | ErrorMergeResult { + const seenOperationIds = new Set(); + + const result: PathAndComponents = { + paths: {}, + components: {}, + }; + + for (let inputIndex = 0; inputIndex < inputs.length; inputIndex++) { + const input = inputs[inputIndex]; + + const { oas: originalOas, pathModification, operationSelection } = input; + const dispute = getDispute(input); + + const oas = dropPathItemsWithNoOperations(runOperationSelection(_.cloneDeep(originalOas), operationSelection)); + + // Original references will be transformed to new non-conflicting references + const referenceModification: { [originalReference: string]: string } = {}; + + // For each component in the original input, place it in the output with deduplicate taking place + if (oas.components !== undefined) { + const resultLookup = new SwaggerLookup.InternalLookup({ + openapi: "3.0.1", + info: { title: "dummy", version: "0" }, + paths: {}, + components: result.components, + }); + const currentLookup = new SwaggerLookup.InternalLookup(oas); + if (oas.components.schemas !== undefined) { + result.components.schemas = result.components.schemas || {}; + + processComponents( + result.components.schemas, + oas.components.schemas, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/schemas/${from}`] = `#/components/schemas/${to}`; + }, + ); + } + + if (oas.components.responses !== undefined) { + result.components.responses = result.components.responses || {}; + + processComponents( + result.components.responses, + oas.components.responses, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/responses/${from}`] = `#/components/responses/${to}`; + }, + ); + } + + if (oas.components.parameters !== undefined) { + result.components.parameters = result.components.parameters || {}; + + processComponents( + result.components.parameters, + oas.components.parameters, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/parameters/${from}`] = `#/components/parameters/${to}`; + }, + ); + } + + // examples + if (oas.components.examples !== undefined) { + result.components.examples = result.components.examples || {}; + + processComponents( + result.components.examples, + oas.components.examples, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/examples/${from}`] = `#/components/examples/${to}`; + }, + ); + } + + // requestBodies + if (oas.components.requestBodies !== undefined) { + result.components.requestBodies = result.components.requestBodies || {}; + + processComponents( + result.components.requestBodies, + oas.components.requestBodies, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/requestBodies/${from}`] = `#/components/requestBodies/${to}`; + }, + ); + } + + // headers + if (oas.components.headers !== undefined) { + result.components.headers = result.components.headers || {}; + + processComponents( + result.components.headers, + oas.components.headers, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/headers/${from}`] = `#/components/headers/${to}`; + }, + ); + } + + // security schemes + if (oas.components.securitySchemes !== undefined) { + result.components.securitySchemes = result.components.securitySchemes || {}; + + processComponents( + result.components.securitySchemes, + oas.components.securitySchemes, + deepEquality(resultLookup, currentLookup), + { prefix: "", mergeDispute: true, ...dispute }, // security should always be merged + (from: string, to: string) => { + referenceModification[`#/components/securitySchemes/${from}`] = `#/components/securitySchemes/${to}`; + }, + ); + } + + // links + if (oas.components.links !== undefined) { + result.components.links = result.components.links || {}; + + processComponents( + result.components.links, + oas.components.links, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/links/${from}`] = `#/components/links/${to}`; + }, + ); + } + + // callbacks + if (oas.components.callbacks !== undefined) { + result.components.callbacks = result.components.callbacks || {}; + + processComponents( + result.components.callbacks, + oas.components.callbacks, + deepEquality(resultLookup, currentLookup), + dispute, + (from: string, to: string) => { + referenceModification[`#/components/callbacks/${from}`] = `#/components/callbacks/${to}`; + }, + ); + } + } + + // For each path, convert it into the right format (looking out for duplicates) + const paths = Object.keys(oas.paths || {}); + + for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) { + const originalPath = paths[pathIndex]; + + const newPath = pathModification === undefined + ? originalPath + : `${pathModification.prepend || ""}${removeFromStart(originalPath, pathModification.stripStart || "")}`; + + if (originalPath !== newPath) { + referenceModification[`#/paths/${originalPath}`] = `#/paths/${newPath}`; + } + + if (result.paths[newPath] !== undefined) { + const operations = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]; + const newMethods = Object.keys(input.oas.paths[originalPath]).filter((method) => + Object.values(operations).includes(method.toLowerCase()) + ); + try { + newMethods.forEach((method) => { + const pathMethod = method.toLowerCase() as Swagger.Method; + if (result.paths[newPath][pathMethod]) { + throw { + type: "duplicate-paths", + message: + `Input ${inputIndex}: The method '${originalPath}:${pathMethod}' is already mapped to '${newPath}:${pathMethod}' and has already been added by another input file`, + }; + } else { + const copyPathItem: Swagger.PathItem = { [method]: oas.paths[originalPath][pathMethod] }; + if (!("uniqueOperations" in input && input.uniqueOperations === false)) { + ensureUniqueOperationIds(copyPathItem, seenOperationIds, dispute); + } + result.paths[newPath][pathMethod] = copyPathItem[pathMethod]; + } + }); + } catch (err) { + return err as ErrorMergeResult; + } + } else { + const copyPathItem = oas.paths[originalPath]; + if (!("uniqueOperations" in input && input.uniqueOperations === false)) { + ensureUniqueOperationIds(copyPathItem, seenOperationIds, dispute); + } + + result.paths[newPath] = copyPathItem; + } + } + + // Update the references to point to the right location + const modifiedKeys = Object.keys(referenceModification); + walkAllReferences(oas, (ref) => { + if (referenceModification[ref] !== undefined) { + return referenceModification[ref]; + } + + const matchingKeys = modifiedKeys.filter((key) => key.startsWith(`${ref}/`)); + + if (matchingKeys.length > 1) { + throw new Error(`Found more than one matching key for reference '${ref}': ${JSON.stringify(matchingKeys)}`); + } else if (matchingKeys.length === 1) { + return referenceModification[matchingKeys[0]]; + } + + return ref; + }); + } + + return result; +} diff --git a/packages/rest/src/transform/openapi-merge/reference-walker.ts b/packages/rest/src/transform/openapi-merge/reference-walker.ts new file mode 100644 index 0000000..1d3ddc1 --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/reference-walker.ts @@ -0,0 +1,315 @@ +import { Swagger, SwaggerTypeChecks as TC } from "atlassian-openapi"; + +export type Modify = (input: string) => string; + +export function walkSchemaReferences(schema: Swagger.Schema | Swagger.Reference, modify: Modify): void { + if (TC.isReference(schema)) { + schema.$ref = modify(schema.$ref); + } else { + if (schema.not !== undefined) walkSchemaReferences(schema.not, modify); + + if (schema.allOf !== undefined) { + for (const childSchema of schema.allOf) { + walkSchemaReferences(childSchema, modify); + } + } + + if (schema.oneOf !== undefined) { + for (const childSchema of schema.oneOf) { + walkSchemaReferences(childSchema, modify); + } + } + + if (schema.anyOf !== undefined) { + for (const childSchema of schema.anyOf) { + walkSchemaReferences(childSchema, modify); + } + } + + if (schema.items !== undefined) { + walkSchemaReferences(schema.items, modify); + } + + for (const propertyKey in schema.properties) { + if (schema.properties.hasOwnProperty(propertyKey)) { + const property = schema.properties[propertyKey]; + walkSchemaReferences(property, modify); + } + } + + if (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== "boolean") { + walkSchemaReferences(schema.additionalProperties, modify); + } + } +} + +export function walkExampleReferences(example: Swagger.Example | Swagger.Reference, modify: Modify): void { + if (TC.isReference(example)) { + example.$ref = modify(example.$ref); + } +} + +function walkMediaTypeReferences(mediaType: Swagger.MediaType, modify: Modify): void { + if (mediaType.schema !== undefined) walkSchemaReferences(mediaType.schema, modify); + + if (TC.isMediaTypeWithExamples(mediaType)) { + if (mediaType.schema !== undefined) walkSchemaReferences(mediaType.schema, modify); + + for (const exampleKey of Object.keys(mediaType.examples)) { + const example = mediaType.examples[exampleKey]; + walkExampleReferences(example, modify); + } + } +} + +export function walkParameterReferences(parameterOrRef: Swagger.ParameterOrRef, modify: Modify): void { + if (TC.isReference(parameterOrRef)) { + parameterOrRef.$ref = modify(parameterOrRef.$ref); + } else if (TC.isParameterWithSchema(parameterOrRef)) { + walkSchemaReferences(parameterOrRef.schema, modify); + + if ("examples" in parameterOrRef) { + for (const exampleKey in parameterOrRef.examples) { + if (parameterOrRef.examples.hasOwnProperty(exampleKey)) { + const example = parameterOrRef.examples[exampleKey]; + walkExampleReferences(example, modify); + } + } + } + } else { + for (const contentKey in parameterOrRef.content) { + if (parameterOrRef.content.hasOwnProperty(contentKey)) { + const mediaType = parameterOrRef.content[contentKey]; + walkMediaTypeReferences(mediaType, modify); + } + } + } +} + +export function walkRequestBodyReferences(requestBody: Swagger.RequestBody | Swagger.Reference, modify: Modify): void { + if (TC.isReference(requestBody)) { + requestBody.$ref = modify(requestBody.$ref); + } else { + for (const contentKey in requestBody.content) { + if (requestBody.content.hasOwnProperty(contentKey)) { + const mediaType = requestBody.content[contentKey]; + walkMediaTypeReferences(mediaType, modify); + } + } + } +} + +export function walkHeaderReferences(header: Swagger.Header | Swagger.Reference, modify: Modify): void { + if (TC.isReference(header)) { + header.$ref = modify(header.$ref); + } else if (TC.isHeaderWithSchema(header)) { + if (header.schema !== undefined) walkSchemaReferences(header.schema, modify); + + if ("examples" in header) { + for (const exampleKey in header.examples) { + if (header.examples.hasOwnProperty(exampleKey)) { + const example = header.examples[exampleKey]; + walkExampleReferences(example, modify); + } + } + } + } else { + for (const contentKey in header.content) { + if (header.content.hasOwnProperty(contentKey)) { + const mediaType = header.content[contentKey]; + walkMediaTypeReferences(mediaType, modify); + } + } + } +} + +export function walkLinkReferences(link: Swagger.Link | Swagger.Reference, modify: Modify): void { + if (TC.isReference(link)) { + link.$ref = modify(link.$ref); + } else { + // TODO work out if there are any references in here that should be updated + } +} + +export function walkResponseReferences(response: Swagger.Response | Swagger.Reference, modify: Modify): void { + if (TC.isReference(response)) { + response.$ref = modify(response.$ref); + } else { + if (response.headers !== undefined) { + for (const headerKey of Object.keys(response.headers)) { + const headerOrRef = response.headers[headerKey]; + walkHeaderReferences(headerOrRef, modify); + } + } + + if (response.content !== undefined) { + const contentKeys = Object.keys(response.content); + for (let contentKeyIndex = 0; contentKeyIndex < contentKeys.length; contentKeyIndex++) { + const contentKey = contentKeys[contentKeyIndex]; + const mediaType = response.content[contentKey]; + walkMediaTypeReferences(mediaType, modify); + } + } + + if (response.links !== undefined) { + const linkKeys = Object.keys(response.links); + for (let linkKeyIndex = 0; linkKeyIndex < linkKeys.length; linkKeyIndex++) { + const linkKey = linkKeys[linkKeyIndex]; + const linkOrRef = response.links[linkKey]; + walkLinkReferences(linkOrRef, modify); + } + } + } +} + +export function walkCallbackReferences(callback: Swagger.Callback | Swagger.Reference, modify: Modify): void { + if (TC.isReference(callback)) { + callback.$ref = modify(callback.$ref); + } else { + for (const pathItemKey in callback) { + if (callback.hasOwnProperty(pathItemKey)) { + const pathItem = callback[pathItemKey]; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + walkPathItemReferences(pathItem, modify); + } + } + } +} + +function walkOperationReferences(operation: Swagger.Operation, modify: Modify): void { + if (operation.parameters !== undefined) { + for (const parameterOrRef of operation.parameters) { + walkParameterReferences(parameterOrRef, modify); + } + } + + if (operation.requestBody !== undefined) { + walkRequestBodyReferences(operation.requestBody, modify); + } + + for (const responseKey in operation.responses) { + if (operation.responses.hasOwnProperty(responseKey)) { + const response = operation.responses[responseKey]; + walkResponseReferences(response, modify); + } + } + + if (operation.callbacks !== undefined) { + const callbackKeys = Object.keys(operation.callbacks); + for (let callbackKeyIndex = 0; callbackKeyIndex < callbackKeys.length; callbackKeyIndex++) { + const callbackKey = callbackKeys[callbackKeyIndex]; + const callback = operation.callbacks[callbackKey]; + walkCallbackReferences(callback, modify); + } + } +} + +function walkPathItemReferences(pathItem: Swagger.PathItem, modify: Modify): void { + if (pathItem["$ref"] !== undefined) { + pathItem["$ref"] = modify(pathItem["$ref"]); + } else { + if (pathItem.get !== undefined) walkOperationReferences(pathItem.get, modify); + if (pathItem.put !== undefined) walkOperationReferences(pathItem.put, modify); + if (pathItem.post !== undefined) walkOperationReferences(pathItem.post, modify); + if (pathItem.delete !== undefined) walkOperationReferences(pathItem.delete, modify); + if (pathItem.options !== undefined) walkOperationReferences(pathItem.options, modify); + if (pathItem.head !== undefined) walkOperationReferences(pathItem.head, modify); + if (pathItem.patch !== undefined) walkOperationReferences(pathItem.patch, modify); + if (pathItem.trace !== undefined) walkOperationReferences(pathItem.trace, modify); + + if (pathItem.parameters !== undefined) { + for (let parameterIndex = 0; parameterIndex < pathItem.parameters.length; parameterIndex++) { + walkParameterReferences(pathItem.parameters[parameterIndex], modify); + } + } + } +} + +export function walkComponentReferences(components: Swagger.Components, modify: Modify): void { + if (components.schemas !== undefined) { + for (const schemaKey in components.schemas) { + if (components.schemas.hasOwnProperty(schemaKey)) { + const schema = components.schemas[schemaKey]; + walkSchemaReferences(schema, modify); + } + } + } + + if (components.responses !== undefined) { + for (const responsesKey in components.responses) { + if (components.responses.hasOwnProperty(responsesKey)) { + const response = components.responses[responsesKey]; + + walkResponseReferences(response, modify); + } + } + } + + if (components.parameters !== undefined) { + for (const parameterKey in components.parameters) { + if (components.parameters.hasOwnProperty(parameterKey)) { + const parameter = components.parameters[parameterKey]; + walkParameterReferences(parameter, modify); + } + } + } + + if (components.examples !== undefined) { + for (const exampleKey in components.examples) { + if (components.examples.hasOwnProperty(exampleKey)) { + const example = components.examples[exampleKey]; + walkExampleReferences(example, modify); + } + } + } + + if (components.requestBodies !== undefined) { + for (const requestBodyKey in components.requestBodies) { + if (components.requestBodies.hasOwnProperty(requestBodyKey)) { + const requestBody = components.requestBodies[requestBodyKey]; + walkRequestBodyReferences(requestBody, modify); + } + } + } + + if (components.headers !== undefined) { + for (const headerKey in components.headers) { + if (components.headers.hasOwnProperty(headerKey)) { + const header = components.headers[headerKey]; + walkHeaderReferences(header, modify); + } + } + } + + if (components.links !== undefined) { + for (const linkKey in components.links) { + if (components.links.hasOwnProperty(linkKey)) { + const link = components.links[linkKey]; + walkLinkReferences(link, modify); + } + } + } + + if (components.callbacks !== undefined) { + for (const componentKey in components.callbacks) { + if (components.callbacks.hasOwnProperty(componentKey)) { + const callback = components.callbacks[componentKey]; + walkCallbackReferences(callback, modify); + } + } + } +} + +export function walkPathReferences(paths: Swagger.Paths, modify: Modify): void { + for (const pathKey in paths) { + if (paths.hasOwnProperty(pathKey)) { + const path = paths[pathKey]; + walkPathItemReferences(path, modify); + } + } +} + +export function walkAllReferences(oas: Swagger.SwaggerV3, modify: Modify): void { + walkPathReferences(oas.paths, modify); + if (oas.components !== undefined) walkComponentReferences(oas.components, modify); +} diff --git a/packages/rest/src/transform/openapi-merge/tags.ts b/packages/rest/src/transform/openapi-merge/tags.ts new file mode 100644 index 0000000..de1df6f --- /dev/null +++ b/packages/rest/src/transform/openapi-merge/tags.ts @@ -0,0 +1,39 @@ +import { Swagger } from "atlassian-openapi"; +import { MergeInput } from "./data"; + +function getNonExcludedTags(originalTags: Swagger.Tag[], excludedTagNames: string[]): Swagger.Tag[] { + if (excludedTagNames.length === 0) { + return originalTags; + } + + return originalTags.filter(tag => !excludedTagNames.includes(tag.name)); +} + +export function mergeTags(inputs: MergeInput): Swagger.Tag[] | undefined { + const result = new Array(); + + const seenTags = new Set(); + inputs.forEach(input => { + const { operationSelection } = input; + const { tags } = input.oas; + if (tags !== undefined) { + const excludeTags = operationSelection !== undefined && operationSelection.excludeTags !== undefined + ? operationSelection.excludeTags + : []; + const nonExcludedTags = getNonExcludedTags(tags, excludeTags); + + nonExcludedTags.forEach(tag => { + if (!seenTags.has(tag.name)) { + seenTags.add(tag.name); + result.push(tag); + } + }); + } + }); + + if (result.length === 0) { + return undefined; + } + + return result; +} diff --git a/packages/rest/src/transform/project.ts b/packages/rest/src/transform/project.ts index a08a45d..ce47cb9 100644 --- a/packages/rest/src/transform/project.ts +++ b/packages/rest/src/transform/project.ts @@ -1,5 +1,17 @@ import type { Options as AJVBaseOptions } from "ajv"; -import { Config, NodeParser, SchemaGenerator, TypeFormatter } from "ts-json-schema-generator"; +import { + BaseType, + Config, + createParser, + NodeParser, + ReferenceType, + SchemaGenerator, + StringType, + SubNodeParser, + TypeFormatter, + UndefinedType, + UnknownType, +} from "ts-json-schema-generator"; import ts from "typescript"; export interface Project { @@ -16,6 +28,7 @@ export interface Project { schemaGenerator: SchemaGenerator; typeFormatter: TypeFormatter; nodeParser: NodeParser; + parsedCommandLine: ts.ParsedCommandLine; } export type AJVOptions = Pick< @@ -40,9 +53,61 @@ export const SCHEMA_DEFAULTS: Config = { jsDoc: "extended", sortProps: true, strictTuples: false, - encodeRefs: true, + encodeRefs: false, additionalProperties: false, topRef: false, + discriminatorType: "open-api", }; export type Options = AJVOptions & SchemaConfig; + +export class TemplateExpressionNodeParser implements SubNodeParser { + supportsNode(node: ts.Node): boolean { + return ts.isTemplateExpression(node); + } + + createType(): BaseType { + return new StringType(); + } +} + +export class UndefinedIdentifierParser implements SubNodeParser { + supportsNode(node: ts.Node): boolean { + return ts.isIdentifier(node) && node.text === "undefined"; + } + + createType(): BaseType { + return new UndefinedType(); + } +} + +export class NornirIgnoreParser implements SubNodeParser { + supportsNode(node: ts.Node): boolean { + // check if the ignore tag is present + return ts.getJSDocTags(node).some(tag => tag.tagName.getText() === "ignore"); + } + + createType(): BaseType { + return new UnknownType(); + } +} + +export class NornirParserThrow implements SubNodeParser { + supportsNode(node: ts.Node) { + return ts.getJSDocTags(node).some(tag => tag.tagName.getText() === "nodeParseThrow"); + } + + createType(node: ts.Node): BaseType { + const throwTagText = ts.getJSDocTags(node).find(tag => tag.tagName.getText() === "nodeParseThrow")?.comment; + throw new Error(`ParserFailure: ${throwTagText}`); + } +} + +export function getSchemaNodeParser(program: ts.Program, config: Config): NodeParser { + return createParser(program as unknown as Parameters[0], config, prs => { + prs.addNodeParser(new TemplateExpressionNodeParser()); + prs.addNodeParser(new UndefinedIdentifierParser()); + prs.addNodeParser(new NornirIgnoreParser()); + prs.addNodeParser(new NornirParserThrow()); + }); +} diff --git a/packages/rest/src/transform/transform.ts b/packages/rest/src/transform/transform.ts index d8752ac..4f93203 100644 --- a/packages/rest/src/transform/transform.ts +++ b/packages/rest/src/transform/transform.ts @@ -1,17 +1,46 @@ -import { createFormatter, createParser, SchemaGenerator } from "ts-json-schema-generator"; +import { + BaseType, + createFormatter, + createParser, + Definition, + SchemaGenerator, + StringType, + SubNodeParser, + SubTypeFormatter, + UndefinedType, +} from "ts-json-schema-generator"; import ts from "typescript"; -import { AJV_DEFAULTS, AJVOptions, Options, Project, SCHEMA_DEFAULTS, SchemaConfig } from "./project.js"; +import { + AJV_DEFAULTS, + AJVOptions, + getSchemaNodeParser, + Options, + Project, + SCHEMA_DEFAULTS, + SchemaConfig, +} from "./project.js"; import { FileTransformer } from "./transformers/file-transformer.js"; let project: Project; -// let files: string[] = []; -// let openapi: OpenAPIV3.Document; + +export class UndefinedFormatter implements SubTypeFormatter { + public supportsType(type: BaseType): boolean { + return type instanceof UndefinedType; + } + + getChildren(): BaseType[] { + return []; + } + + getDefinition(): Definition { + return {}; + } +} export function transform(program: ts.Program, options?: Options): ts.TransformerFactory { const { loopEnum, loopRequired, additionalProperties, - encodeRefs, strictTuples, jsDoc, removeAdditional, @@ -19,6 +48,7 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme allErrors, sortProps, expose, + encodeRefs, } = options ?? {}; const schemaConfig: SchemaConfig = { @@ -40,11 +70,11 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme allErrors: allErrors ?? AJV_DEFAULTS.allErrors, }; - const nodeParser = createParser(program as unknown as Parameters[0], { - ...schemaConfig, - }); + const nodeParser = getSchemaNodeParser(program, schemaConfig); const typeFormatter = createFormatter({ ...schemaConfig, + }, frm => { + frm.addTypeFormatter(new UndefinedFormatter()); }); const schemaGenerator = new SchemaGenerator( @@ -54,6 +84,25 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme schemaConfig, ); + const compilerHost = program.getCompilerOptions().incremental + ? ts.createIncrementalCompilerHost(program.getCompilerOptions()) + : ts.createCompilerHost(program.getCompilerOptions()); + + const configPath = program.getCompilerOptions().configFilePath as string; + + const parseConfigHost: ts.ParseConfigFileHost = { + useCaseSensitiveFileNames: true, + fileExists: fileName => compilerHost!.fileExists(fileName), + readFile: fileName => compilerHost!.readFile(fileName), + directoryExists: f => compilerHost!.directoryExists!(f), + getDirectories: f => compilerHost!.getDirectories!(f), + realpath: compilerHost.realpath, + readDirectory: (...args) => compilerHost!.readDirectory!(...args), + trace: compilerHost.trace, + getCurrentDirectory: compilerHost.getCurrentDirectory, + onUnRecoverableConfigFileDiagnostic: () => {}, + }; + project = { transformOnly: false, program, @@ -67,9 +116,12 @@ export function transform(program: ts.Program, options?: Options): ts.Transforme schemaGenerator, nodeParser, typeFormatter, - compilerHost: program.getCompilerOptions().incremental - ? ts.createIncrementalCompilerHost(program.getCompilerOptions()) - : ts.createCompilerHost(program.getCompilerOptions()), + compilerHost, + parsedCommandLine: ts.getParsedCommandLineOfConfigFile( + configPath, + program.getCompilerOptions(), + parseConfigHost, + )!, }; return (context) => { diff --git a/packages/rest/src/transform/transformers/controller-method-transformer.ts b/packages/rest/src/transform/transformers/controller-method-transformer.ts index 30382b9..83a2965 100644 --- a/packages/rest/src/transform/transformers/controller-method-transformer.ts +++ b/packages/rest/src/transform/transformers/controller-method-transformer.ts @@ -1,5 +1,6 @@ import ts from "typescript"; import { ControllerMeta } from "../controller-meta"; +import { TransformationError } from "../error"; import { NornirDecoratorInfo, separateNornirDecorators } from "../lib"; import { Project } from "../project"; import { ChainMethodDecoratorTypes, ChainRouteProcessor } from "./processors/chain-route-processor"; @@ -18,17 +19,26 @@ export abstract class ControllerMethodTransformer { if (nornirDecorators.length === 0) return node; const methodDecorator = nornirDecorators.find(decorator => { - const name = project.checker.getTypeAtLocation(decorator.declaration.parent).symbol.name; + const name = decorator.symbol.name; return METHOD_DECORATOR_PROCESSORS[name] != undefined; }); if (!methodDecorator) return node; - const method = project.checker.getTypeAtLocation(methodDecorator.declaration.parent).symbol.name; + const method = methodDecorator.symbol.name; if (!method) return node; - return METHOD_DECORATOR_PROCESSORS[method](methodDecorator, project, source, node, controller); + try { + return METHOD_DECORATOR_PROCESSORS[method](methodDecorator, project, source, node, controller); + } catch (e) { + throw e; + console.error(e); + if (e instanceof TransformationError) { + throw e; + } + return node; + } } public static transformControllerMethods( diff --git a/packages/rest/src/transform/transformers/file-transformer.ts b/packages/rest/src/transform/transformers/file-transformer.ts index 487ad93..fe70cb6 100644 --- a/packages/rest/src/transform/transformers/file-transformer.ts +++ b/packages/rest/src/transform/transformers/file-transformer.ts @@ -1,5 +1,6 @@ +import { rmSync } from "fs"; import ts from "typescript"; -import { ControllerMeta } from "../controller-meta"; +import { ControllerMeta, OpenApiSpecHolder } from "../controller-meta"; import { TransformationError } from "../error"; import { Project } from "../project.js"; import { NodeTransformer } from "./node-transformer"; @@ -10,8 +11,18 @@ export abstract class FileTransformer { // return file; const transformed = FileTransformer.iterateNode(project, context, file, file); + const outputFileNames = ts.getOutputFileNames(project.parsedCommandLine, file.fileName, false); + const schemaFileName = `${outputFileNames[0]}.nornir.oas.json`; + + const { compilerHost } = project; const nodesToAdd = FileTransformer.getStatementsForFile(file); - if (nodesToAdd == undefined) return transformed; + if (nodesToAdd == undefined) { + // delete schema file if it exists + if (compilerHost.fileExists(schemaFileName)) { + rmSync(schemaFileName); + } + return transformed; + } const updated = ts.factory.updateSourceFile( transformed, @@ -27,6 +38,10 @@ export abstract class FileTransformer { file.libReferenceDirectives, ); + const mergedSpec = OpenApiSpecHolder.getSpecForFile(file); + + compilerHost.writeFile(schemaFileName, JSON.stringify(mergedSpec, null, 2), false, undefined, []); + FileTransformer.FILE_NODE_MAP.delete(file.fileName); FileTransformer.IMPORT_MAP.delete(file.fileName); ControllerMeta.clearCache(); diff --git a/packages/rest/src/transform/transformers/node-transformer.ts b/packages/rest/src/transform/transformers/node-transformer.ts index adfa6ae..5ed9930 100644 --- a/packages/rest/src/transform/transformers/node-transformer.ts +++ b/packages/rest/src/transform/transformers/node-transformer.ts @@ -10,7 +10,7 @@ export abstract class NodeTransformer { context: ts.TransformationContext, ): ts.Node { if (ts.isClassDeclaration(node)) { - return ClassTransformer.transform(project, source, node, context); + return ClassTransformer.transform(project, source, node as ts.ClassDeclaration, context); } return node; diff --git a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts index e741607..1392a6b 100644 --- a/packages/rest/src/transform/transformers/processors/chain-route-processor.ts +++ b/packages/rest/src/transform/transformers/processors/chain-route-processor.ts @@ -1,7 +1,9 @@ import { schemaToValidator } from "@nrfcloud/ts-json-schema-transformer/utils"; -import { isTypeReference } from "tsutils"; +import tsp from "ts-morph"; import ts from "typescript"; -import { ControllerMeta } from "../../controller-meta"; +import { ControllerMeta, RouteIndex } from "../../controller-meta"; +import { TransformationError } from "../../error"; +import { moveRefsToAllOf } from "../../json-schema-utils"; import { getStringLiteralOrConst, isNornirNode, NornirDecoratorInfo, separateNornirDecorators } from "../../lib"; import { Project } from "../../project"; @@ -15,6 +17,20 @@ export const ChainMethodDecoratorTypeMap = { HeadChain: "HEAD", } as const; +const TagMapper = { + summary: (value?: string) => value, + description: (value?: string) => value, + deprecated: (value?: string) => value != "false", + operationId: (value?: string) => value, + tags: (value?: string) => value?.split(",").map(tag => tag.trim()) || [], +} as const; + +const SupportedTags = Object.keys(TagMapper) as (keyof typeof TagMapper)[]; + +type RouteTags = { + -readonly [K in keyof typeof TagMapper]?: Exclude, undefined>; +}; + export const ChainMethodDecoratorTypes = Object.keys( ChainMethodDecoratorTypeMap, ) as (keyof typeof ChainMethodDecoratorTypeMap)[]; @@ -30,27 +46,34 @@ export abstract class ChainRouteProcessor { const path = ChainRouteProcessor.getPath(project, methodDecorator); const method = ChainRouteProcessor.getMethod(project, methodDecorator); - // const wrappedNode = createWrappedNode(node, { typeChecker: project.checker }) as MethodDeclaration; + const routeIndex = controller.getRouteIndex(method, path); - const inputTypeNode = ChainRouteProcessor.resolveInputType(project, node); - const inputType = project.checker.getTypeFromTypeNode(inputTypeNode); + const { typeNode: inputTypeNode } = ChainRouteProcessor.resolveInputType(project, node, routeIndex); - // const outputType = ChainRouteProcessor.resolveOutputType(project, methodSignature); + const outputType = ChainRouteProcessor.resolveOutputType(project, node, routeIndex); - const inputSchema = project.schemaGenerator.createSchemaFromNodes([inputTypeNode]); + const { inputSchema, outputSchema } = ChainRouteProcessor.generateInputOutputSchema( + project, + routeIndex, + inputTypeNode, + outputType.node, + ); - const inputValidator = schemaToValidator(inputSchema, project.options.validation); + const inputValidator = schemaToValidator(moveRefsToAllOf(inputSchema), project.options.validation); - controller.registerRoute(node, { - method, - path, - input: inputType, + const parsedDocComments = ChainRouteProcessor.parseJSDoc(project, node); + controller.registerRoute(method, path, { + input: inputTypeNode, + inputSchema, + outputSchema: outputSchema, // FIXME: this should be the output type not the input type - output: inputType, - // description, - // summary, + description: parsedDocComments.description, + summary: parsedDocComments.summary, filePath: source.fileName, + deprecated: parsedDocComments.deprecated, + operationId: parsedDocComments.operationId, + tags: parsedDocComments.tags, }); controller.addInitializationStatement( @@ -64,7 +87,7 @@ export abstract class ChainRouteProcessor { ), ); const { otherDecorators } = separateNornirDecorators(project, ts.getDecorators(node) || []); - return ts.factory.createMethodDeclaration( + const recreatedNode = ts.factory.createMethodDeclaration( [...(ts.getModifiers(node) || []), ...otherDecorators], node.asteriskToken, node.name, @@ -74,6 +97,50 @@ export abstract class ChainRouteProcessor { node.type, node.body, ); + + ts.setTextRange(recreatedNode, node); + // ts.setOriginalNode(recreatedNode, node); + + return recreatedNode; + } + + private static generateInputOutputSchema( + project: Project, + routeIndex: RouteIndex, + inputTypeNode: ts.TypeNode, + outputTypeNode: ts.TypeNode, + ) { + try { + const inputSchema = project.schemaGenerator.createSchemaFromNodes([inputTypeNode]); + const outputSchema = project.schemaGenerator.createSchemaFromNodes([outputTypeNode]); + return { + inputSchema, + outputSchema, + }; + } catch (e) { + console.error(e); + throw new TransformationError(`Could not generate schema for route`, routeIndex); + } + } + + private static parseJSDoc(_project: Project, method: ts.MethodDeclaration): RouteTags { + const docs = ts.getJSDocCommentsAndTags(method); + const topLevel = docs[0]; + if (!topLevel) { + return {}; + } + const description = ts.getTextOfJSDocComment(topLevel.comment); + if (!ts.isJSDoc(topLevel)) { + return {}; + } + + const tagSet = (topLevel.tags || []) + .map(tag => [tag.tagName.escapedText as string, ts.getTextOfJSDocComment(tag.comment)] as const) + .filter(tag => SupportedTags.includes(tag[0] as keyof typeof TagMapper)) + .map(tag => [tag[0], TagMapper[tag[0] as keyof typeof TagMapper](tag[1])]); + + tagSet.push(["description", description]); + return Object.fromEntries(tagSet) as RouteTags; } private static generateRouteStatement( @@ -124,47 +191,78 @@ export abstract class ChainRouteProcessor { return path; } - private static getMethod(project: Project, methodDecorator: NornirDecoratorInfo): string { - const name = project.checker.getTypeAtLocation(methodDecorator.declaration.parent).symbol.name; + private static getMethod(_project: Project, methodDecorator: NornirDecoratorInfo): string { + const name = methodDecorator.symbol.name; return ChainMethodDecoratorTypeMap[name as keyof typeof ChainMethodDecoratorTypeMap]; } - private static resolveInputType(project: Project, method: ts.MethodDeclaration): ts.TypeNode { + private static resolveInputType( + project: Project, + method: ts.MethodDeclaration, + routeIndex: RouteIndex, + ): { type: ts.Type; typeNode: ts.TypeNode } { const params = method.parameters; if (params.length !== 1) { - throw new Error("Handler chain must have 1 parameter"); + throw new TransformationError("Handler chain must have 1 parameter", routeIndex); } const [param] = params; - const paramType = project.checker.getTypeAtLocation(param); + const paramTypeNode = param.type; + if (!paramTypeNode || !ts.isTypeReferenceNode(paramTypeNode)) { + throw new TransformationError("Handler chain parameter must have a type", routeIndex); + } + + const paramType = project.checker.getTypeFromTypeNode(paramTypeNode); const paramDeclaration = paramType.symbol?.declarations?.[0]; if (!paramDeclaration || !isNornirNode(paramDeclaration)) { - throw new Error("Handler chain input must be a Nornir class"); + throw new TransformationError("Handler chain input must be a Nornir class", routeIndex); } - // eslint-disable-next-line unicorn/no-useless-undefined - const paramTypeNode = project.checker.typeToTypeNode(paramType, undefined, undefined) as ts.TypeReferenceNode; + // const paramTypeNode = project.checker.typeToTypeNode(paramType, undefined, undefined) as ts.TypeReferenceNode; const [paramTypeArg] = paramTypeNode.typeArguments || []; + + const paramTypeArgType = project.checker.getTypeFromTypeNode(paramTypeArg); + if (!paramTypeArg) { - throw new Error("Handler chain input must have a type argument"); + throw new TransformationError("Handler chain input must have a type argument", routeIndex); } - return paramTypeArg; + return { + type: paramTypeArgType, + typeNode: paramTypeArg, + }; } - private static resolveOutputType(project: Project, methodSignature: ts.Signature): ts.Type { - const returnType = methodSignature.getReturnType(); - const returnTypeDeclaration = returnType.symbol?.declarations?.[0]; - if (!returnTypeDeclaration) { - throw new Error("Handler chain return type declaration not found"); + private static resolveOutputType( + project: Project, + methodDeclaration: ts.MethodDeclaration, + routeIndex: RouteIndex, + ): { type: ts.Type; node: ts.TypeNode } { + const returnedTypeNode = methodDeclaration.type; + + if (returnedTypeNode == null) { + throw new TransformationError( + "Endpoint is missing an explicit return type. Explicit return types are required for all endpoints to promote contract stability", + routeIndex, + ); } - if (!isNornirNode(returnTypeDeclaration)) { - throw new Error("Handler chain return must be a Nornir class"); + if (!ts.isTypeReferenceNode(returnedTypeNode)) { + throw new TransformationError("Endpoint return type must be a type reference", routeIndex); } - if (!isTypeReference(returnType)) { - throw new Error("Handler chain return must use a type reference"); + + const returnType = project.checker.getTypeFromTypeNode(returnedTypeNode); + const returnTypeDeclaration = returnType.symbol.getDeclarations()?.[0]; + if (!returnTypeDeclaration || !isNornirNode(returnTypeDeclaration)) { + throw new TransformationError("Handler chain output must be a Nornir class", routeIndex); } - const typeArguments = project.checker.getTypeArguments(returnType); - const [outputType] = typeArguments; - return outputType; + + const [, returnTypeArg] = returnedTypeNode.typeArguments || []; + if (returnTypeArg == null) { + throw new TransformationError("Could not get the output type arguments", routeIndex); + } + + return { + type: project.checker.getTypeFromTypeNode(returnTypeArg), + node: returnTypeArg, + }; } } diff --git a/packages/rest/src/transform/transformers/processors/controller-processor.ts b/packages/rest/src/transform/transformers/processors/controller-processor.ts index 6fe757b..cfcc2a0 100644 --- a/packages/rest/src/transform/transformers/processors/controller-processor.ts +++ b/packages/rest/src/transform/transformers/processors/controller-processor.ts @@ -27,7 +27,7 @@ export abstract class ControllerProcessor { const { otherDecorators } = separateNornirDecorators(project, ts.getDecorators(transformedNode) || []); - return ts.factory.createClassDeclaration( + const recreatedNode = ts.factory.createClassDeclaration( [...transformedModifiers, ...otherDecorators], transformedNode.name, transformedNode.typeParameters, @@ -37,15 +37,18 @@ export abstract class ControllerProcessor { ...routeMeta.getGeneratedMembers(), ], ); + + ts.setTextRange(recreatedNode, node); + // ts.setOriginalNode(recreatedNode, node); + + return recreatedNode; } private static getArguments(project: Project, nornirDecorators: NornirDecoratorInfo[]): { basePath: string; apiId: string; } { - const basePathDecorator = nornirDecorators.find((decorator) => - project.checker.getTypeAtLocation(decorator.declaration.parent).symbol.name === "Controller" - ); + const basePathDecorator = nornirDecorators.find((decorator) => decorator.symbol.name === "Controller"); if (basePathDecorator == undefined) throw new Error("Controller must have a controller decorator"); if (!ts.isCallExpression(basePathDecorator.decorator.expression)) { throw new Error("Controller decorator is not a call expression"); diff --git a/packages/test/__tests__/src/test.spec.ts b/packages/test/__tests__/src/test.spec.ts index 05de923..3f79cc4 100644 --- a/packages/test/__tests__/src/test.spec.ts +++ b/packages/test/__tests__/src/test.spec.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line unicorn/no-empty-file describe("test something", () => { it.skip("should do something", () => { expect(true).toBe(true); diff --git a/packages/test/__tests__/tsconfig.json b/packages/test/__tests__/tsconfig.json index 6aefe25..bda034e 100644 --- a/packages/test/__tests__/tsconfig.json +++ b/packages/test/__tests__/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "pretty": true, - + "incremental": false, "baseUrl": ".", "outDir": "dist", "rootDir": "src", diff --git a/packages/test/base.openapi.json b/packages/test/base.openapi.json new file mode 100644 index 0000000..d7df19d --- /dev/null +++ b/packages/test/base.openapi.json @@ -0,0 +1,8 @@ +{ + "openapi": "3.1.0", + "info": { + "description": "A test api", + "version": "1.0.0", + "title": "Test API" + } +} diff --git a/packages/test/openapi.json b/packages/test/openapi.json new file mode 100644 index 0000000..d8cb91b --- /dev/null +++ b/packages/test/openapi.json @@ -0,0 +1,567 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Nornir API", + "version": "1.0.0" + }, + "paths": { + "/docs": { + "get": { + "responses": { + "200": { + "description": "", + "headers": { + "content-type": { + "name": "content-type", + "in": "header", + "required": true, + "deprecated": false, + "schema": { + "type": "string", + "const": "text/html" + } + } + }, + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "parameters": [] + } + }, + "/openapi.json": { + "get": { + "responses": { + "200": { + "description": "", + "headers": { + "content-type": { + "name": "content-type", + "in": "header", + "required": true, + "deprecated": false, + "schema": { + "type": "string", + "const": "application/json" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": {} + } + } + } + } + }, + "parameters": [] + } + }, + "/root/basepath/route/{cool}": { + "get": { + "description": "Cool get route", + "responses": { + "200": { + "description": "This is a comment", + "headers": { + "content-type": { + "name": "content-type", + "in": "header", + "required": true, + "deprecated": false, + "schema": { + "type": "string", + "const": "application/json" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bleep": { + "type": "string" + }, + "bloop": { + "type": "number" + } + }, + "required": [ + "bleep", + "bloop" + ], + "additionalProperties": false + } + } + } + }, + "201": { + "description": "This is a comment", + "headers": { + "content-type": { + "name": "content-type", + "in": "header", + "required": true, + "deprecated": false, + "schema": { + "type": "string", + "const": "application/json" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bleep": { + "type": "string" + }, + "bloop": { + "type": "number" + } + }, + "required": [ + "bleep", + "bloop" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "This is a comment on RouteGetOutputError", + "headers": { + "content-type": { + "name": "content-type", + "in": "header", + "required": true, + "deprecated": false, + "schema": { + "type": "string", + "const": "application/json" + } + } + }, + "content": { + "application/json": {} + } + } + }, + "parameters": [ + { + "name": "cool", + "in": "path", + "required": true, + "deprecated": false, + "schema": { + "pattern": "^[a-z]+$", + "allOf": [ + { + "$ref": "#/components/schemas/TestStringType" + } + ] + } + } + ] + }, + "post": { + "deprecated": true, + "tags": [ + "cool" + ], + "operationId": "coolRoute", + "summary": "Cool Route", + "description": "A simple post route", + "responses": { + "200": { + "description": "", + "headers": {} + } + }, + "requestBody": { + "required": true, + "content": { + "text/csv": { + "schema": { + "description": "This is a CSV body", + "examples": [ + "cool,cool2" + ], + "allOf": [ + { + "$ref": "#/components/schemas/TestStringType" + } + ] + } + }, + "application/json": { + "example": { + "cool": "stuff" + }, + "schema": { + "type": "object", + "properties": { + "cool": { + "type": "string", + "description": "This is a cool property", + "minLength": 5 + } + }, + "required": [ + "cool" + ], + "additionalProperties": false, + "description": "A cool json input", + "examples": [ + { + "cool": "stuff" + } + ] + } + }, + "text/plain": { + "example": { + "cool": "stuff" + }, + "schema": { + "type": "object", + "properties": { + "cool": { + "type": "string", + "description": "This is a cool property", + "minLength": 5 + } + }, + "required": [ + "cool" + ], + "additionalProperties": false, + "description": "A cool json input", + "examples": [ + { + "cool": "stuff" + } + ] + } + } + } + }, + "parameters": [ + { + "name": "reallyCool", + "in": "path", + "required": true, + "description": "Very cool property that does a thing", + "example": "true", + "deprecated": false, + "schema": { + "anyOf": [ + { + "deprecated": true, + "allOf": [ + { + "$ref": "#/components/schemas/TestStringType" + } + ] + }, + { + "type": "string", + "enum": [ + "true", + "false" + ], + "description": "Very cool property that does a thing", + "examples": [ + "true" + ], + "pattern": "^[a-z]+$" + } + ] + } + }, + { + "name": "evenCooler", + "in": "path", + "required": false, + "description": "Even cooler property", + "deprecated": false, + "schema": { + "type": "number", + "description": "Even cooler property" + } + }, + { + "name": "test", + "in": "query", + "required": false, + "deprecated": false, + "schema": { + "type": "string", + "const": "boolean" + } + }, + { + "name": "content-type", + "in": "header", + "required": true, + "deprecated": false, + "schema": { + "anyOf": [ + { + "type": "string", + "const": "text/csv" + }, + { + "type": "string", + "const": "application/json" + }, + { + "type": "string", + "const": "text/plain" + } + ] + } + }, + { + "name": "csv-header", + "in": "header", + "required": false, + "description": "This is a CSV header", + "example": "cool,cool2", + "deprecated": false, + "schema": { + "type": "string", + "description": "This is a CSV header", + "examples": [ + "cool,cool2" + ], + "pattern": "^[a-z]+,[a-z]+$", + "minLength": 5 + } + } + ] + } + } + }, + "components": { + "schemas": { + "HttpHeadersWithoutContentType": { + "type": "object", + "additionalProperties": { + "type": [ + "number", + "string" + ] + }, + "properties": { + "content-type": {} + } + }, + "TestStringType": { + "description": "Amazing string", + "pattern": "^[a-z]+$", + "minLength": 5, + "allOf": [ + { + "$ref": "#/components/schemas/Nominal" + } + ] + }, + "Nominal": { + "type": [ + "string" + ], + "description": "Constructs a nominal type of type `T`. Useful to prevent any value of type `T` from being used or modified in places it shouldn't (think `id`s)." + }, + "RouteGetOutputSuccess": { + "type": "object", + "properties": { + "statusCode": { + "type": "string", + "enum": [ + "200", + "201" + ], + "description": "This is a property" + }, + "headers": { + "type": "object", + "properties": { + "content-type": { + "type": "string", + "const": "application/json" + } + }, + "required": [ + "content-type" + ], + "additionalProperties": false + }, + "body": { + "type": "object", + "properties": { + "bleep": { + "type": "string" + }, + "bloop": { + "type": "number" + } + }, + "required": [ + "bleep", + "bloop" + ], + "additionalProperties": false + } + }, + "required": [ + "body", + "headers", + "statusCode" + ], + "additionalProperties": false, + "description": "This is a comment" + }, + "RouteGetOutputError": { + "type": "object", + "properties": { + "statusCode": { + "type": "string", + "const": "400" + }, + "headers": { + "type": "object", + "properties": { + "content-type": { + "type": "string", + "const": "application/json" + } + }, + "required": [ + "content-type" + ], + "additionalProperties": false + }, + "body": {} + }, + "required": [ + "headers", + "statusCode" + ], + "additionalProperties": false, + "description": "This is a comment on RouteGetOutputError" + }, + "RoutePostInputJSONAlias": { + "type": "object", + "properties": { + "headers": { + "anyOf": [ + { + "type": "object", + "properties": { + "content-type": { + "type": "string", + "const": "application/json" + } + }, + "required": [ + "content-type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "content-type": { + "type": "string", + "const": "text/plain" + } + }, + "required": [ + "content-type" + ], + "additionalProperties": false + } + ] + }, + "query": { + "type": "object", + "properties": { + "test": { + "type": "string", + "const": "boolean" + } + }, + "required": [ + "test" + ], + "additionalProperties": false + }, + "body": { + "type": "object", + "properties": { + "cool": { + "type": "string", + "description": "This is a cool property", + "minLength": 5 + } + }, + "required": [ + "cool" + ], + "additionalProperties": false, + "description": "A cool json input", + "examples": [ + { + "cool": "stuff" + } + ] + }, + "pathParams": { + "type": "object", + "properties": { + "reallyCool": { + "type": "string", + "enum": [ + "true", + "false" + ], + "description": "Very cool property that does a thing", + "examples": [ + "true" + ], + "pattern": "^[a-z]+$" + }, + "evenCooler": { + "type": "number", + "description": "Even cooler property" + } + }, + "required": [ + "reallyCool" + ], + "additionalProperties": false + } + }, + "required": [ + "body", + "headers", + "pathParams", + "query" + ], + "additionalProperties": false + } + }, + "parameters": {} + } +} diff --git a/packages/test/package.json b/packages/test/package.json index 0068033..509b568 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -8,15 +8,15 @@ }, "devDependencies": { "@jest/globals": "^29.5.0", - "@nrfcloud/ts-json-schema-transformer": "^1.2.4", + "@nrfcloud/ts-json-schema-transformer": "^1.3.0", "@types/aws-lambda": "^8.10.115", "@types/jest": "^29.4.0", "@types/node": "^18.15.11", "esbuild": "^0.17.18", "eslint": "^8.45.0", "jest": "^29.5.0", - "ts-patch": "^3.0.2", - "typescript": "^5.2.2" + "ts-patch": "^3.1.1", + "typescript": "^5.3.3" }, "engines": { "node": ">=18.0.0", diff --git a/packages/test/src/controller.ts b/packages/test/src/controller.ts index df16f2e..852547a 100644 --- a/packages/test/src/controller.ts +++ b/packages/test/src/controller.ts @@ -1,32 +1,52 @@ import { Nornir } from "@nornir/core"; import { - AnyMimeType, Controller, GetChain, type HttpRequest, HttpRequestEmpty, + HttpResponse, HttpStatusCode, MimeType, PostChain, + ValidateRequestType, + ValidateResponseType, } from "@nornir/rest"; import { assertValid } from "@nrfcloud/ts-json-schema-transformer"; -interface RouteGetInput extends HttpRequestEmpty { - headers: { - // eslint-disable-next-line sonarjs/no-duplicate-string - "content-type": AnyMimeType; +interface RouteGetInput extends HttpRequest { + pathParams: { + /** + * @pattern ^[a-z]+$ + */ + cool: TestStringType; }; } interface RoutePostInputJSON extends HttpRequest { headers: { - // eslint-disable-next-line sonarjs/no-duplicate-string "content-type": MimeType.ApplicationJson; - }; + } | { "content-type": MimeType.TextPlain }; + /** + * A cool json input + * @example { "cool": "stuff" } + */ body: RoutePostBodyInput; query: { test: "boolean"; }; + pathParams: { + /** + * Very cool property that does a thing + * @pattern ^[a-z]+$ + * @example "true" + */ + reallyCool: "true" | "false"; + + /** + * Even cooler property + */ + evenCooler?: number; + }; } interface RoutePostInputCSV extends HttpRequest { @@ -40,10 +60,61 @@ interface RoutePostInputCSV extends HttpRequest { */ "csv-header": string; }; + /** + * This is a CSV body + * @example "cool,cool2" + */ body: TestStringType; + pathParams: { + /** + * @deprecated + */ + reallyCool: TestStringType; + }; } -type RoutePostInput = RoutePostInputJSON | RoutePostInputCSV; +export type RoutePostInput = RoutePostInputCSV | RoutePostInputJSONAlias; + +export type RoutePostInputJSONAlias = RoutePostInputJSON; + +/** + * This is a comment + */ +export interface RouteGetOutputSuccess extends HttpResponse { + /** + * This is a property + */ + statusCode: HttpStatusCode.Ok | HttpStatusCode.Created; + body: { + bleep: string; + bloop: number; + }; + headers: { + "content-type": MimeType.ApplicationJson; + }; +} + +/** + * This is a comment on RouteGetOutputError + */ +export interface RouteGetOutputError extends HttpResponse { + statusCode: HttpStatusCode.BadRequest; + // /** + // * @example { "message": "Bad Request"} + // */ + // body: { + // message: string; + // }; + body: undefined; + headers: { + "content-type": MimeType.ApplicationJson; + }; +} + +/** + * Output of the GET route + */ +export type RouteGetOutput = RouteGetOutputSuccess | RouteGetOutputError; /** * this is a comment @@ -54,6 +125,8 @@ interface RoutePostBodyInput { * @minLength 5 */ cool: string; + + omitted: boolean; } /** @@ -61,7 +134,7 @@ interface RoutePostBodyInput { * @pattern ^[a-z]+$ * @minLength 5 */ -type TestStringType = Nominal; +export type TestStringType = Nominal; export declare class Tagged { protected _nominal_: N; @@ -77,43 +150,56 @@ export declare class Tagged { */ export type Nominal = T & Tagged> = (T & Tagged) | E; -const basePath = "/basepath"; +const overallBase = "/root"; +const basePath = `${overallBase}/basepath`; + +/** + * This is a controller + * @summary This is a summary + */ @Controller(basePath) export class TestController { - static { - console.log("hello"); - } - /** - * A simple get route - * @summary Cool Route + * Cool get route */ - @GetChain("/route") - public getRoute(chain: Nornir) { + @GetChain("/route/:cool") + public getRoute(chain: Nornir): Nornir { return chain .use(input => { assertValid(input); return input; }) - .use(input => input.headers["content-type"]) - .use(contentType => ({ - statusCode: HttpStatusCode.Ok, - body: `Content-Type: ${contentType}`, - headers: { - "content-type": MimeType.TextPlain, + .use(input => input.headers?.toString()) + .use(_contentType => ({ + statusCode: "200" as const, + body: { + bleep: "bloop", + bloop: 5, }, - })); + headers: { + "content-type": "application/json" as const, + } as const, + } as RouteGetOutput)); } - @PostChain("/route") - public postRoute(chain: Nornir) { + + /** + * A simple post route + * @summary Cool Route + * @tags cool + * @deprecated + * @operationId coolRoute + */ + @PostChain("/route/:cool") + public postRoute( + chain: Nornir, + ): Nornir }> { return chain - .use(contentType => ({ + .use(_contentType => ({ statusCode: HttpStatusCode.Ok, - body: `Content-Type: ${contentType}`, - headers: { - "content-type": MimeType.TextPlain, - }, + // body: `Content-Type: ${contentType}`, + headers: {}, + body: "", })); } } diff --git a/packages/test/src/controller2.ts b/packages/test/src/controller2.ts index a91d72b..9519bbe 100644 --- a/packages/test/src/controller2.ts +++ b/packages/test/src/controller2.ts @@ -1,96 +1,93 @@ -import { Nornir } from "@nornir/core"; -import { - AnyMimeType, - Controller, - GetChain, - HttpRequest, - HttpRequestEmpty, - HttpResponse, - HttpResponseEmpty, - HttpStatusCode, - MimeType, - Provider, - PutChain, -} from "@nornir/rest"; - -interface RouteGetInput extends HttpRequestEmpty { - headers: GetHeaders; -} -interface GetHeaders { - "content-type": AnyMimeType; - [key: string]: string; -} - -interface RoutePostInputJSON extends HttpRequest { - headers: { - "content-type": MimeType.ApplicationJson; - }; - body: RoutePostBodyInput; -} - -interface RoutePostInputCSV extends HttpRequest { - headers: { - "content-type": MimeType.TextCsv; - }; - body: string; -} - -type RoutePutInput = RoutePostInputJSON | RoutePostInputCSV; - -interface RoutePostBodyInput { - cool: string; -} - -const basePath = "/basepath/2"; - -@Controller(basePath, "test") -export class TestController { - @Provider() - public static test() { - return new TestController(); - } - - /** - * The second simple GET route. - * @summary Get route - */ - @GetChain("/route") - public getRoute(chain: Nornir) { - return chain - .use(input => input.headers["content-type"]) - .use(contentType => ({ - statusCode: HttpStatusCode.Ok, - body: `Content-Type: ${contentType}`, - headers: { - "content-type": MimeType.TextPlain, - }, - })); - } - - @PutChain("/route") - public postRoute(chain: Nornir): Nornir { - return chain - .use(() => ({ - statusCode: HttpStatusCode.Created, - headers: { - "content-type": AnyMimeType, - }, - })); - } -} - -type PutResponse = PutSuccessResponse | PutBadRequestResponse; - -interface PutSuccessResponse extends HttpResponseEmpty { - statusCode: HttpStatusCode.Created; -} - -interface PutBadRequestResponse extends HttpResponse { - statusCode: HttpStatusCode.BadRequest; - headers: { - "content-type": MimeType.ApplicationJson; - }; - body: { - potato: boolean; - }; -} +// import { Nornir } from "@nornir/core"; +// import { +// Controller, +// GetChain, +// HttpRequest, +// HttpRequestEmpty, +// HttpResponse, +// HttpResponseEmpty, +// Provider, +// PutChain, +// } from "@nornir/rest"; +// +// interface RouteGetInput extends HttpRequestEmpty { +// } +// +// interface RoutePostInputJSON extends HttpRequest { +// headers: { +// "content-type": "application/json"; +// }; +// body: RoutePostBodyInput; +// } +// +// interface RoutePostInputCSV extends HttpRequest { +// headers: { +// "content-type": "text/csv"; +// }; +// body: string; +// } +// +// type RoutePutInput = RoutePostInputJSON | RoutePostInputCSV; +// +// interface RoutePostBodyInput { +// cool: string; +// } +// +// const basePath = "/basepath/2"; +// +// /** +// * This is a second controller +// * @summary This is a summary +// */ +// @Controller(basePath, "test") +// export class TestController { +// @Provider() +// public static test() { +// return new TestController(); +// } +// +// /** +// * The second simple GET route. +// * @summary Get route +// */ +// @GetChain("/route") +// public getRoute(chain: Nornir) { +// return chain +// .use(contentType => ({ +// statusCode: "200", +// body: `Content-Type: ${contentType}`, +// headers: { +// "content-type": "text/plain", +// }, +// } as const)); +// } +// +// /** +// * The second simple PUT route. +// * @summary Put route +// */ +// @PutChain("/route") +// public postRoute(chain: Nornir): Nornir { +// return chain +// .use(() => ({ +// statusCode: "201", +// headers: {}, +// })); +// } +// } +// +// type PutResponse = PutSuccessResponse | PutBadRequestResponse; +// +// interface PutSuccessResponse extends HttpResponseEmpty { +// statusCode: "201"; +// } +// +// interface PutBadRequestResponse extends HttpResponse { +// statusCode: "422"; +// headers: { +// "content-type": "application/json"; +// }; +// body: { +// potato: boolean; +// }; +// } diff --git a/packages/test/src/rest.ts b/packages/test/src/rest.ts index 8a2e88e..0a8f63f 100644 --- a/packages/test/src/rest.ts +++ b/packages/test/src/rest.ts @@ -1,13 +1,11 @@ import nornir from "@nornir/core"; import { - AnyMimeType, ApiGatewayProxyV2, httpErrorHandler, httpEventParser, httpResponseSerializer, HttpStatusCode, mapErrorClass, - MimeType, normalizeEventHeaders, router, startLocalServer, @@ -21,6 +19,7 @@ import type { } from "aws-lambda"; import "./controller.js"; import "./controller2.js"; +import "./docs-controller.js"; import { getMockObject } from "@nrfcloud/ts-json-schema-transformer"; export class TestError implements NodeJS.ErrnoException { @@ -33,19 +32,19 @@ export class TestError implements NodeJS.ErrnoException { const frameworkChain = nornir() .use(normalizeEventHeaders) .use(httpEventParser({ - "text/csv": body => ({ cool: "stuff" }), + "text/csv": _body => ({ cool: "stuff" }), })) .use(router()) .useResult(httpErrorHandler([ - mapErrorClass(TestError, (err) => ({ - statusCode: HttpStatusCode.InternalServerError, - headers: { - "content-type": AnyMimeType, - }, + mapErrorClass(TestError, (_err) => ({ + statusCode: HttpStatusCode.BadRequest, + headers: {}, })), ])) .use(httpResponseSerializer({ - [MimeType.ApplicationZip]: () => Buffer.from(""), + ["application/bzip"]: () => Buffer.from(""), + ["text/csv"]: (input) => Buffer.from(input as string), + ["text/html"]: (input) => Buffer.from(input as string), })); export const handler: APIGatewayProxyHandlerV2 = nornir() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00d31ca..f788bbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,10 +31,10 @@ importers: version: 18.15.11 '@typescript-eslint/eslint-plugin': specifier: ^6.2.0 - version: 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.2.2) + version: 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^6.2.0 - version: 6.2.0(eslint@8.45.0)(typescript@5.2.2) + version: 6.2.0(eslint@8.45.0)(typescript@5.3.3) dprint: specifier: ^0.34.5 version: 0.34.5 @@ -52,16 +52,10 @@ importers: version: 3.2.0(eslint@8.45.0) eslint-plugin-jest: specifier: ^27.2.3 - version: 27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.2.2) + version: 27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.3.3) eslint-plugin-no-secrets: specifier: ^0.8.9 version: 0.8.9(eslint@8.45.0) - eslint-plugin-sonarjs: - specifier: ^0.19.0 - version: 0.19.0(eslint@8.45.0) - eslint-plugin-unicorn: - specifier: ^48.0.0 - version: 48.0.0(eslint@8.45.0) eslint-plugin-workspaces: specifier: ^0.9.0 version: 0.9.0 @@ -87,14 +81,14 @@ importers: specifier: ^9.8.4 version: 9.8.4 ts-patch: - specifier: ^3.0.2 - version: 3.0.2 + specifier: ^3.1.1 + version: 3.1.1 turbo: specifier: ^1.9.2 version: 1.9.2 typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.3.3 + version: 5.3.3 packages/core: devDependencies: @@ -102,8 +96,8 @@ importers: specifier: ^29.5.0 version: 29.5.0 '@nrfcloud/ts-json-schema-transformer': - specifier: ^1.2.4 - version: 1.2.4(typescript@5.2.2) + specifier: ^1.3.0 + version: 1.3.0(typescript@5.3.3) '@types/jest': specifier: ^29.4.0 version: 29.4.0 @@ -120,41 +114,65 @@ importers: specifier: ^29.5.0 version: 29.5.0(@types/node@18.15.11) ts-patch: - specifier: ^3.0.2 - version: 3.0.2 + specifier: ^3.1.1 + version: 3.1.1 typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.3.3 + version: 5.3.3 packages/rest: dependencies: + '@apidevtools/json-schema-ref-parser': + specifier: ^11.1.0 + version: 11.1.0 '@nornir/core': specifier: workspace:^ version: link:../core '@nrfcloud/ts-json-schema-transformer': - specifier: ^1.2.4 - version: 1.2.4(typescript@5.2.2) + specifier: ^1.3.0 + version: 1.3.0(typescript@5.3.3) '@types/aws-lambda': specifier: ^8.10.115 version: 8.10.115 ajv: specifier: ^8.12.0 version: 8.12.0 + atlassian-openapi: + specifier: ^1.0.18 + version: 1.0.18 + glob: + specifier: ^10.3.10 + version: 10.3.10 + json-schema-traverse: + specifier: ^1.0.0 + version: 1.0.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + openapi-diff: + specifier: ^0.23.6 + version: 0.23.6(openapi-types@12.1.0) openapi-types: specifier: ^12.1.0 version: 12.1.0 trouter: specifier: ^3.2.1 version: 3.2.1 + ts-is-present: + specifier: ^1.2.2 + version: 1.2.2 ts-json-schema-generator: - specifier: ^1.4.0 - version: 1.4.0 + specifier: ^1.5.0 + version: 1.5.0 ts-morph: - specifier: ^19.0.0 - version: 19.0.0 + specifier: ^21.0.1 + version: 21.0.1 tsutils: specifier: ^3.21.0 - version: 3.21.0(typescript@5.2.2) + version: 3.21.0(typescript@5.3.3) + yargs: + specifier: ^17.7.2 + version: 17.7.2 devDependencies: '@jest/globals': specifier: ^29.5.0 @@ -162,9 +180,18 @@ importers: '@types/jest': specifier: ^29.4.0 version: 29.4.0 + '@types/json-schema': + specifier: ^7.0.15 + version: 7.0.15 + '@types/lodash': + specifier: ^4.14.202 + version: 4.14.202 '@types/node': specifier: ^18.15.11 version: 18.15.11 + '@types/yargs': + specifier: ^17.0.32 + version: 17.0.32 eslint: specifier: ^8.45.0 version: 8.45.0 @@ -172,11 +199,11 @@ importers: specifier: ^29.5.0 version: 29.5.0(@types/node@18.15.11) ts-patch: - specifier: ^3.0.2 - version: 3.0.2 + specifier: ^3.1.1 + version: 3.1.1 typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.3.3 + version: 5.3.3 packages/scripts: {} @@ -193,8 +220,8 @@ importers: specifier: ^29.5.0 version: 29.5.0 '@nrfcloud/ts-json-schema-transformer': - specifier: ^1.2.4 - version: 1.2.4(typescript@5.2.2) + specifier: ^1.3.0 + version: 1.3.0(typescript@5.3.3) '@types/aws-lambda': specifier: ^8.10.115 version: 8.10.115 @@ -214,11 +241,11 @@ importers: specifier: ^29.5.0 version: 29.5.0(@types/node@18.15.11) ts-patch: - specifier: ^3.0.2 - version: 3.0.2 + specifier: ^3.1.1 + version: 3.1.1 typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.3.3 + version: 5.3.3 packages: @@ -240,11 +267,63 @@ packages: engines: {node: '>= 16'} dependencies: '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.12 + '@types/json-schema': 7.0.15 '@types/lodash.clonedeep': 4.5.7 js-yaml: 4.1.0 lodash.clonedeep: 4.5.0 + /@apidevtools/json-schema-ref-parser@11.1.0: + resolution: {integrity: sha512-g/VW9ZQEFJAOwAyUb8JFf7MLiLy2uEB4rU270rGzDwICxnxMlPy0O11KVePSgS36K1NI29gSlK84n5INGhd4Ag==} + engines: {node: '>= 16'} + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + '@types/lodash.clonedeep': 4.5.7 + js-yaml: 4.1.0 + lodash.clonedeep: 4.5.0 + dev: false + + /@apidevtools/json-schema-ref-parser@9.0.9: + resolution: {integrity: sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==} + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + dev: false + + /@apidevtools/json-schema-ref-parser@9.1.2: + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + dev: false + + /@apidevtools/openapi-schemas@2.1.0: + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + dev: false + + /@apidevtools/swagger-methods@3.0.2: + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + dev: false + + /@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.0): + resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} + peerDependencies: + openapi-types: '>=7' + dependencies: + '@apidevtools/json-schema-ref-parser': 9.1.2 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + '@jsdevtools/ono': 7.1.3 + call-me-maybe: 1.0.2 + openapi-types: 12.1.0 + z-schema: 5.0.5 + dev: false + /@babel/code-frame@7.21.4: resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} engines: {node: '>=6.9.0'} @@ -488,11 +567,6 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-identifier@7.22.5: - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} - engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-option@7.21.0: resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} engines: {node: '>=6.9.0'} @@ -1681,6 +1755,15 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.9: + resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true optional: true /@esbuild/android-arm@0.17.18: @@ -1689,6 +1772,15 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.9: + resolution: {integrity: sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true optional: true /@esbuild/android-x64@0.17.18: @@ -1697,6 +1789,15 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.9: + resolution: {integrity: sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true optional: true /@esbuild/darwin-arm64@0.17.18: @@ -1705,6 +1806,15 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.9: + resolution: {integrity: sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true optional: true /@esbuild/darwin-x64@0.17.18: @@ -1713,6 +1823,15 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.9: + resolution: {integrity: sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true optional: true /@esbuild/freebsd-arm64@0.17.18: @@ -1721,6 +1840,15 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.9: + resolution: {integrity: sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true optional: true /@esbuild/freebsd-x64@0.17.18: @@ -1729,6 +1857,15 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.9: + resolution: {integrity: sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true optional: true /@esbuild/linux-arm64@0.17.18: @@ -1737,6 +1874,15 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.9: + resolution: {integrity: sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-arm@0.17.18: @@ -1745,6 +1891,15 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.9: + resolution: {integrity: sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-ia32@0.17.18: @@ -1753,6 +1908,15 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.9: + resolution: {integrity: sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-loong64@0.17.18: @@ -1761,6 +1925,15 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.9: + resolution: {integrity: sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-mips64el@0.17.18: @@ -1769,6 +1942,15 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.9: + resolution: {integrity: sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-ppc64@0.17.18: @@ -1777,6 +1959,15 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.9: + resolution: {integrity: sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-riscv64@0.17.18: @@ -1785,6 +1976,15 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.9: + resolution: {integrity: sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-s390x@0.17.18: @@ -1793,6 +1993,15 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.9: + resolution: {integrity: sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-x64@0.17.18: @@ -1801,6 +2010,15 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.9: + resolution: {integrity: sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true optional: true /@esbuild/netbsd-x64@0.17.18: @@ -1809,6 +2027,15 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.9: + resolution: {integrity: sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true optional: true /@esbuild/openbsd-x64@0.17.18: @@ -1817,6 +2044,15 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.9: + resolution: {integrity: sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true optional: true /@esbuild/sunos-x64@0.17.18: @@ -1825,6 +2061,15 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.9: + resolution: {integrity: sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true optional: true /@esbuild/win32-arm64@0.17.18: @@ -1833,6 +2078,15 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.9: + resolution: {integrity: sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true optional: true /@esbuild/win32-ia32@0.17.18: @@ -1841,6 +2095,15 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.9: + resolution: {integrity: sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true optional: true /@esbuild/win32-x64@0.17.18: @@ -1849,6 +2112,15 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.9: + resolution: {integrity: sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.45.0): @@ -1925,6 +2197,18 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.0.1 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: false + /@istanbuljs/load-nyc-config@1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -2150,7 +2434,7 @@ packages: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 18.15.11 - '@types/yargs': 17.0.24 + '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -2241,18 +2525,18 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - /@nrfcloud/ts-json-schema-transformer@1.2.4(typescript@5.2.2): - resolution: {integrity: sha512-JyZkblORtJCRzE2wOYQC3pIeXjkxg/irNYtC7ufcErCPBB9NHzvqMoAaIPu0bZJdxCLvLKL4qTh7NYpm1gg/Gg==} + /@nrfcloud/ts-json-schema-transformer@1.3.0(typescript@5.3.3): + resolution: {integrity: sha512-b/WjwnLSYtikoADzb6gNPLM4BVELePplR2xOKEYTvj4ozGITlX6RaHVc70SXm4GwyX+riagL3dW2MmFBeM6NLw==} engines: {node: '>=18.0.0'} peerDependencies: typescript: '>=5' dependencies: '@apidevtools/json-schema-ref-parser': 10.1.0 ajv: 8.12.0 - esbuild: 0.17.18 + esbuild: 0.19.9 json-schema-faker: /@jfconley/json-schema-faker@0.5.0-rcv.48 - ts-json-schema-generator: 1.4.0 - typescript: 5.2.2 + ts-json-schema-generator: 1.5.0 + typescript: 5.3.3 /@parcel/source-map@2.1.1: resolution: {integrity: sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==} @@ -2261,6 +2545,13 @@ packages: detect-libc: 1.0.3 dev: true + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + /@sinclair/typebox@0.25.24: resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} dev: true @@ -2277,12 +2568,12 @@ packages: '@sinonjs/commons': 2.0.0 dev: true - /@ts-morph/common@0.20.0: - resolution: {integrity: sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q==} + /@ts-morph/common@0.22.0: + resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==} dependencies: - fast-glob: 3.2.12 - minimatch: 7.4.6 - mkdirp: 2.1.6 + fast-glob: 3.3.2 + minimatch: 9.0.3 + mkdirp: 3.0.1 path-browserify: 1.0.1 dev: false @@ -2364,8 +2655,8 @@ packages: pretty-format: 29.5.0 dev: true - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} /@types/liftoff@4.0.0: resolution: {integrity: sha512-Ny/PJkO6nxWAQnaet8q/oWz15lrfwvdvBpuY4treB0CSsBO1CG0fVuNLngR3m3bepQLd+E4c3Y3DlC2okpUvPw==} @@ -2377,10 +2668,10 @@ packages: /@types/lodash.clonedeep@4.5.7: resolution: {integrity: sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==} dependencies: - '@types/lodash': 4.14.194 + '@types/lodash': 4.14.202 - /@types/lodash@4.14.194: - resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} + /@types/lodash@4.14.202: + resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} /@types/minimist@1.2.2: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} @@ -2420,13 +2711,13 @@ packages: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true - /@types/yargs@17.0.24: - resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} dependencies: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin@6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.2.2): + /@typescript-eslint/eslint-plugin@6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.3.3): resolution: {integrity: sha512-rClGrMuyS/3j0ETa1Ui7s6GkLhfZGKZL3ZrChLeAiACBE/tRc1wq8SNZESUuluxhLj9FkUefRs2l6bCIArWBiQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2438,10 +2729,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 6.2.0(eslint@8.45.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.2.0(eslint@8.45.0)(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.2.0 - '@typescript-eslint/type-utils': 6.2.0(eslint@8.45.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.2.0(eslint@8.45.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.2.0 debug: 4.3.4 eslint: 8.45.0 @@ -2450,13 +2741,13 @@ packages: natural-compare: 1.4.0 natural-compare-lite: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.2.0(eslint@8.45.0)(typescript@5.2.2): + /@typescript-eslint/parser@6.2.0(eslint@8.45.0)(typescript@5.3.3): resolution: {integrity: sha512-igVYOqtiK/UsvKAmmloQAruAdUHihsOCvplJpplPZ+3h4aDkC/UKZZNKgB6h93ayuYLuEymU3h8nF1xMRbh37g==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2468,11 +2759,11 @@ packages: dependencies: '@typescript-eslint/scope-manager': 6.2.0 '@typescript-eslint/types': 6.2.0 - '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.2.0 debug: 4.3.4 eslint: 8.45.0 - typescript: 5.2.2 + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true @@ -2493,7 +2784,7 @@ packages: '@typescript-eslint/visitor-keys': 6.2.0 dev: true - /@typescript-eslint/type-utils@6.2.0(eslint@8.45.0)(typescript@5.2.2): + /@typescript-eslint/type-utils@6.2.0(eslint@8.45.0)(typescript@5.3.3): resolution: {integrity: sha512-DnGZuNU2JN3AYwddYIqrVkYW0uUQdv0AY+kz2M25euVNlujcN2u+rJgfJsBFlUEzBB6OQkUqSZPyuTLf2bP5mw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2503,12 +2794,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.2.0(eslint@8.45.0)(typescript@5.3.3) debug: 4.3.4 eslint: 8.45.0 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true @@ -2523,7 +2814,7 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@5.59.2(typescript@5.2.2): + /@typescript-eslint/typescript-estree@5.59.2(typescript@5.3.3): resolution: {integrity: sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -2538,13 +2829,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - tsutils: 3.21.0(typescript@5.2.2) - typescript: 5.2.2 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.2.0(typescript@5.2.2): + /@typescript-eslint/typescript-estree@6.2.0(typescript@5.3.3): resolution: {integrity: sha512-Mts6+3HQMSM+LZCglsc2yMIny37IhUgp1Qe8yJUYVyO6rHP7/vN0vajKu3JvHCBIy8TSiKddJ/Zwu80jhnGj1w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2559,24 +2850,24 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.59.2(eslint@8.45.0)(typescript@5.2.2): + /@typescript-eslint/utils@5.59.2(eslint@8.45.0)(typescript@5.3.3): resolution: {integrity: sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - '@types/json-schema': 7.0.12 + '@types/json-schema': 7.0.15 '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 5.59.2 '@typescript-eslint/types': 5.59.2 - '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 5.59.2(typescript@5.3.3) eslint: 8.45.0 eslint-scope: 5.1.1 semver: 7.5.4 @@ -2585,18 +2876,18 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.2.0(eslint@8.45.0)(typescript@5.2.2): + /@typescript-eslint/utils@6.2.0(eslint@8.45.0)(typescript@5.3.3): resolution: {integrity: sha512-RCFrC1lXiX1qEZN8LmLrxYRhOkElEsPKTVSNout8DMzf8PeWoQG7Rxz2SadpJa3VSh5oYKGwt7j7X/VRg+Y3OQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - '@types/json-schema': 7.0.12 + '@types/json-schema': 7.0.15 '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 6.2.0 '@typescript-eslint/types': 6.2.0 - '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.3.3) eslint: 8.45.0 semver: 7.5.4 transitivePeerDependencies: @@ -2697,12 +2988,10 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-regex@6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} - dev: true /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -2716,13 +3005,17 @@ packages: engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} dev: true + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -2789,11 +3082,31 @@ packages: engines: {node: '>=0.10.0'} dev: true + /assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: false + + /atlassian-openapi@1.0.18: + resolution: {integrity: sha512-IXgF/cYD8DW1mYB/ejDm/lKQMNXi2iCsxus2Y0ffZOxfa/SLoz0RuEZ4xu4suSRjtlda7qZDonQ6TAkQPVuQig==} + dependencies: + jsonpointer: 5.0.1 + urijs: 1.19.11 + dev: false + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} dev: true + /axios@0.24.0: + resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} + dependencies: + follow-redirects: 1.15.3 + transitivePeerDependencies: + - debug + dev: false + /babel-jest@29.5.0(@babel/core@7.21.4): resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2995,11 +3308,6 @@ packages: ieee754: 1.2.1 dev: true - /builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - dev: true - /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -3007,6 +3315,10 @@ packages: get-intrinsic: 1.2.1 dev: true + /call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3107,13 +3419,6 @@ packages: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} dev: true - /clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true - /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -3158,7 +3463,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} @@ -3189,7 +3493,6 @@ packages: engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} @@ -3197,7 +3500,6 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true /commander@10.0.0: resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} @@ -3208,6 +3510,23 @@ packages: resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} engines: {node: '>=16'} + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: false + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + requiresBuild: true + dev: false + optional: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -3234,6 +3553,10 @@ packages: browserslist: 4.21.5 dev: true + /core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: false + /cosmiconfig@8.0.0: resolution: {integrity: sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==} engines: {node: '>=14'} @@ -3259,7 +3582,6 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -3412,6 +3734,10 @@ packages: - supports-color dev: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + /electron-to-chromium@1.4.380: resolution: {integrity: sha512-XKGdI4pWM78eLH2cbXJHiBnWUwFSzZM7XujsB6stDiGu9AeSqziedP6amNLpJzE3i0rLTcfAwdCTs5ecP5yeSg==} dev: true @@ -3423,7 +3749,10 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false /enquirer@2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} @@ -3535,11 +3864,40 @@ packages: '@esbuild/win32-arm64': 0.17.18 '@esbuild/win32-ia32': 0.17.18 '@esbuild/win32-x64': 0.17.18 + dev: true + + /esbuild@0.19.9: + resolution: {integrity: sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.9 + '@esbuild/android-arm64': 0.19.9 + '@esbuild/android-x64': 0.19.9 + '@esbuild/darwin-arm64': 0.19.9 + '@esbuild/darwin-x64': 0.19.9 + '@esbuild/freebsd-arm64': 0.19.9 + '@esbuild/freebsd-x64': 0.19.9 + '@esbuild/linux-arm': 0.19.9 + '@esbuild/linux-arm64': 0.19.9 + '@esbuild/linux-ia32': 0.19.9 + '@esbuild/linux-loong64': 0.19.9 + '@esbuild/linux-mips64el': 0.19.9 + '@esbuild/linux-ppc64': 0.19.9 + '@esbuild/linux-riscv64': 0.19.9 + '@esbuild/linux-s390x': 0.19.9 + '@esbuild/linux-x64': 0.19.9 + '@esbuild/netbsd-x64': 0.19.9 + '@esbuild/openbsd-x64': 0.19.9 + '@esbuild/sunos-x64': 0.19.9 + '@esbuild/win32-arm64': 0.19.9 + '@esbuild/win32-ia32': 0.19.9 + '@esbuild/win32-x64': 0.19.9 /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} - dev: true /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -3576,7 +3934,7 @@ packages: ignore: 5.2.4 dev: true - /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.2.2): + /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.2.0)(eslint@8.45.0)(jest@29.5.0)(typescript@5.3.3): resolution: {integrity: sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -3589,8 +3947,8 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.2.2) - '@typescript-eslint/utils': 5.59.2(eslint@8.45.0)(typescript@5.2.2) + '@typescript-eslint/eslint-plugin': 6.2.0(@typescript-eslint/parser@6.2.0)(eslint@8.45.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.59.2(eslint@8.45.0)(typescript@5.3.3) eslint: 8.45.0 jest: 29.5.0(@types/node@18.15.11) transitivePeerDependencies: @@ -3607,15 +3965,6 @@ packages: eslint: 8.45.0 dev: true - /eslint-plugin-sonarjs@0.19.0(eslint@8.45.0): - resolution: {integrity: sha512-6+s5oNk5TFtVlbRxqZN7FIGmjdPCYQKaTzFPmqieCmsU1kBYDzndTeQav0xtQNwZJWu5awWfTGe8Srq9xFOGnw==} - engines: {node: '>=14'} - peerDependencies: - eslint: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - eslint: 8.45.0 - dev: true - /eslint-plugin-turbo@1.9.3(eslint@8.45.0): resolution: {integrity: sha512-ZsRtksdzk3v+z5/I/K4E50E4lfZ7oYmLX395gkrUMBz4/spJlYbr+GC8hP9oVNLj9s5Pvnm9rLv/zoj5PVYaVw==} peerDependencies: @@ -3624,30 +3973,6 @@ packages: eslint: 8.45.0 dev: true - /eslint-plugin-unicorn@48.0.0(eslint@8.45.0): - resolution: {integrity: sha512-8fk/v3p1ro34JSVDBEmtOq6EEQRpMR0iTir79q69KnXFZ6DJyPkT3RAi+ZoTqhQMdDSpGh8BGR68ne1sP5cnAA==} - engines: {node: '>=16'} - peerDependencies: - eslint: '>=8.44.0' - dependencies: - '@babel/helper-validator-identifier': 7.22.5 - '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - ci-info: 3.8.0 - clean-regexp: 1.0.0 - eslint: 8.45.0 - esquery: 1.5.0 - indent-string: 4.0.0 - is-builtin-module: 3.2.1 - jsesc: 3.0.2 - lodash: 4.17.21 - pluralize: 8.0.0 - read-pkg-up: 7.0.1 - regexp-tree: 0.1.27 - regjsparser: 0.10.0 - semver: 7.5.4 - strip-indent: 3.0.0 - dev: true - /eslint-plugin-workspaces@0.9.0: resolution: {integrity: sha512-krMuZ+yZgzwv1oTBfz50oamNVPDIm7CDyot3i1GRKBqMD2oXAwnXHLQWH7ctpV8k6YVrkhcaZhuV9IJxD8OPAQ==} dependencies: @@ -3838,6 +4163,11 @@ packages: tmp: 0.0.33 dev: true + /extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3850,6 +4180,18 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: false /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3965,6 +4307,16 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /follow-redirects@1.15.3: + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -3983,6 +4335,14 @@ packages: for-in: 1.0.2 dev: true + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: false + /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -4056,7 +4416,6 @@ packages: /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - dev: true /get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} @@ -4098,6 +4457,18 @@ packages: is-glob: 4.0.3 dev: true + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: false + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -4442,13 +4813,6 @@ packages: has-tostringtag: 1.0.0 dev: true - /is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} - dependencies: - builtin-modules: 3.3.0 - dev: true - /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -4487,7 +4851,6 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} @@ -4646,7 +5009,6 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true /isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} @@ -4699,6 +5061,15 @@ packages: istanbul-lib-report: 3.0.0 dev: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false + /jest-changed-files@29.5.0: resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5154,16 +5525,35 @@ packages: hasBin: true dev: true - /jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true - dev: true - /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true + /json-schema-diff@0.17.1: + resolution: {integrity: sha512-bBflcH+NRM/bKbw2G0WIh0ltCZb3PCyruTdopx3hZaXSHKM1+F7ILfDzyl9CxbLAS40/6EhkBYQUMFBefhBkgg==} + engines: {node: '>=6.14.1'} + hasBin: true + dependencies: + ajv: 8.12.0 + commander: 7.2.0 + json-schema-ref-parser: 9.0.9 + json-schema-spec-types: 0.1.2 + lodash: 4.17.21 + verror: 1.10.1 + dev: false + + /json-schema-ref-parser@9.0.9: + resolution: {integrity: sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==} + engines: {node: '>=10'} + deprecated: Please switch to @apidevtools/json-schema-ref-parser + dependencies: + '@apidevtools/json-schema-ref-parser': 9.0.9 + dev: false + + /json-schema-spec-types@0.1.2: + resolution: {integrity: sha512-MDl8fA8ONckmQOm2+eXKJaFJNvxk7eGin+XFofNjS3q3PRKSoEvgMVb0ehOpCAYkUiLoMiqdU7obV7AmzAmyLw==} + dev: false + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -5202,6 +5592,16 @@ packages: resolution: {integrity: sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==} engines: {node: '>=10.0.0'} + /jsonpointer@4.1.0: + resolution: {integrity: sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==} + engines: {node: '>=0.10.0'} + dev: false + + /jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + dev: false + /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -5281,7 +5681,10 @@ packages: /lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -5293,7 +5696,6 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -5317,6 +5719,11 @@ packages: tslib: 2.5.0 dev: true + /lru-cache@10.1.0: + resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + engines: {node: 14 || >=16.14} + dev: false + /lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} dependencies: @@ -5433,9 +5840,9 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@7.4.6: - resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} - engines: {node: '>=10'} + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 dev: false @@ -5453,6 +5860,11 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: false + /mixme@0.5.9: resolution: {integrity: sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==} engines: {node: '>= 8.0.0'} @@ -5464,8 +5876,8 @@ packages: hasBin: true dev: true - /mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + /mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true dev: false @@ -5618,10 +6030,35 @@ packages: is-wsl: 2.2.0 dev: true + /openapi-diff@0.23.6(openapi-types@12.1.0): + resolution: {integrity: sha512-ukUueb7BnumShfwpuwYs0ncE/WWLggph1L6IN5En02sEkaN0RrUFp/a0WgE/NNRm5S2JX/dvemsdUNrKPqiw5Q==} + engines: {node: '>=6.11.4'} + hasBin: true + dependencies: + axios: 0.24.0 + commander: 8.3.0 + js-yaml: 4.1.0 + json-schema-diff: 0.17.1 + jsonpointer: 4.1.0 + lodash: 4.17.21 + openapi3-ts: 2.0.2 + swagger-parser: 10.0.3(openapi-types@12.1.0) + verror: 1.10.1 + transitivePeerDependencies: + - debug + - openapi-types + dev: false + /openapi-types@12.1.0: resolution: {integrity: sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA==} dev: false + /openapi3-ts@2.0.2: + resolution: {integrity: sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==} + dependencies: + yaml: 1.10.2 + dev: false + /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -5794,7 +6231,6 @@ packages: /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: true /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -5812,6 +6248,14 @@ packages: path-root-regex: 0.1.2 dev: true + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.1.0 + minipass: 7.0.4 + dev: false + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -5873,11 +6317,6 @@ packages: v8flags: 4.0.0 dev: true - /pluralize@8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - dev: true - /preferred-pm@3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} @@ -6022,11 +6461,6 @@ packages: '@babel/runtime': 7.21.5 dev: true - /regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - dev: true - /regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} @@ -6053,13 +6487,6 @@ packages: unicode-match-property-value-ecmascript: 2.1.0 dev: true - /regjsparser@0.10.0: - resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} - hasBin: true - dependencies: - jsesc: 0.5.0 - dev: true - /regjsparser@0.9.1: resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} hasBin: true @@ -6070,7 +6497,6 @@ packages: /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - dev: true /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} @@ -6242,7 +6668,6 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: true /shebang-regex@1.0.0: resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} @@ -6252,7 +6677,6 @@ packages: /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: true /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} @@ -6266,6 +6690,11 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: false + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -6380,7 +6809,15 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: false /string.prototype.trim@1.2.7: resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} @@ -6418,14 +6855,12 @@ packages: engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true /strip-ansi@7.0.1: resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} engines: {node: '>=12'} dependencies: ansi-regex: 6.0.1 - dev: true /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} @@ -6480,6 +6915,15 @@ packages: engines: {node: '>= 0.4'} dev: true + /swagger-parser@10.0.3(openapi-types@12.1.0): + resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} + engines: {node: '>=10'} + dependencies: + '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.0) + transitivePeerDependencies: + - openapi-types + dev: false + /syncpack@9.8.4: resolution: {integrity: sha512-i81rO+dHuJ2dO8YQq6SCExcyN0x9ZVTY7cVPn8pWjS5Dml0A8uM0cOaneOludFesdrLXMZUA/uEWa74ddBgkPQ==} engines: {node: '>=14'} @@ -6560,37 +7004,41 @@ packages: regexparam: 1.3.0 dev: false - /ts-api-utils@1.0.1(typescript@5.2.2): + /ts-api-utils@1.0.1(typescript@5.3.3): resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.2.2 + typescript: 5.3.3 dev: true - /ts-json-schema-generator@1.4.0: - resolution: {integrity: sha512-wm8vyihmGgYpxrqRshmYkWGNwEk+sf3xV2rUgxv8Ryeh7bSpMO7pZQOht+2rS002eDkFTxR7EwRPXVzrS0WJTg==} + /ts-is-present@1.2.2: + resolution: {integrity: sha512-cA5MPLWGWYXvnlJb4TamUUx858HVHBsxxdy8l7jxODOLDyGYnQOllob2A2jyDghGa5iJHs2gzFNHvwGJ0ZfR8g==} + dev: false + + /ts-json-schema-generator@1.5.0: + resolution: {integrity: sha512-RkiaJ6YxGc5EWVPfyHxszTmpGxX8HC2XBvcFlAl1zcvpOG4tjjh+eXioStXJQYTvr9MoK8zCOWzAUlko3K0DiA==} engines: {node: '>=10.0.0'} hasBin: true dependencies: - '@types/json-schema': 7.0.12 + '@types/json-schema': 7.0.15 commander: 11.0.0 glob: 8.1.0 json5: 2.2.3 normalize-path: 3.0.0 safe-stable-stringify: 2.4.3 - typescript: 5.2.2 + typescript: 5.3.3 - /ts-morph@19.0.0: - resolution: {integrity: sha512-D6qcpiJdn46tUqV45vr5UGM2dnIEuTGNxVhg0sk5NX11orcouwj6i1bMqZIz2mZTZB1Hcgy7C3oEVhAT+f6mbQ==} + /ts-morph@21.0.1: + resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==} dependencies: - '@ts-morph/common': 0.20.0 + '@ts-morph/common': 0.22.0 code-block-writer: 12.0.0 dev: false - /ts-patch@3.0.2: - resolution: {integrity: sha512-iTg8euqiNsNM1VDfOsVIsP0bM4kAVXU38n7TGQSkky7YQX/syh6sDPIRkvSS0HjT8ZOr0pq1h+5Le6jdB3hiJQ==} + /ts-patch@3.1.1: + resolution: {integrity: sha512-ReGYz9jQYC80PFafBx25TC0UI9cSgmUBtpT+WIy8IrhpLVzEHf430k03XQYOMldQMyZDBbzn5fBPELgtIl65cA==} hasBin: true dependencies: chalk: 4.1.2 @@ -6608,14 +7056,14 @@ packages: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} dev: true - /tsutils@3.21.0(typescript@5.2.2): + /tsutils@3.21.0(typescript@5.3.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.2.2 + typescript: 5.3.3 /tty-table@4.2.1: resolution: {integrity: sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==} @@ -6767,8 +7215,8 @@ packages: is-typed-array: 1.1.12 dev: true - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true @@ -6859,6 +7307,10 @@ packages: dependencies: punycode: 2.3.0 + /urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -6889,6 +7341,20 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + engines: {node: '>= 0.10'} + dev: false + + /verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + dev: false + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -6947,7 +7413,6 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: true /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -6969,7 +7434,15 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.0.1 + dev: false /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -6993,7 +7466,6 @@ packages: /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - dev: true /yallist@2.1.2: resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} @@ -7007,6 +7479,11 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: false + /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} @@ -7023,7 +7500,6 @@ packages: /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - dev: true /yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} @@ -7053,7 +7529,6 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true /yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} @@ -7067,6 +7542,18 @@ packages: engines: {node: '>=10'} dev: true + /z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.11.0 + optionalDependencies: + commander: 9.5.0 + dev: false + /zod@3.20.6: resolution: {integrity: sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==} dev: true