counterfact 0.37.2 → 0.38.1
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/registry.js +12 -7
- package/dist/server/types.d.ts +56 -26
- package/dist/typescript-generator/operation-type-coder.js +10 -14
- package/dist/typescript-generator/parameters-type-coder.js +1 -1
- package/dist/typescript-generator/repository.js +3 -9
- package/dist/typescript-generator/response-type-coder.js +12 -10
- package/dist/typescript-generator/script.js +12 -0
- package/package.json +3 -3
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
|
@@ -24,6 +24,7 @@ type OmitValueWhenNever<Base> = Pick<
|
|
|
24
24
|
interface OpenApiResponse {
|
|
25
25
|
content: { [key: MediaType]: OpenApiContent };
|
|
26
26
|
headers: { [key: string]: OpenApiHeader };
|
|
27
|
+
requiredHeaders: string
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
interface OpenApiResponses {
|
|
@@ -41,39 +42,40 @@ type MaybeShortcut<
|
|
|
41
42
|
Response["content"],
|
|
42
43
|
ContentType,
|
|
43
44
|
(body: Response["content"][ContentType]["schema"]) => GenericResponseBuilder<{
|
|
44
|
-
content: Omit<Response["content"], ContentType
|
|
45
|
+
content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
|
|
45
46
|
headers: Response["headers"];
|
|
47
|
+
requiredHeaders: Response["requiredHeaders"];
|
|
46
48
|
}>,
|
|
47
49
|
never
|
|
48
50
|
>;
|
|
49
51
|
|
|
52
|
+
type NeverIfEmpty<Record> = {} extends Record ? never : Record;
|
|
53
|
+
|
|
50
54
|
type MatchFunction<Response extends OpenApiResponse> = <
|
|
51
55
|
ContentType extends MediaType & keyof Response["content"],
|
|
52
56
|
>(
|
|
53
57
|
contentType: ContentType,
|
|
54
|
-
body: Response["content"][ContentType]["schema"]
|
|
58
|
+
body: Response["content"][ContentType]["schema"]
|
|
55
59
|
) => GenericResponseBuilder<{
|
|
56
|
-
content: Omit<Response["content"], ContentType
|
|
60
|
+
content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
|
|
57
61
|
headers: Response["headers"];
|
|
62
|
+
requiredHeaders: Response["requiredHeaders"];
|
|
58
63
|
}>;
|
|
59
64
|
|
|
60
65
|
type HeaderFunction<Response extends OpenApiResponse> = <
|
|
61
66
|
Header extends string & keyof Response["headers"],
|
|
62
67
|
>(
|
|
63
68
|
header: Header,
|
|
64
|
-
value: Response["headers"][Header]["schema"]
|
|
69
|
+
value: Response["headers"][Header]["schema"]
|
|
65
70
|
) => GenericResponseBuilder<{
|
|
66
|
-
content: Response["content"]
|
|
67
|
-
headers: Omit<Response["headers"], Header
|
|
71
|
+
content: NeverIfEmpty<Response["content"]>;
|
|
72
|
+
headers: NeverIfEmpty<Omit<Response["headers"], Header>>;
|
|
73
|
+
requiredHeaders: Exclude<Response["requiredHeaders"], Header>;
|
|
68
74
|
}>;
|
|
69
75
|
|
|
70
76
|
type RandomFunction<Response extends OpenApiResponse> = <
|
|
71
77
|
Header extends string & keyof Response["headers"],
|
|
72
|
-
>() =>
|
|
73
|
-
content: {};
|
|
74
|
-
headers: Response["headers"];
|
|
75
|
-
}>;
|
|
76
|
-
|
|
78
|
+
>() => "COUNTERFACT_RESPONSE";
|
|
77
79
|
|
|
78
80
|
interface ResponseBuilder {
|
|
79
81
|
[status: number | `${number} ${string}`]: ResponseBuilder;
|
|
@@ -90,23 +92,27 @@ interface ResponseBuilder {
|
|
|
90
92
|
xml: (body: unknown) => ResponseBuilder;
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
type GenericResponseBuilderInner<
|
|
96
|
+
Response extends OpenApiResponse = OpenApiResponse,
|
|
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]
|
|
107
|
+
? never
|
|
108
|
+
: RandomFunction<Response>;
|
|
109
|
+
text: MaybeShortcut<"text/plain", Response>;
|
|
110
|
+
xml: MaybeShortcut<"application/xml" | "text/xml", Response>;
|
|
111
|
+
}>;
|
|
112
|
+
|
|
93
113
|
type GenericResponseBuilder<
|
|
94
114
|
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
|
-
}>;
|
|
115
|
+
> = {} extends OmitValueWhenNever<Response> ? "COUNTERFACT_RESPONSE" : keyof OmitValueWhenNever<Response> extends "headers" ? { header: HeaderFunction<Response>, ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE" } : GenericResponseBuilderInner<Response>;
|
|
110
116
|
|
|
111
117
|
type ResponseBuilderFactory<
|
|
112
118
|
Responses extends OpenApiResponses = OpenApiResponses,
|
|
@@ -199,6 +205,26 @@ interface OpenApiOperation {
|
|
|
199
205
|
};
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
type WideResponseBuilder = {
|
|
209
|
+
header: (body: unknown) => WideResponseBuilder;
|
|
210
|
+
html: (body: unknown) => WideResponseBuilder;
|
|
211
|
+
json: (body: unknown) => WideResponseBuilder;
|
|
212
|
+
match: (contentType: string, body: unknown) => WideResponseBuilder;
|
|
213
|
+
random: () => WideResponseBuilder;
|
|
214
|
+
text: (body: unknown) => WideResponseBuilder;
|
|
215
|
+
xml: (body: unknown) => WideResponseBuilder;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type WideOperationArgument = {
|
|
219
|
+
path: Record<string, string>;
|
|
220
|
+
query: Record<string, string>;
|
|
221
|
+
header: Record<string, string>;
|
|
222
|
+
body: unknown;
|
|
223
|
+
response: Record<number, WideResponseBuilder>;
|
|
224
|
+
proxy: (url: string) => { proxyUrl: string };
|
|
225
|
+
context: unknown
|
|
226
|
+
};
|
|
227
|
+
|
|
202
228
|
export type {
|
|
203
229
|
HttpStatusCode,
|
|
204
230
|
MediaType,
|
|
@@ -207,4 +233,8 @@ export type {
|
|
|
207
233
|
OpenApiResponse,
|
|
208
234
|
ResponseBuilder,
|
|
209
235
|
ResponseBuilderFactory,
|
|
236
|
+
WideOperationArgument,
|
|
237
|
+
OmitValueWhenNever
|
|
210
238
|
};
|
|
239
|
+
|
|
240
|
+
|
|
@@ -53,28 +53,24 @@ export class OperationTypeCoder extends Coder {
|
|
|
53
53
|
write(script) {
|
|
54
54
|
// eslint-disable-next-line no-param-reassign
|
|
55
55
|
script.comments = READ_ONLY_COMMENTS;
|
|
56
|
+
const xType = script.importSharedType("WideOperationArgument");
|
|
57
|
+
script.importSharedType("OmitValueWhenNever");
|
|
56
58
|
const contextTypeImportName = script.importExternalType("Context", CONTEXT_FILE_TOKEN);
|
|
57
59
|
const parameters = this.requirement.get("parameters");
|
|
58
|
-
const queryType = parameters
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const pathType = parameters === undefined
|
|
62
|
-
? "never"
|
|
63
|
-
: new ParametersTypeCoder(parameters, "path").write(script);
|
|
64
|
-
const headerType = parameters === undefined
|
|
65
|
-
? "never"
|
|
66
|
-
: 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);
|
|
67
63
|
const bodyRequirement = this.requirement.get("consumes")
|
|
68
64
|
? parameters
|
|
69
65
|
.find((parameter) => ["body", "formData"].includes(parameter.get("in").data))
|
|
70
66
|
.get("schema")
|
|
71
67
|
: this.requirement.select("requestBody/content/application~1json/schema");
|
|
72
|
-
const bodyType = bodyRequirement
|
|
73
|
-
?
|
|
74
|
-
:
|
|
68
|
+
const bodyType = bodyRequirement === undefined
|
|
69
|
+
? "never"
|
|
70
|
+
: new SchemaTypeCoder(bodyRequirement).write(script);
|
|
75
71
|
const responseType = new ResponseTypeCoder(this.requirement.get("responses"), this.requirement.get("produces")?.data ??
|
|
76
72
|
this.requirement.specification?.rootRequirement?.get("produces")?.data).write(script);
|
|
77
|
-
const proxyType =
|
|
78
|
-
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" | { ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE" }`;
|
|
79
75
|
}
|
|
80
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));
|
|
@@ -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);
|
|
@@ -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";
|
|
@@ -40,7 +39,7 @@ export class ResponseTypeCoder extends Coder {
|
|
|
40
39
|
if (response.has("content") || response.has("schema")) {
|
|
41
40
|
return printObject(this.buildContentObjectType(script, response));
|
|
42
41
|
}
|
|
43
|
-
return "
|
|
42
|
+
return "never";
|
|
44
43
|
}
|
|
45
44
|
buildHeaders(script, response) {
|
|
46
45
|
return response
|
|
@@ -52,29 +51,32 @@ export class ResponseTypeCoder extends Coder {
|
|
|
52
51
|
}
|
|
53
52
|
printHeaders(script, response) {
|
|
54
53
|
if (!response.has("headers")) {
|
|
55
|
-
return "
|
|
54
|
+
return "never";
|
|
56
55
|
}
|
|
57
56
|
return printObject(this.buildHeaders(script, response));
|
|
58
57
|
}
|
|
58
|
+
printRequiredHeaders(response) {
|
|
59
|
+
const requiredHeaders = (response.get("headers") ?? [])
|
|
60
|
+
.map((value, name) => ({ name, required: value.data.required }))
|
|
61
|
+
.filter(({ required }) => required)
|
|
62
|
+
.map(({ name }) => `"${name}"`);
|
|
63
|
+
return requiredHeaders.length === 0 ? "never" : requiredHeaders.join(" | ");
|
|
64
|
+
}
|
|
59
65
|
buildResponseObjectType(script) {
|
|
60
66
|
return printObjectWithoutQuotes(this.requirement.map((response, responseCode) => [
|
|
61
67
|
this.normalizeStatusCode(responseCode),
|
|
62
68
|
`{
|
|
63
69
|
headers: ${this.printHeaders(script, response)};
|
|
70
|
+
requiredHeaders: ${this.printRequiredHeaders(response)};
|
|
64
71
|
content: ${this.printContentObjectType(script, response)};
|
|
65
72
|
}`,
|
|
66
73
|
]));
|
|
67
74
|
}
|
|
68
75
|
write(script) {
|
|
69
|
-
|
|
70
|
-
.split("/")
|
|
71
|
-
.slice(0, -1)
|
|
72
|
-
.map(() => "..")
|
|
73
|
-
.join("/");
|
|
74
|
-
script.importExternalType("ResponseBuilderFactory", nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"));
|
|
76
|
+
script.importSharedType("ResponseBuilderFactory");
|
|
75
77
|
const text = `ResponseBuilderFactory<${this.buildResponseObjectType(script)}>`;
|
|
76
78
|
if (text.includes("HttpStatusCode")) {
|
|
77
|
-
script.
|
|
79
|
+
script.importSharedType("HttpStatusCode");
|
|
78
80
|
}
|
|
79
81
|
return text;
|
|
80
82
|
}
|
|
@@ -13,6 +13,13 @@ export class Script {
|
|
|
13
13
|
this.typeCache = new Map();
|
|
14
14
|
this.path = path;
|
|
15
15
|
}
|
|
16
|
+
get relativePathToBase() {
|
|
17
|
+
return this.path
|
|
18
|
+
.split("/")
|
|
19
|
+
.slice(0, -1)
|
|
20
|
+
.map(() => "..")
|
|
21
|
+
.join("/");
|
|
22
|
+
}
|
|
16
23
|
firstUniqueName(coder) {
|
|
17
24
|
for (const name of coder.names()) {
|
|
18
25
|
if (!this.imports.has(name) && !this.exports.has(name)) {
|
|
@@ -97,6 +104,11 @@ export class Script {
|
|
|
97
104
|
importExternalType(name, modulePath) {
|
|
98
105
|
return this.importExternal(name, modulePath, true);
|
|
99
106
|
}
|
|
107
|
+
importSharedType(name) {
|
|
108
|
+
return this.importExternal(name, nodePath
|
|
109
|
+
.join(this.relativePathToBase, "types.d.ts")
|
|
110
|
+
.replaceAll("\\", "/"), true);
|
|
111
|
+
}
|
|
100
112
|
exportType(coder) {
|
|
101
113
|
return this.export(coder, true);
|
|
102
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counterfact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.1",
|
|
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,7 +92,7 @@
|
|
|
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",
|