counterfact 0.37.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -110,9 +110,15 @@ export class Dispatcher {
110
110
  }
111
111
  return false;
112
112
  }
113
- // eslint-disable-next-line sonarjs/cognitive-complexity
113
+ // eslint-disable-next-line sonarjs/cognitive-complexity, max-statements
114
114
  async request({ body, headers = {}, method, path, query, req, }) {
115
115
  debug(`request: ${method} ${path}`);
116
+ // If the incoming path includes the base path, remove it
117
+ if (this.openApiDocument?.basePath !== undefined &&
118
+ path.toLowerCase().startsWith(this.openApiDocument.basePath.toLowerCase())) {
119
+ // eslint-disable-next-line security/detect-non-literal-regexp
120
+ path = path.replace(new RegExp(this.openApiDocument.basePath, "iu"), "");
121
+ }
116
122
  const { matchedPath } = this.registry.handler(path);
117
123
  const operation = this.operationForPathAndMethod(matchedPath, method);
118
124
  const response = await this.registry.endpoint(method, path, this.parameterTypes(operation?.parameters))({
@@ -46,12 +46,17 @@ export class Registry {
46
46
  status: 404,
47
47
  });
48
48
  }
49
- return async ({ ...requestData }) => await execute({
50
- ...requestData,
51
- headers: castParameters(requestData.headers, parameterTypes.header),
52
- matchedPath: handler.matchedPath,
53
- path: castParameters(handler.path, parameterTypes.path),
54
- query: castParameters(requestData.query, parameterTypes.query),
55
- });
49
+ return async ({ ...requestData }) => {
50
+ const operationArgument = {
51
+ ...requestData,
52
+ headers: castParameters(requestData.headers, parameterTypes.header),
53
+ matchedPath: handler.matchedPath,
54
+ path: castParameters(handler.path, parameterTypes.path),
55
+ query: castParameters(requestData.query, parameterTypes.query),
56
+ };
57
+ // eslint-disable-next-line id-length
58
+ operationArgument.x = operationArgument;
59
+ return await execute(operationArgument);
60
+ };
56
61
  }
57
62
  }
@@ -41,19 +41,21 @@ type MaybeShortcut<
41
41
  Response["content"],
42
42
  ContentType,
43
43
  (body: Response["content"][ContentType]["schema"]) => GenericResponseBuilder<{
44
- content: Omit<Response["content"], ContentType>;
44
+ content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
45
45
  headers: Response["headers"];
46
46
  }>,
47
47
  never
48
48
  >;
49
49
 
50
+ type NeverIfEmpty<Record> = {} extends Record ? never : Record;
51
+
50
52
  type MatchFunction<Response extends OpenApiResponse> = <
51
53
  ContentType extends MediaType & keyof Response["content"],
52
54
  >(
53
55
  contentType: ContentType,
54
- body: Response["content"][ContentType]["schema"],
56
+ body: Response["content"][ContentType]["schema"]
55
57
  ) => GenericResponseBuilder<{
56
- content: Omit<Response["content"], ContentType>;
58
+ content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
57
59
  headers: Response["headers"];
58
60
  }>;
59
61
 
@@ -61,19 +63,15 @@ type HeaderFunction<Response extends OpenApiResponse> = <
61
63
  Header extends string & keyof Response["headers"],
62
64
  >(
63
65
  header: Header,
64
- value: Response["headers"][Header]["schema"],
66
+ value: Response["headers"][Header]["schema"]
65
67
  ) => GenericResponseBuilder<{
66
- content: Response["content"];
67
- headers: Omit<Response["headers"], Header>;
68
+ content: NeverIfEmpty<Response["content"]>;
69
+ headers: NeverIfEmpty<Omit<Response["headers"], Header>>;
68
70
  }>;
69
71
 
70
72
  type RandomFunction<Response extends OpenApiResponse> = <
71
73
  Header extends string & keyof Response["headers"],
72
- >() => GenericResponseBuilder<{
73
- content: {};
74
- headers: Response["headers"];
75
- }>;
76
-
74
+ >() => "COUNTERFACT_RESPONSE";
77
75
 
