From 61013b65bc32a672e14827db0b90f31abf7593ce Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Tue, 24 Oct 2023 18:16:00 -0400 Subject: [PATCH 01/11] move OpenApiParameters into types.ts --- src/server/dispatcher.ts | 12 +----------- src/server/response-builder.ts | 2 +- src/server/types.ts | 7 +++++++ 3 files changed, 9 insertions(+), 12 deletions(-) create mode 100644 src/server/types.ts diff --git a/src/server/dispatcher.ts b/src/server/dispatcher.ts index a90d2443..0506391a 100644 --- a/src/server/dispatcher.ts +++ b/src/server/dispatcher.ts @@ -1,5 +1,4 @@ /* eslint-disable import/newline-after-import */ -/* eslint-disable max-lines */ import { mediaTypes } from "@hapi/accept"; import createDebugger from "debug"; @@ -18,6 +17,7 @@ import { type OpenApiOperation, } from "./response-builder.js"; import { Tools } from "./tools.js"; +import type { OpenApiParameters } from "./types.js"; const debug = createDebugger("counterfact:server:dispatcher"); @@ -42,14 +42,6 @@ interface ParameterTypes { }; } -interface OpenApiParameters { - in: "body" | "cookie" | "formData" | "header" | "path" | "query"; - name: string; - schema?: { - type: string; - }; -} - export interface OpenApiDocument { paths: { [key: string]: { @@ -304,5 +296,3 @@ export class Dispatcher { return normalizedResponse; } } - -export type { OpenApiParameters }; diff --git a/src/server/response-builder.ts b/src/server/response-builder.ts index 1b1194fd..1abd9570 100644 --- a/src/server/response-builder.ts +++ b/src/server/response-builder.ts @@ -1,6 +1,6 @@ import { JSONSchemaFaker, type Schema } from "json-schema-faker"; -import type { OpenApiParameters } from "./dispatcher.js"; +import type { OpenApiParameters } from "./types.js"; interface ResponseBuilder { [status: number | `${number} ${string}`]: ResponseBuilder; diff --git a/src/server/types.ts b/src/server/types.ts new file mode 100644 index 00000000..f4c1fad0 --- /dev/null +++ b/src/server/types.ts @@ -0,0 +1,7 @@ +export interface OpenApiParameters { + in: "body" | "cookie" | "formData" | "header" | "path" | "query"; + name: string; + schema?: { + type: string; + }; +} From 1301eda9f8a2778abac8b00e496b0f5aea3c1dc9 Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Tue, 24 Oct 2023 19:27:28 -0400 Subject: [PATCH 02/11] put types shared by server and application code in types.d.ts --- bin/counterfact.js | 4 +- package.json | 2 +- src/server/dispatcher.ts | 7 +- src/server/registry.ts | 3 +- src/server/response-builder.ts | 61 +----- src/server/types.d.ts | 201 ++++++++++++++++++ src/server/types.ts | 7 - src/typescript-generator/repository.js | 20 +- .../response-type-coder.js | 8 +- templates/response-builder-factory.ts | 2 +- test/server/response-builder.test.ts | 6 +- .../__snapshots__/generate.test.ts.snap | 64 +++--- 12 files changed, 256 insertions(+), 129 deletions(-) create mode 100644 src/server/types.d.ts delete mode 100644 src/server/types.ts diff --git a/bin/counterfact.js b/bin/counterfact.js index ae55ac65..af54ca32 100755 --- a/bin/counterfact.js +++ b/bin/counterfact.js @@ -6,8 +6,8 @@ import { program } from "commander"; import createDebug from "debug"; import open from "open"; -import { migrate } from "../dist/src/migrations/0.27.js"; -import { counterfact } from "../dist/src/server/app.js"; +import { migrate } from "../dist/migrations/0.27.js"; +import { counterfact } from "../dist/server/app.js"; const DEFAULT_PORT = 3100; diff --git a/package.json b/package.json index 3af79382..18b7600c 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box --forceExit", "test:black-box": "rimraf dist && rimraf out && yarn build && yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest black-box --forceExit --coverage=false", "test:mutants": "stryker run stryker.config.json", - "build": "tsc && copyfiles src/client/** dist && copyfiles templates/** dist", + "build": "tsc && copyfiles -f 'src/client/**' dist/client", "prepack": "yarn build", "release": "npx changeset publish", "prepare": "husky install", diff --git a/src/server/dispatcher.ts b/src/server/dispatcher.ts index 0506391a..8e63b892 100644 --- a/src/server/dispatcher.ts +++ b/src/server/dispatcher.ts @@ -12,12 +12,9 @@ import type { NormalizedCounterfactResponseObject, Registry, } from "./registry.js"; -import { - createResponseBuilder, - type OpenApiOperation, -} from "./response-builder.js"; +import { createResponseBuilder } from "./response-builder.js"; import { Tools } from "./tools.js"; -import type { OpenApiParameters } from "./types.js"; +import type { OpenApiOperation, OpenApiParameters } from "./types.d.ts"; const debug = createDebugger("counterfact:server:dispatcher"); diff --git a/src/server/registry.ts b/src/server/registry.ts index 8de9bae8..8fc30fe2 100644 --- a/src/server/registry.ts +++ b/src/server/registry.ts @@ -1,8 +1,7 @@ import createDebugger from "debug"; -import type { ResponseBuilderFactory } from "../../templates/response-builder-factory.js"; -import type { MediaType } from "./response-builder.js"; import type { Tools } from "./tools.js"; +import type { MediaType, ResponseBuilderFactory } from "./types.d.ts"; const debug = createDebugger("counterfact:server:registry"); diff --git a/src/server/response-builder.ts b/src/server/response-builder.ts index 1abd9570..1cf8ce44 100644 --- a/src/server/response-builder.ts +++ b/src/server/response-builder.ts @@ -1,28 +1,6 @@ -import { JSONSchemaFaker, type Schema } from "json-schema-faker"; - -import type { OpenApiParameters } from "./types.js"; - -interface ResponseBuilder { - [status: number | `${number} ${string}`]: ResponseBuilder; - content?: { body: unknown; type: string }[]; - header: (name: string, value: string) => ResponseBuilder; - headers: { [name: string]: string }; - html: (body: unknown) => ResponseBuilder; - json: (body: unknown) => ResponseBuilder; - match: (contentType: string, body: unknown) => ResponseBuilder; - random: () => ResponseBuilder; - randomLegacy: () => ResponseBuilder; - status?: number; - text: (body: unknown) => ResponseBuilder; -} - -interface OpenApiHeader { - schema: unknown; -} +import { JSONSchemaFaker } from "json-schema-faker"; -interface OpenApiContent { - schema: unknown; -} +import type { OpenApiOperation, ResponseBuilder } from "./types.d.ts"; JSONSchemaFaker.option("useExamplesValue", true); JSONSchemaFaker.option("minItems", 0); @@ -52,36 +30,6 @@ function unknownStatusCodeResponse(statusCode: number | undefined) { }; } -interface Example { - description: string; - summary: string; - value: unknown; -} - -export type MediaType = `${string}/${string}`; - -export interface OpenApiResponse { - content: { [key: MediaType]: OpenApiContent }; - headers: { [key: string]: OpenApiHeader }; -} - -export interface OpenApiOperation { - parameters?: OpenApiParameters[]; - produces?: string[]; - responses: { - [status: string]: { - content?: { - [type: number | string]: { - examples?: { [key: string]: Example }; - schema: unknown; - }; - }; - examples?: { [key: string]: unknown }; - schema?: Schema; - }; - }; -} - export function createResponseBuilder( operation: OpenApiOperation, ): ResponseBuilder { @@ -176,7 +124,8 @@ export function createResponseBuilder( const body = response.examples ? oneOf(response.examples) - : JSONSchemaFaker.generate(response.schema ?? { type: "object" }); + : // eslint-disable-next-line total-functions/no-unsafe-readonly-mutable-assignment + JSONSchemaFaker.generate(response.schema ?? { type: "object" }); return { ...this, @@ -196,3 +145,5 @@ export function createResponseBuilder( }), }); } + +export type { OpenApiOperation }; diff --git a/src/server/types.d.ts b/src/server/types.d.ts new file mode 100644 index 00000000..0881c427 --- /dev/null +++ b/src/server/types.d.ts @@ -0,0 +1,201 @@ +interface OpenApiHeader { + schema: unknown; +} + +interface OpenApiContent { + schema: unknown; +} + +interface Example { + description: string; + summary: string; + value: unknown; +} + +type MediaType = `${string}/${string}`; + +type OmitValueWhenNever = Pick< + Base, + { + [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key; + }[keyof Base] +>; + +interface OpenApiResponse { + content: { [key: MediaType]: OpenApiContent }; + headers: { [key: string]: OpenApiHeader }; +} + +interface OpenApiResponses { + [key: string]: OpenApiResponse; +} + +type IfHasKey = Key extends keyof SomeObject + ? Yes + : No; + +type MaybeShortcut< + ContentType extends MediaType, + Response extends OpenApiResponse, +> = IfHasKey< + Response["content"], + ContentType, + (body: Response["content"][ContentType]["schema"]) => GenericResponseBuilder<{ + content: Omit; + headers: Response["headers"]; + }>, + never +>; + +type MatchFunction = < + ContentType extends MediaType & keyof Response["content"], +>( + contentType: ContentType, + body: Response["content"][ContentType]["schema"], +) => GenericResponseBuilder<{ + content: Omit; + headers: Response["headers"]; +}>; + +type HeaderFunction = < + Header extends string & keyof Response["headers"], +>( + header: Header, + value: Response["headers"][Header]["schema"], +) => GenericResponseBuilder<{ + content: Response["content"]; + headers: Omit; +}>; + +interface ResponseBuilder { + [status: number | `${number} ${string}`]: ResponseBuilder; + content?: { body: unknown; type: string }[]; + header: (name: string, value: string) => ResponseBuilder; + headers: { [name: string]: string }; + html: (body: unknown) => ResponseBuilder; + json: (body: unknown) => ResponseBuilder; + match: (contentType: string, body: unknown) => ResponseBuilder; + random: () => ResponseBuilder; + randomLegacy: () => ResponseBuilder; + status?: number; + text: (body: unknown) => ResponseBuilder; +} + +type GenericResponseBuilder< + Response extends OpenApiResponse = OpenApiResponse, +> = [keyof Response["content"]] extends [never] + ? // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + void + : OmitValueWhenNever<{ + header: [keyof Response["headers"]] extends [never] + ? never + : HeaderFunction; + html: MaybeShortcut<"text/html", Response>; + json: MaybeShortcut<"application/json", Response>; + match: [keyof Response["content"]] extends [never] + ? never + : MatchFunction; + random: [keyof Response["content"]] extends [never] ? never : () => void; + text: MaybeShortcut<"text/plain", Response>; + }>; + +type ResponseBuilderFactory< + Responses extends OpenApiResponses = OpenApiResponses, +> = { + [StatusCode in keyof Responses]: GenericResponseBuilder< + Responses[StatusCode] + >; +} & { [key: string]: GenericResponseBuilder }; + +type HttpStatusCode = + | 100 + | 101 + | 102 + | 200 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 226 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 506 + | 507 + | 511; + +interface OpenApiParameters { + in: "body" | "cookie" | "formData" | "header" | "path" | "query"; + name: string; + schema?: { + type: string; + }; +} + +interface OpenApiOperation { + parameters?: OpenApiParameters[]; + produces?: string[]; + responses: { + [status: string]: { + content?: { + [type: number | string]: { + examples?: { [key: string]: Example }; + schema: unknown; + }; + }; + examples?: { [key: string]: unknown }; + schema?: unknown; + }; + }; +} + +export type { + HttpStatusCode, + MediaType, + OpenApiOperation, + OpenApiParameters, + OpenApiResponse, + ResponseBuilder, + ResponseBuilderFactory, +}; diff --git a/src/server/types.ts b/src/server/types.ts deleted file mode 100644 index f4c1fad0..00000000 --- a/src/server/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface OpenApiParameters { - in: "body" | "cookie" | "formData" | "header" | "path" | "query"; - name: string; - schema?: { - type: string; - }; -} diff --git a/src/typescript-generator/repository.js b/src/typescript-generator/repository.js index 4f5c5af8..9f93ab14 100644 --- a/src/typescript-generator/repository.js +++ b/src/typescript-generator/repository.js @@ -50,20 +50,12 @@ export class Repository { } copyCoreFiles(destination) { - const files = ["response-builder-factory.ts"]; - - return files.map((file) => { - const path = nodePath.join(destination, file).replaceAll("\\", "/"); - - process.stdout.write(`writing ${path}\n`); - - return fs.copyFile( - nodePath - .join(__dirname, `../../templates/${file}`) - .replaceAll("\\", "/"), - path, - ); - }); + return fs.copyFile( + nodePath + .join(__dirname, "../../src/server/types.d.ts") + .replaceAll("\\", "/"), + nodePath.join(destination, "types.d.ts").replaceAll("\\", "/"), + ); } async writeFiles(destination) { diff --git a/src/typescript-generator/response-type-coder.js b/src/typescript-generator/response-type-coder.js index 90556907..987769d6 100644 --- a/src/typescript-generator/response-type-coder.js +++ b/src/typescript-generator/response-type-coder.js @@ -101,17 +101,13 @@ export class ResponseTypeCoder extends Coder { script.importExternalType( "ResponseBuilderFactory", - nodePath - .join(basePath, "response-builder-factory.js") - .replaceAll("\\", "/"), + nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"), ); if (this.needsHttpStatusCodeImport) { script.importExternalType( "HttpStatusCode", - nodePath - .join(basePath, "response-builder-factory.js") - .replaceAll("\\", "/"), + nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"), ); } diff --git a/templates/response-builder-factory.ts b/templates/response-builder-factory.ts index 3a5f70a7..288a9823 100644 --- a/templates/response-builder-factory.ts +++ b/templates/response-builder-factory.ts @@ -1,4 +1,4 @@ -import type { OpenApiResponse } from "../src/server/response-builder.js"; +import type { OpenApiResponse } from "../src/server/types.d.ts"; type OmitValueWhenNever = Pick< Base, diff --git a/test/server/response-builder.test.ts b/test/server/response-builder.test.ts index 8323aadc..6bc9ad81 100644 --- a/test/server/response-builder.test.ts +++ b/test/server/response-builder.test.ts @@ -1,7 +1,5 @@ -import { - createResponseBuilder, - type OpenApiOperation, -} from "../../src/server/response-builder.js"; +import { createResponseBuilder } from "../../src/server/response-builder.js"; +import type { OpenApiOperation } from "../../src/server/types.d.ts"; describe("a response builder", () => { it("starts building a response object when the status is selected", () => { diff --git a/test/typescript-generator/__snapshots__/generate.test.ts.snap b/test/typescript-generator/__snapshots__/generate.test.ts.snap index a04ce388..0154e947 100644 --- a/test/typescript-generator/__snapshots__/generate.test.ts.snap +++ b/test/typescript-generator/__snapshots__/generate.test.ts.snap @@ -141,7 +141,7 @@ Map { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../response-builder-factory.js", + "modulePath": "../types.d.ts", }, }, "imports": Map { @@ -408,7 +408,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../response-builder-factory.js", + "modulePath": "../types.d.ts", }, }, "imports": Map { @@ -679,7 +679,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../response-builder-factory.js", + "modulePath": "../types.d.ts", }, }, "imports": Map { @@ -917,7 +917,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -1115,7 +1115,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -1330,7 +1330,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -1528,7 +1528,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -1813,7 +1813,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -2051,7 +2051,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -2289,7 +2289,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -2531,7 +2531,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -2733,7 +2733,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../../response-builder-factory.js", + "modulePath": "../../../types.d.ts", }, }, "imports": Map { @@ -2853,7 +2853,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../../response-builder-factory.js", + "modulePath": "../../../types.d.ts", }, }, "imports": Map { @@ -2989,7 +2989,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -3077,7 +3077,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -3189,7 +3189,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -3315,7 +3315,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -3509,7 +3509,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../../response-builder-factory.js", + "modulePath": "../../../types.d.ts", }, }, "imports": Map { @@ -3669,7 +3669,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../../response-builder-factory.js", + "modulePath": "../../../types.d.ts", }, }, "imports": Map { @@ -3833,7 +3833,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../../response-builder-factory.js", + "modulePath": "../../../types.d.ts", }, }, "imports": Map { @@ -3977,7 +3977,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../response-builder-factory.js", + "modulePath": "../types.d.ts", }, }, "imports": Map { @@ -4127,7 +4127,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../response-builder-factory.js", + "modulePath": "../types.d.ts", }, }, "imports": Map { @@ -4300,7 +4300,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -4433,7 +4433,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -4585,7 +4585,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -4689,7 +4689,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -4786,7 +4786,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -4868,7 +4868,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -5063,7 +5063,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -5242,7 +5242,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -5421,7 +5421,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { @@ -5604,7 +5604,7 @@ class Context { "externalImport": Map { "ResponseBuilderFactory" => { "isType": true, - "modulePath": "../../response-builder-factory.js", + "modulePath": "../../types.d.ts", }, }, "imports": Map { From 93a6f386bac48ae20f743257630c9af30f45323a Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Tue, 24 Oct 2023 19:38:06 -0400 Subject: [PATCH 03/11] fix issue where HttpStatusCode type was not imported --- .../response-type-coder.js | 10 +++-- .../__snapshots__/generate.test.ts.snap | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/typescript-generator/response-type-coder.js b/src/typescript-generator/response-type-coder.js index 987769d6..ef85b00f 100644 --- a/src/typescript-generator/response-type-coder.js +++ b/src/typescript-generator/response-type-coder.js @@ -12,8 +12,6 @@ export class ResponseTypeCoder extends Coder { } typeForDefaultStatusCode(listedStatusCodes) { - this.needsHttpStatusCodeImport = true; - const definedStatusCodes = listedStatusCodes.filter( (key) => key !== "default", ); @@ -104,13 +102,17 @@ export class ResponseTypeCoder extends Coder { nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"), ); - if (this.needsHttpStatusCodeImport) { + const text = `ResponseBuilderFactory<${this.buildResponseObjectType( + script, + )}>`; + + if (text.includes("HttpStatusCode")) { script.importExternalType( "HttpStatusCode", nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"), ); } - return `ResponseBuilderFactory<${this.buildResponseObjectType(script)}>`; + return text; } } diff --git a/test/typescript-generator/__snapshots__/generate.test.ts.snap b/test/typescript-generator/__snapshots__/generate.test.ts.snap index 0154e947..aa63ba33 100644 --- a/test/typescript-generator/__snapshots__/generate.test.ts.snap +++ b/test/typescript-generator/__snapshots__/generate.test.ts.snap @@ -3979,6 +3979,10 @@ class Context { "isType": true, "modulePath": "../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -4129,6 +4133,10 @@ class Context { "isType": true, "modulePath": "../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -4302,6 +4310,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -4435,6 +4447,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -4788,6 +4804,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -4870,6 +4890,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -5065,6 +5089,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -5244,6 +5272,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -5423,6 +5455,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { @@ -5606,6 +5642,10 @@ class Context { "isType": true, "modulePath": "../../types.d.ts", }, + "HttpStatusCode" => { + "isType": true, + "modulePath": "../../types.d.ts", + }, }, "imports": Map { "ContextType" => { From 8f5d0c462b7dc870ba3202022d894711a89635f4 Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Wed, 25 Oct 2023 17:15:21 -0400 Subject: [PATCH 04/11] debug Windows --- .github/workflows/debug-windows.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/debug-windows.yaml b/.github/workflows/debug-windows.yaml index 17e0f277..a93a4099 100644 --- a/.github/workflows/debug-windows.yaml +++ b/.github/workflows/debug-windows.yaml @@ -1,6 +1,7 @@ name: Debug Windows on: + pull_request: workflow_dispatch: env: @@ -33,7 +34,5 @@ jobs: run: yarn install --frozen-lockfile --network-timeout 100000 - name: Build run: yarn build - - name: Try running the petstore - run: npx . https://petstore3.swagger.io/api/v3/openapi.json out - timeout-minutes: 2 - continue-on-error: true + - name: Show files + run: Get-ChildItem -Recurse From 40711de58fe8223781880b0484e8998f0c91aaee Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Wed, 25 Oct 2023 17:26:01 -0400 Subject: [PATCH 05/11] on windows, globs must be double quoted --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 18b7600c..b51df8e0 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box --forceExit", "test:black-box": "rimraf dist && rimraf out && yarn build && yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest black-box --forceExit --coverage=false", "test:mutants": "stryker run stryker.config.json", - "build": "tsc && copyfiles -f 'src/client/**' dist/client", + "build": "tsc && copyfiles -f \"src/client/**\" dist/client", "prepack": "yarn build", "release": "npx changeset publish", "prepare": "husky install", From f676905b7a829f1445e1c6fb9c6de94a3fe92286 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:11:45 +0000 Subject: [PATCH 06/11] chore(deps): update dependency @types/koa to v2.13.11 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6b4631dc..91e53310 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@testing-library/dom": "9.3.3", "@types/jest": "29.5.8", "@types/js-yaml": "4.0.8", - "@types/koa": "2.13.10", + "@types/koa": "2.13.11", "@types/koa-bodyparser": "4.3.11", "@types/koa-proxy": "1.0.7", "@types/koa-static": "4.0.4", diff --git a/yarn.lock b/yarn.lock index ddb313f1..ecc08538 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2581,10 +2581,10 @@ "@types/koa-compose" "*" "@types/node" "*" -"@types/koa@2.13.10": - version "2.13.10" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.10.tgz#2c2a1cdf1252d654b05f444194328a3d23a880c4" - integrity sha512-weKc5IBeORLDGwD1FMgPjaZIg0/mtP7KxXAXEzPRCN78k274D9U2acmccDNPL1MwyV40Jj+hQQ5N2eaV6O0z8g== +"@types/koa@2.13.11": + version "2.13.11" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.11.tgz#b7fd4a077d3683a9212a105f60859e631ed3a13b" + integrity sha512-0HZSGNdmLlLRvSxv0ngLSp09Hw98c+2XL3ZRYmkE6y8grqTweKEyyaj7LgxkyPUv0gQ5pNS/a7kHXo2Iwha1rA== dependencies: "@types/accepts" "*" "@types/content-disposition" "*" From 0dc11ea67a1ecc0c5d9fb6c81af40f421a972a8c Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Fri, 17 Nov 2023 12:48:38 -0500 Subject: [PATCH 07/11] add a --prefix option for route prefix fixes #636 --- .changeset/mighty-penguins-hide.md | 5 ++++ .eslintrc.cjs | 3 +++ bin/counterfact.js | 5 ++++ docs/usage.md | 15 ++++++------ src/server/koa-middleware.ts | 11 +++++++-- src/server/registry.ts | 1 + test/server/koa-middleware.test.ts | 37 ++++++++++++++++++++++++++++++ 7 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 .changeset/mighty-penguins-hide.md diff --git a/.changeset/mighty-penguins-hide.md b/.changeset/mighty-penguins-hide.md new file mode 100644 index 00000000..16d933b8 --- /dev/null +++ b/.changeset/mighty-penguins-hide.md @@ -0,0 +1,5 @@ +--- +"counterfact": minor +--- + +added a --prefix option to specify prefix from which routes are served (e.g. /api/v1) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f210f29d..e5b43945 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,6 +19,8 @@ const rules = { }, ], + "max-lines": "warn", + "n/shebang": "off", "no-magic-numbers": [ @@ -30,6 +32,7 @@ const rules = { "node/file-extension-in-import": "off", "node/no-callback-literal": "off", + "node/no-missing-import": "off", "prettier/prettier": [ diff --git a/bin/counterfact.js b/bin/counterfact.js index ae55ac65..3ca5cd16 100755 --- a/bin/counterfact.js +++ b/bin/counterfact.js @@ -48,6 +48,7 @@ async function main(source, destination) { port: options.port, proxyEnabled: Boolean(options.proxyUrl), proxyUrl: options.proxyUrl, + routePrefix: options.prefix, }; debug("loading counterfact (%o)", config); @@ -106,6 +107,10 @@ program .option("--swagger", "include swagger-ui") .option("--open", "open a browser") .option("--proxy-url ", "proxy URL") + .option( + "--prefix ", + "base path from which routes will be served (e.g. /api/v1)", + ) .action(main) // eslint-disable-next-line sonar/process-argv .parse(process.argv); diff --git a/docs/usage.md b/docs/usage.md index 778c7492..70dcbe5a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -32,15 +32,16 @@ Usage: counterfact [options] [destination] Counterfact is a tool for generating a REST API from an OpenAPI document. Arguments: -openapi.yaml path or URL to OpenAPI document -destination path to generated code (default: ".") + openapi.yaml path or URL to OpenAPI document + destination path to generated code (default: ".") Options: ---serve start the server after generating code ---port server port number (default: 3100) ---swagger include swagger-ui (implies --serve) ---open open a browser to swagger-ui (implies --swagger and --serve) --h, --help display help for command + --port server port number (default: 3100) + --swagger include swagger-ui + --open open a browser + --proxy-url proxy URL + --prefix base path from which routes will be served (e.g. /api/v1) + -h, --help display help for command ``` diff --git a/src/server/koa-middleware.ts b/src/server/koa-middleware.ts index 8e5850b9..cb419a75 100644 --- a/src/server/koa-middleware.ts +++ b/src/server/koa-middleware.ts @@ -26,13 +26,20 @@ function addCors(ctx: Koa.ExtendableContext, headers?: IncomingHttpHeaders) { export function koaMiddleware( dispatcher: Dispatcher, - { proxyEnabled = false, proxyUrl = "" } = {}, + { proxyEnabled = false, proxyUrl = "", routePrefix = "" } = {}, proxy = koaProxy, ): Koa.Middleware { // eslint-disable-next-line max-statements return async function middleware(ctx, next) { + if (!ctx.request.path.startsWith(routePrefix)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await next(); + } + /* @ts-expect-error the body comes from koa-bodyparser, not sure how to fix this */ - const { body, headers, path, query } = ctx.request; + const { body, headers, query } = ctx.request; + + const path = ctx.request.path.slice(routePrefix.length); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const method = ctx.request.method as HttpMethods; diff --git a/src/server/registry.ts b/src/server/registry.ts index 8de9bae8..4b8791f1 100644 --- a/src/server/registry.ts +++ b/src/server/registry.ts @@ -204,6 +204,7 @@ export class Registry { } } else { node = node.children[matchingChild]; + matchedParts.push(matchingChild); } } diff --git a/test/server/koa-middleware.test.ts b/test/server/koa-middleware.test.ts index 07b887c6..4d78bd36 100644 --- a/test/server/koa-middleware.test.ts +++ b/test/server/koa-middleware.test.ts @@ -263,4 +263,41 @@ describe("koa middleware", () => { expect(ctx.set).toHaveBeenCalledWith("X-Custom-Header", "custom value"); }); + + it("passes the request to the dispatcher and returns the response", async () => { + const registry = new Registry(); + + registry.add("/hello", { + // @ts-expect-error - not obvious how to make TS happy here, and it's just a unit test + POST({ body }: { body: { name: string } }) { + return { + body: `Hello, ${body.name}!`, + }; + }, + }); + + const dispatcher = new Dispatcher(registry, new ContextRegistry()); + const middleware = koaMiddleware(dispatcher, { routePrefix: "/api/v1" }); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const ctx = { + req: { + path: "/api/v1/hello", + }, + + request: { + body: { name: "Homer" }, + method: "POST", + path: "/api/v1/hello", + }, + + set: jest.fn(), + } as unknown as ParameterizedContext; + + await middleware(ctx, async () => { + await Promise.resolve(undefined); + }); + + expect(ctx.status).toBe(200); + expect(ctx.body).toBe("Hello, Homer!"); + }); }); From 32bf0719677cbaa729295149a40b46c349b9060d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:17:47 +0000 Subject: [PATCH 08/11] chore(deps): update dependency @types/koa-bodyparser to v4.3.12 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 91e53310..7a770dd0 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@types/jest": "29.5.8", "@types/js-yaml": "4.0.8", "@types/koa": "2.13.11", - "@types/koa-bodyparser": "4.3.11", + "@types/koa-bodyparser": "4.3.12", "@types/koa-proxy": "1.0.7", "@types/koa-static": "4.0.4", "copyfiles": "2.4.1", diff --git a/yarn.lock b/yarn.lock index ecc08538..f75eea00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2531,10 +2531,10 @@ resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== -"@types/koa-bodyparser@4.3.11": - version "4.3.11" - resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.11.tgz#26a72b93204ba4fa1fb8444f070cb69ad84d0813" - integrity sha512-ClE+n+3w7BtepmyvAuXBbYDz13lmihha27FF2iIpqymsY6j2VS2f/I9wieyN/sZBMcM/POU7LPlMi2m2iNV+Vg== +"@types/koa-bodyparser@4.3.12": + version "4.3.12" + resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.12.tgz#c19355e504422fd2a8fdb3496a32da48cd29133c" + integrity sha512-hKMmRMVP889gPIdLZmmtou/BijaU1tHPyMNmcK7FAHAdATnRcGQQy78EqTTxLH1D4FTsrxIzklAQCso9oGoebQ== dependencies: "@types/koa" "*" From e82725a6f34fe49629034f4c38061f5c85022a00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:25:38 +0000 Subject: [PATCH 09/11] chore(deps): update dependency @types/js-yaml to v4.0.9 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7a770dd0..bd3a32ae 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@swc/jest": "0.2.29", "@testing-library/dom": "9.3.3", "@types/jest": "29.5.8", - "@types/js-yaml": "4.0.8", + "@types/js-yaml": "4.0.9", "@types/koa": "2.13.11", "@types/koa-bodyparser": "4.3.12", "@types/koa-proxy": "1.0.7", diff --git a/yarn.lock b/yarn.lock index f75eea00..d58ac090 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2506,10 +2506,10 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/js-yaml@4.0.8": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.8.tgz#7574e422d70d4a1b41f517d1d9abc61be2299a97" - integrity sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA== +"@types/js-yaml@4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== "@types/json-schema@7.0.15": version "7.0.15" From b763aa21dd7dac9d2e4d003eb30406a1f07b67a5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 17 Nov 2023 19:45:40 +0000 Subject: [PATCH 10/11] Version Packages --- .changeset/dependencies-GH-638.md | 5 ----- .changeset/dependencies-GH-648.md | 5 ----- .changeset/mighty-penguins-hide.md | 5 ----- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) delete mode 100644 .changeset/dependencies-GH-638.md delete mode 100644 .changeset/dependencies-GH-648.md delete mode 100644 .changeset/mighty-penguins-hide.md diff --git a/.changeset/dependencies-GH-638.md b/.changeset/dependencies-GH-638.md deleted file mode 100644 index 162a7a47..00000000 --- a/.changeset/dependencies-GH-638.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"counterfact": patch ---- - -Update dependency @types/jest to v29.5.7 diff --git a/.changeset/dependencies-GH-648.md b/.changeset/dependencies-GH-648.md deleted file mode 100644 index bf17509a..00000000 --- a/.changeset/dependencies-GH-648.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"counterfact": patch ---- - -chore(deps): update dependency @types/jest to v29.5.8 diff --git a/.changeset/mighty-penguins-hide.md b/.changeset/mighty-penguins-hide.md deleted file mode 100644 index 16d933b8..00000000 --- a/.changeset/mighty-penguins-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"counterfact": minor ---- - -added a --prefix option to specify prefix from which routes are served (e.g. /api/v1) diff --git a/CHANGELOG.md b/CHANGELOG.md index 579944f2..9031ade4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # counterfact +## 0.30.0 + +### Minor Changes + +- 0dc11ea: added a --prefix option to specify prefix from which routes are served (e.g. /api/v1) + +### Patch Changes + +- 4de94f0: Update dependency @types/jest to v29.5.7 +- 5f8e97b: chore(deps): update dependency @types/jest to v29.5.8 + ## 0.29.0 ### Minor Changes diff --git a/package.json b/package.json index bd3a32ae..2e0d7d3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "counterfact", - "version": "0.29.0", + "version": "0.30.0", "description": "a library for building a fake REST API for testing", "type": "module", "main": "./src/server/counterfact.js", From e01a41fcdfd72f0b2b812c0dd31aca5519d68e62 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:56:19 +0000 Subject: [PATCH 11/11] fix(deps): update dependency prettier to v3.1.0 --- package.json | 2 +- yarn.lock | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2e0d7d3c..1f939334 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "koa2-swagger-ui": "5.9.1", "node-fetch": "3.3.2", "open": "9.1.0", - "prettier": "3.0.3", + "prettier": "3.1.0", "typescript": "5.2.2" } } diff --git a/yarn.lock b/yarn.lock index d58ac090..96a54a17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8974,16 +8974,21 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@3.0.3, prettier@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" - integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +prettier@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e" + integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw== prettier@^2.7.1: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== + pretty-format@^27.0.2: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"