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.
- package/dist/server/dispatcher.js +7 -1
- package/dist/server/registry.js +12 -7
- package/dist/server/types.d.ts +50 -26
- package/dist/typescript-generator/operation-type-coder.js +14 -14
- package/dist/typescript-generator/parameters-type-coder.js +1 -1
- package/dist/typescript-generator/read-only-comments.js +5 -0
- package/dist/typescript-generator/repository.js +6 -10
- package/dist/typescript-generator/response-type-coder.js +2 -8
- package/dist/typescript-generator/schema-type-coder.js +1 -0
- package/dist/typescript-generator/script.js +15 -0
- package/package.json +5 -5
|
@@ -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))({
|
package/dist/server/registry.js
CHANGED
|
@@ -46,12 +46,17 @@ export class Registry {
|
|
|
46
46
|
status: 404,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
return async ({ ...requestData }) =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -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
|
-
>() =>
|
|
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
|
-
> =
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
?
|
|
70
|
-
:
|
|
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 =
|
|
74
|
-
return `(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
100
|
+
"open": "10.1.0",
|
|
101
101
|
"prettier": "3.2.5",
|
|
102
|
-
"typescript": "5.
|
|
102
|
+
"typescript": "5.4.2"
|
|
103
103
|
}
|
|
104
104
|
}
|