78
76
  interface ResponseBuilder {
79
77
  [status: number | `${number} ${string}`]: ResponseBuilder;
@@ -90,23 +88,27 @@ interface ResponseBuilder {
90
88
  xml: (body: unknown) => ResponseBuilder;
91
89
  }
92
90
 
91
+ type GenericResponseBuilderInner<
92
+ Response extends OpenApiResponse = OpenApiResponse,
93
+ > = OmitValueWhenNever<{
94
+ header: [keyof Response["headers"]] extends [never]
95
+ ? never
96
+ : HeaderFunction<Response>;
97
+ html: MaybeShortcut<"text/html", Response>;
98
+ json: MaybeShortcut<"application/json", Response>;
99
+ match: [keyof Response["content"]] extends [never]
100
+ ? never
101
+ : MatchFunction<Response>;
102
+ random: [keyof Response["content"]] extends [never]
103
+ ? never
104
+ : RandomFunction<Response>;
105
+ text: MaybeShortcut<"text/plain", Response>;
106
+ xml: MaybeShortcut<"application/xml" | "text/xml", Response>;
107
+ }>;
108
+
93
109
  type GenericResponseBuilder<
94
110
  Response extends OpenApiResponse = OpenApiResponse,
95
- > = [keyof Response["content"]] extends [never]
96
- ? { }
97
- : OmitValueWhenNever<{
98
- header: [keyof Response["headers"]] extends [never]
99
- ? never
100
- : HeaderFunction<Response>;
101
- html: MaybeShortcut<"text/html", Response>;
102
- json: MaybeShortcut<"application/json", Response>;
103
- match: [keyof Response["content"]] extends [never]
104
- ? never
105
- : MatchFunction<Response>;
106
- random: [keyof Response["content"]] extends [never] ? never : RandomFunction<Response>;
107
- text: MaybeShortcut<"text/plain", Response>;
108
- xml: MaybeShortcut<"application/xml" | "text/xml", Response>;
109
- }>;
111
+ > = {} extends OmitValueWhenNever<Response> ? "COUNTERFACT_RESPONSE" : GenericResponseBuilderInner<Response>;
110
112
 
111
113
  type ResponseBuilderFactory<
112
114
  Responses extends OpenApiResponses = OpenApiResponses,
@@ -199,6 +201,26 @@ interface OpenApiOperation {
199
201
  };
200
202
  }
201
203
 
204
+ type WideResponseBuilder = {
205
+ header: (body: unknown) => WideResponseBuilder;
206
+ html: (body: unknown) => WideResponseBuilder;
207
+ json: (body: unknown) => WideResponseBuilder;
208
+ match: (contentType: string, body: unknown) => WideResponseBuilder;
209
+ random: () => WideResponseBuilder;
210
+ text: (body: unknown) => WideResponseBuilder;
211
+ xml: (body: unknown) => WideResponseBuilder;
212
+ }
213
+
214
+ type WideOperationArgument = {
215
+ path: Record<string, string>;
216
+ query: Record<string, string>;
217
+ header: Record<string, string>;
218
+ body: unknown;
219
+ response: Record<number, WideResponseBuilder>;
220
+ proxy: (url: string) => { proxyUrl: string };
221
+ context: unknown
222
+ };
223
+
202
224
  export type {
203
225
  HttpStatusCode,
204
226
  MediaType,
@@ -207,4 +229,6 @@ export type {
207
229
  OpenApiResponse,
208
230
  ResponseBuilder,
209
231
  ResponseBuilderFactory,
232
+ WideOperationArgument,
233
+ OmitValueWhenNever
210
234
  };
@@ -2,6 +2,7 @@ import nodePath from "node:path";
2
2
  import { Coder } from "./coder.js";
3
3
  import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
4
4
  import { ParametersTypeCoder } from "./parameters-type-coder.js";
5
+ import { READ_ONLY_COMMENTS } from "./read-only-comments.js";
5
6
  import { ResponseTypeCoder } from "./response-type-coder.js";
6
7
  import { SchemaTypeCoder } from "./schema-type-coder.js";
7
8
  export class OperationTypeCoder extends Coder {
@@ -48,29 +49,28 @@ export class OperationTypeCoder extends Coder {
48
49
  .join("path-types", pathString)
49
50
  .replaceAll("\\", "/")}.types.ts`;
50
51
  }
52
+ // eslint-disable-next-line max-statements
51
53
  write(script) {
54
+ // eslint-disable-next-line no-param-reassign
55
+ script.comments = READ_ONLY_COMMENTS;
56
+ const xType = script.importSharedType("WideOperationArgument");
57
+ script.importSharedType("OmitValueWhenNever");
52
58
  const contextTypeImportName = script.importExternalType("Context", CONTEXT_FILE_TOKEN);
53
59
  const parameters = this.requirement.get("parameters");
54
- const queryType = parameters === undefined
55
- ? "never"
56
- : new ParametersTypeCoder(parameters, "query").write(script);
57
- const pathType = parameters === undefined
58
- ? "never"
59
- : new ParametersTypeCoder(parameters, "path").write(script);
60
- const headerType = parameters === undefined
61
- ? "never"
62
- : new ParametersTypeCoder(parameters, "header").write(script);
60
+ const queryType = new ParametersTypeCoder(parameters, "query").write(script);
61
+ const pathType = new ParametersTypeCoder(parameters, "path").write(script);
62
+ const headerType = new ParametersTypeCoder(parameters, "header").write(script);
63
63
  const bodyRequirement = this.requirement.get("consumes")
64
64
  ? parameters
65
65
  .find((parameter) => ["body", "formData"].includes(parameter.get("in").data))
66
66
  .get("schema")
67
67
  : this.requirement.select("requestBody/content/application~1json/schema");
68
- const bodyType = bodyRequirement
69
- ? new SchemaTypeCoder(bodyRequirement).write(script)
70
- : "undefined";
68
+ const bodyType = bodyRequirement === undefined
69
+ ? "never"
70
+ : new SchemaTypeCoder(bodyRequirement).write(script);
71
71
  const responseType = new ResponseTypeCoder(this.requirement.get("responses"), this.requirement.get("produces")?.data ??
72
72
  this.requirement.specification?.rootRequirement?.get("produces")?.data).write(script);
73
- const proxyType = "(url: string) => { proxyUrl: string }";
74
- return `({ query, path, header, body, context, proxy }: { query: ${queryType}, path: ${pathType}, header: ${headerType}, body: ${bodyType}, context: ${contextTypeImportName}, response: ${responseType}, proxy: ${proxyType} }) => ${this.responseTypes(script)} | { status: 415, contentType: "text/plain", body: string } | { }`;
73
+ const proxyType = '(url: string) => "COUNTERFACT_RESPONSE"';
74
+ return `($: OmitValueWhenNever<{ query: ${queryType}, path: ${pathType}, header: ${headerType}, body: ${bodyType}, context: ${contextTypeImportName}, response: ${responseType}, x: ${xType}, proxy: ${proxyType} }>) => ${this.responseTypes(script)} | { status: 415, contentType: "text/plain", body: string } | "COUNTERFACT_RESPONSE"`;
75
75
  }
76
76
  }
@@ -10,7 +10,7 @@ export class ParametersTypeCoder extends Coder {
10
10
  return super.names("parameters");
11
11
  }
12
12
  write(script) {
13
- const typeDefinitions = this.requirement.data
13
+ const typeDefinitions = (this.requirement?.data ?? [])
14
14
  .filter((parameter) => parameter.in === this.placement)
15
15
  .map((parameter, index) => {
16
16
  const requirement = this.requirement.get(String(index));
@@ -0,0 +1,5 @@
1
+ export const READ_ONLY_COMMENTS = [
2
+ "This code was automatically generated from an OpenAPI description.",
3
+ "Do not edit this file. Edit the OpenAPI file instead.",
4
+ "For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md",
5
+ ];
@@ -33,19 +33,13 @@ export class Repository {
33
33
  }
34
34
  }
35
35
  async copyCoreFiles(destination) {
36
- const sourcePath = nodePath
37
- .join(__dirname, "../../dist/server/types.d.ts")
38
- .replaceAll("\\", "/");
36
+ const sourcePath = nodePath.join(__dirname, "../../dist/server/types.d.ts");
37
+ const destinationPath = nodePath.join(destination, "types.d.ts");
39
38
  if (!existsSync(sourcePath)) {
40
39
  return false;
41
40
  }
42
- const destinationPath = nodePath
43
- .join(destination, "types.d.ts")
44
- .replaceAll("\\", "/");
45
41
  await ensureDirectoryExists(destination);
46
- return fs.copyFile(nodePath
47
- .join(__dirname, "../../dist/server/types.d.ts")
48
- .replaceAll("\\", "/"), destinationPath);
42
+ return fs.copyFile(sourcePath, destinationPath);
49
43
  }
50
44
  async writeFiles(destination) {
51
45
  debug("waiting for %i or more scripts to finish before writing files", this.scripts.size);
@@ -93,7 +87,9 @@ export class Context {
93
87
  `);
94
88
  }
95
89
  findContextPath(destination, path) {
96
- return nodePath.relative(nodePath.join(destination, nodePath.dirname(path)), this.nearestContextFile(destination, path));
90
+ return nodePath
91
+ .relative(nodePath.join(destination, nodePath.dirname(path)), this.nearestContextFile(destination, path))
92
+ .replaceAll("\\", "/");
97
93
  }
98
94
  nearestContextFile(destination, path) {
99
95
  const directory = nodePath.dirname(path).replace("path-types", "paths");
@@ -1,4 +1,3 @@
1
- import nodePath from "node:path";
2
1
  import { Coder } from "./coder.js";
3
2
  import { printObject, printObjectWithoutQuotes } from "./printers.js";
4
3
  import { SchemaTypeCoder } from "./schema-type-coder.js";
@@ -66,15 +65,10 @@ export class ResponseTypeCoder extends Coder {
66
65
  ]));
67
66
  }
68
67
  write(script) {
69
- const basePath = script.path
70
- .split("/")
71
- .slice(0, -1)
72
- .map(() => "..")
73
- .join("/");
74
- script.importExternalType("ResponseBuilderFactory", nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"));
68
+ script.importSharedType("ResponseBuilderFactory");
75
69
  const text = `ResponseBuilderFactory<${this.buildResponseObjectType(script)}>`;
76
70
  if (text.includes("HttpStatusCode")) {
77
- script.importExternalType("HttpStatusCode", nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"));
71
+ script.importSharedType("HttpStatusCode");
78
72
  }
79
73
  return text;
80
74
  }
@@ -73,6 +73,7 @@ export class SchemaTypeCoder extends Coder {
73
73
  return `components/${this.requirement.data.$ref.split("/").at(-1)}.ts`;
74
74
  }
75
75
  write(script) {
76
+ // script.comments = READ_ONLY_COMMENTS;
76
77
  if (this.requirement.isReference) {
77
78
  return script.importType(this);
78
79
  }
@@ -5,6 +5,7 @@ const debug = createDebugger("counterfact:typescript-generator:script");
5
5
  export class Script {
6
6
  constructor(repository, path) {
7
7
  this.repository = repository;
8
+ this.comments = [];
8
9
  this.exports = new Map();
9
10
  this.imports = new Map();
10
11
  this.externalImport = new Map();
@@ -12,6 +13,13 @@ export class Script {
12
13
  this.typeCache = new Map();
13
14
  this.path = path;
14
15
  }
16
+ get relativePathToBase() {
17
+ return this.path
18
+ .split("/")
19
+ .slice(0, -1)
20
+ .map(() => "..")
21
+ .join("/");
22
+ }
15
23
  firstUniqueName(coder) {
16
24
  for (const name of coder.names()) {
17
25
  if (!this.imports.has(name) && !this.exports.has(name)) {
@@ -96,6 +104,11 @@ export class Script {
96
104
  importExternalType(name, modulePath) {
97
105
  return this.importExternal(name, modulePath, true);
98
106
  }
107
+ importSharedType(name) {
108
+ return this.importExternal(name, nodePath
109
+ .join(this.relativePathToBase, "types.d.ts")
110
+ .replaceAll("\\", "/"), true);
111
+ }
99
112
  exportType(coder) {
100
113
  return this.export(coder, true);
101
114
  }
@@ -131,6 +144,8 @@ export class Script {
131
144
  }
132
145
  contents() {
133
146
  return prettier.format([
147
+ this.comments.map((comment) => `// ${comment}`).join("\n"),
148
+ this.comments.length > 0 ? "\n\n" : "",
134
149
  this.externalImportStatements().join("\n"),
135
150
  this.importStatements().join("\n"),
136
151
  "\n\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "counterfact",
3
- "version": "0.37.1",
3
+ "version": "0.38.0",
4
4
  "description": "a library for building a fake REST API for testing",
5
5
  "type": "module",
6
6
  "main": "./src/server/counterfact.js",
@@ -47,7 +47,7 @@
47
47
  "@stryker-mutator/core": "8.2.6",
48
48
  "@stryker-mutator/jest-runner": "8.2.6",
49
49
  "@stryker-mutator/typescript-checker": "8.2.6",
50
- "@swc/core": "1.4.4",
50
+ "@swc/core": "1.4.8",
51
51
  "@swc/jest": "0.2.36",
52
52
  "@testing-library/dom": "9.3.4",
53
53
  "@types/jest": "29.5.12",
@@ -92,13 +92,13 @@
92
92
  "json-schema-faker": "0.5.6",
93
93
  "json-schema-ref-parser": "9.0.9",
94
94
  "jsonwebtoken": "9.0.2",
95
- "koa": "2.15.0",
95
+ "koa": "2.15.1",
96
96
  "koa-bodyparser": "4.4.1",
97
97
  "koa-proxy": "1.0.0-alpha.3",
98
98
  "koa2-swagger-ui": "5.10.0",
99
99
  "node-fetch": "3.3.2",
100
- "open": "10.0.4",
100
+ "open": "10.1.0",
101
101
  "prettier": "3.2.5",
102
- "typescript": "5.3.3"
102
+ "typescript": "5.4.2"
103
103
  }
104
104
  }