counterfact 1.1.7 → 1.3.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/bin/counterfact.js +5 -0
- package/dist/app.js +1 -1
- package/dist/server/dispatcher.js +7 -11
- package/dist/server/module-loader.js +24 -15
- package/dist/server/module-tree.js +20 -13
- package/dist/server/registry.js +38 -1
- package/dist/server/response-builder.js +7 -2
- package/dist/server/types.ts +1 -0
- package/package.json +18 -18
package/bin/counterfact.js
CHANGED
|
@@ -135,6 +135,7 @@ async function main(source, destination) {
|
|
|
135
135
|
const swaggerUrl = `${url}/counterfact/swagger/`;
|
|
136
136
|
|
|
137
137
|
const config = {
|
|
138
|
+
alwaysFakeOptionals: options.alwaysFakeOptionals,
|
|
138
139
|
basePath,
|
|
139
140
|
|
|
140
141
|
generate: {
|
|
@@ -268,5 +269,9 @@ program
|
|
|
268
269
|
"base path from which routes will be served (e.g. /api/v1)",
|
|
269
270
|
"",
|
|
270
271
|
)
|
|
272
|
+
.option(
|
|
273
|
+
"--always-fake-optionals",
|
|
274
|
+
"random responses will include optional fields",
|
|
275
|
+
)
|
|
271
276
|
.action(main)
|
|
272
277
|
.parse(process.argv);
|
package/dist/app.js
CHANGED
|
@@ -32,7 +32,7 @@ export async function counterfact(config) {
|
|
|
32
32
|
const registry = new Registry();
|
|
33
33
|
const contextRegistry = new ContextRegistry();
|
|
34
34
|
const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath, config.generate);
|
|
35
|
-
const dispatcher = new Dispatcher(registry, contextRegistry, await loadOpenApiDocument(config.openApiPath));
|
|
35
|
+
const dispatcher = new Dispatcher(registry, contextRegistry, await loadOpenApiDocument(config.openApiPath), config);
|
|
36
36
|
const transpiler = new Transpiler(nodePath.join(modulesPath, "routes").replaceAll("\\", "/"), compiledPathsDirectory, "commonjs");
|
|
37
37
|
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
|
|
38
38
|
const middleware = koaMiddleware(dispatcher, config);
|
|
@@ -9,11 +9,13 @@ export class Dispatcher {
|
|
|
9
9
|
contextRegistry;
|
|
10
10
|
openApiDocument;
|
|
11
11
|
fetch;
|
|
12
|
-
|
|
12
|
+
config; // Add config property
|
|
13
|
+
constructor(registry, contextRegistry, openApiDocument, config) {
|
|
13
14
|
this.registry = registry;
|
|
14
15
|
this.contextRegistry = contextRegistry;
|
|
15
16
|
this.openApiDocument = openApiDocument;
|
|
16
17
|
this.fetch = fetch;
|
|
18
|
+
this.config = config;
|
|
17
19
|
}
|
|
18
20
|
parameterTypes(parameters) {
|
|
19
21
|
const types = {
|
|
@@ -60,14 +62,6 @@ export class Dispatcher {
|
|
|
60
62
|
return operation;
|
|
61
63
|
}
|
|
62
64
|
normalizeResponse(response, acceptHeader) {
|
|
63
|
-
if (typeof response === "string") {
|
|
64
|
-
return {
|
|
65
|
-
body: response,
|
|
66
|
-
contentType: "text/plain",
|
|
67
|
-
headers: {},
|
|
68
|
-
status: 200,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
65
|
if (response.content !== undefined) {
|
|
72
66
|
const content = this.selectContent(acceptHeader, response.content);
|
|
73
67
|
if (content === undefined) {
|
|
@@ -87,7 +81,9 @@ export class Dispatcher {
|
|
|
87
81
|
}
|
|
88
82
|
return {
|
|
89
83
|
...response,
|
|
90
|
-
contentType: response.headers?.["content-type"]?.toString() ??
|
|
84
|
+
contentType: response.headers?.["content-type"]?.toString() ??
|
|
85
|
+
response.contentType ??
|
|
86
|
+
"unknown/unknown",
|
|
91
87
|
};
|
|
92
88
|
}
|
|
93
89
|
selectContent(acceptHeader, content) {
|
|
@@ -147,7 +143,7 @@ export class Dispatcher {
|
|
|
147
143
|
},
|
|
148
144
|
query,
|
|
149
145
|
// @ts-expect-error - Might be pushing the limits of what TypeScript can do here
|
|
150
|
-
response: createResponseBuilder(operation ?? { responses: {} }),
|
|
146
|
+
response: createResponseBuilder(operation ?? { responses: {} }, this.config), // Pass config
|
|
151
147
|
tools: new Tools({ headers }),
|
|
152
148
|
});
|
|
153
149
|
if (response === undefined) {
|
|
@@ -15,6 +15,11 @@ const debug = createDebug("counterfact:server:module-loader");
|
|
|
15
15
|
function isContextModule(module) {
|
|
16
16
|
return "Context" in module && typeof module.Context === "function";
|
|
17
17
|
}
|
|
18
|
+
function isMiddlewareModule(module) {
|
|
19
|
+
return ("middleware" in module &&
|
|
20
|
+
typeof Object.getOwnPropertyDescriptor(module, "middleware")?.value ===
|
|
21
|
+
"function");
|
|
22
|
+
}
|
|
18
23
|
export class ModuleLoader extends EventTarget {
|
|
19
24
|
basePath;
|
|
20
25
|
registry;
|
|
@@ -96,23 +101,27 @@ export class ModuleLoader extends EventTarget {
|
|
|
96
101
|
const doImport = (await determineModuleKind(pathName)) === "commonjs"
|
|
97
102
|
? uncachedRequire
|
|
98
103
|
: uncachedImport;
|
|
99
|
-
const endpoint = (await doImport(pathName))
|
|
104
|
+
const endpoint = (await doImport(pathName).catch((err) => {
|
|
105
|
+
console.log("ERROR");
|
|
106
|
+
}));
|
|
100
107
|
this.dispatchEvent(new Event("add"));
|
|
101
|
-
if (basename(pathName).startsWith("_.context")
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
if (basename(pathName).startsWith("_.context.") &&
|
|
109
|
+
isContextModule(endpoint)) {
|
|
110
|
+
const loadContext = (path) => this.contextRegistry.find(path);
|
|
111
|
+
this.contextRegistry.update(directory,
|
|
112
|
+
// @ts-expect-error TS says Context has no constructable signatures but that's not true?
|
|
113
|
+
new endpoint.Context({
|
|
114
|
+
loadContext,
|
|
115
|
+
}));
|
|
116
|
+
return;
|
|
110
117
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.registry.add(url, endpoint);
|
|
118
|
+
if (basename(pathName).startsWith("_.middleware.") &&
|
|
119
|
+
isMiddlewareModule(endpoint)) {
|
|
120
|
+
this.registry.addMiddleware(url.slice(0, url.lastIndexOf("/")) || "/", endpoint.middleware);
|
|
115
121
|
}
|
|
122
|
+
if (url === "/index")
|
|
123
|
+
this.registry.add("/", endpoint);
|
|
124
|
+
this.registry.add(url, endpoint);
|
|
116
125
|
}
|
|
117
126
|
catch (error) {
|
|
118
127
|
if (String(error) ===
|
|
@@ -120,8 +129,8 @@ export class ModuleLoader extends EventTarget {
|
|
|
120
129
|
// Not sure why Node throws this error. It doesn't seem to matter.
|
|
121
130
|
return;
|
|
122
131
|
}
|
|
123
|
-
throw error;
|
|
124
132
|
process.stdout.write(`\nError loading ${pathName}:\n${String(error)}\n`);
|
|
133
|
+
throw error;
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
}
|
|
@@ -9,31 +9,38 @@ export class ModuleTree {
|
|
|
9
9
|
name: "",
|
|
10
10
|
rawName: "",
|
|
11
11
|
};
|
|
12
|
-
|
|
13
|
-
if (directory === undefined) {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
12
|
+
putDirectory(directory, segments) {
|
|
16
13
|
const [segment, ...remainingSegments] = segments;
|
|
17
14
|
if (segment === undefined) {
|
|
18
15
|
throw new Error("segments array is empty");
|
|
19
16
|
}
|
|
20
17
|
if (remainingSegments.length === 0) {
|
|
21
|
-
directory
|
|
22
|
-
isWildcard: segment.startsWith("{"),
|
|
23
|
-
module,
|
|
24
|
-
name: segment.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
|
|
25
|
-
rawName: segment,
|
|
26
|
-
};
|
|
27
|
-
return;
|
|
18
|
+
return directory;
|
|
28
19
|
}
|
|
29
|
-
directory.directories[segment.toLowerCase()] ??= {
|
|
20
|
+
const nextDirectory = (directory.directories[segment.toLowerCase()] ??= {
|
|
30
21
|
directories: {},
|
|
31
22
|
files: {},
|
|
32
23
|
isWildcard: segment.startsWith("{"),
|
|
33
24
|
name: segment.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
|
|
34
25
|
rawName: segment,
|
|
26
|
+
});
|
|
27
|
+
return this.putDirectory(nextDirectory, remainingSegments);
|
|
28
|
+
}
|
|
29
|
+
addModuleToDirectory(directory, segments, module) {
|
|
30
|
+
if (directory === undefined) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const targetDirectory = this.putDirectory(directory, segments);
|
|
34
|
+
const filename = segments.at(-1);
|
|
35
|
+
if (filename === undefined) {
|
|
36
|
+
throw new Error("The file name (the last segment of the URL) is undefined. This is theoretically impossible but TypeScript can't enforce it.");
|
|
37
|
+
}
|
|
38
|
+
targetDirectory.files[filename] = {
|
|
39
|
+
isWildcard: filename.startsWith("{"),
|
|
40
|
+
module,
|
|
41
|
+
name: filename.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
|
|
42
|
+
rawName: filename,
|
|
35
43
|
};
|
|
36
|
-
this.addModuleToDirectory(directory.directories[segment.toLocaleLowerCase()], remainingSegments, module);
|
|
37
44
|
}
|
|
38
45
|
add(url, module) {
|
|
39
46
|
this.addModuleToDirectory(this.root, url.split("/").slice(1), module);
|
package/dist/server/registry.js
CHANGED
|
@@ -25,12 +25,19 @@ function castParameters(parameters = {}, parameterTypes = {}) {
|
|
|
25
25
|
}
|
|
26
26
|
export class Registry {
|
|
27
27
|
moduleTree = new ModuleTree();
|
|
28
|
+
middlewares = new Map();
|
|
29
|
+
constructor() {
|
|
30
|
+
this.middlewares.set("/", ($, respondTo) => respondTo($));
|
|
31
|
+
}
|
|
28
32
|
get routes() {
|
|
29
33
|
return this.moduleTree.routes;
|
|
30
34
|
}
|
|
31
35
|
add(url, module) {
|
|
32
36
|
this.moduleTree.add(url, module);
|
|
33
37
|
}
|
|
38
|
+
addMiddleware(url, callback) {
|
|
39
|
+
this.middlewares.set(url, callback);
|
|
40
|
+
}
|
|
34
41
|
remove(url) {
|
|
35
42
|
this.moduleTree.remove(url);
|
|
36
43
|
}
|
|
@@ -66,7 +73,37 @@ export class Registry {
|
|
|
66
73
|
query: castParameters(requestData.query, parameterTypes.query),
|
|
67
74
|
};
|
|
68
75
|
operationArgument.x = operationArgument;
|
|
69
|
-
|
|
76
|
+
const executeAndNormalizeResponse = async (requestData) => {
|
|
77
|
+
const result = await execute(requestData);
|
|
78
|
+
if (typeof result === "string") {
|
|
79
|
+
return {
|
|
80
|
+
headers: {},
|
|
81
|
+
status: 200,
|
|
82
|
+
body: result,
|
|
83
|
+
contentType: "text/plain",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (typeof result === "undefined") {
|
|
87
|
+
return {
|
|
88
|
+
headers: {},
|
|
89
|
+
body: `The ${httpRequestMethod} function did not return anything. Did you forget a return statement?`,
|
|
90
|
+
status: 500,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
};
|
|
95
|
+
const middlewares = this.middlewares;
|
|
96
|
+
function recurse(path, respondTo) {
|
|
97
|
+
if (path === null)
|
|
98
|
+
return respondTo;
|
|
99
|
+
const nextPath = path === "" ? null : path.slice(0, path.lastIndexOf("/"));
|
|
100
|
+
const middleware = middlewares.get(path);
|
|
101
|
+
if (middleware !== undefined) {
|
|
102
|
+
return recurse(nextPath, ($) => middleware($, respondTo));
|
|
103
|
+
}
|
|
104
|
+
return recurse(nextPath, respondTo);
|
|
105
|
+
}
|
|
106
|
+
return recurse(operationArgument.matchedPath ?? "/", executeAndNormalizeResponse)(operationArgument);
|
|
70
107
|
};
|
|
71
108
|
}
|
|
72
109
|
}
|
|
@@ -29,7 +29,7 @@ function unknownStatusCodeResponse(statusCode) {
|
|
|
29
29
|
status: 500,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
export function createResponseBuilder(operation) {
|
|
32
|
+
export function createResponseBuilder(operation, config) {
|
|
33
33
|
return new Proxy({}, {
|
|
34
34
|
get: (target, statusCode) => ({
|
|
35
35
|
header(name, value) {
|
|
@@ -55,7 +55,7 @@ export function createResponseBuilder(operation) {
|
|
|
55
55
|
return {
|
|
56
56
|
...this,
|
|
57
57
|
content: [
|
|
58
|
-
...(this.content ?? []),
|
|
58
|
+
...(this.content ?? []).filter((response) => response.type !== contentType),
|
|
59
59
|
{
|
|
60
60
|
body: convertToXmlIfNecessary(contentType, body, operation.responses[this.status ?? "default"]?.content?.[contentType]?.schema),
|
|
61
61
|
type: contentType,
|
|
@@ -64,6 +64,11 @@ export function createResponseBuilder(operation) {
|
|
|
64
64
|
};
|
|
65
65
|
},
|
|
66
66
|
random() {
|
|
67
|
+
if (config?.alwaysFakeOptionals) {
|
|
68
|
+
JSONSchemaFaker.option("alwaysFakeOptionals", true);
|
|
69
|
+
JSONSchemaFaker.option("fixedProbabilities", true);
|
|
70
|
+
JSONSchemaFaker.option("optionalsProbability", 1.0);
|
|
71
|
+
}
|
|
67
72
|
if (operation.produces) {
|
|
68
73
|
return this.randomLegacy();
|
|
69
74
|
}
|
package/dist/server/types.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counterfact",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"postinstall": "patch-package"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@changesets/cli": "2.
|
|
47
|
+
"@changesets/cli": "2.29.2",
|
|
48
48
|
"@stryker-mutator/core": "8.7.1",
|
|
49
49
|
"@stryker-mutator/jest-runner": "8.7.1",
|
|
50
50
|
"@stryker-mutator/typescript-checker": "8.7.1",
|
|
51
|
-
"@swc/core": "1.
|
|
51
|
+
"@swc/core": "1.11.21",
|
|
52
52
|
"@swc/jest": "0.2.37",
|
|
53
53
|
"@testing-library/dom": "10.4.0",
|
|
54
54
|
"@types/jest": "29.5.14",
|
|
@@ -57,19 +57,19 @@
|
|
|
57
57
|
"@types/koa-bodyparser": "4.3.12",
|
|
58
58
|
"@types/koa-proxy": "1.0.7",
|
|
59
59
|
"@types/koa-static": "4.0.4",
|
|
60
|
-
"@types/lodash": "4.17.
|
|
60
|
+
"@types/lodash": "4.17.16",
|
|
61
61
|
"copyfiles": "2.4.1",
|
|
62
|
-
"eslint": "9.
|
|
62
|
+
"eslint": "9.25.0",
|
|
63
63
|
"eslint-config-hardcore": "47.0.1",
|
|
64
64
|
"eslint-formatter-github-annotations": "0.1.0",
|
|
65
|
-
"eslint-import-resolver-typescript": "3.
|
|
65
|
+
"eslint-import-resolver-typescript": "4.3.2",
|
|
66
66
|
"eslint-plugin-etc": "2.0.3",
|
|
67
|
-
"eslint-plugin-file-progress": "3.0.
|
|
67
|
+
"eslint-plugin-file-progress": "3.0.2",
|
|
68
68
|
"eslint-plugin-import": "2.31.0",
|
|
69
69
|
"eslint-plugin-jest": "28.11.0",
|
|
70
70
|
"eslint-plugin-jest-dom": "5.5.0",
|
|
71
71
|
"eslint-plugin-no-explicit-type-exports": "0.12.1",
|
|
72
|
-
"eslint-plugin-prettier": "5.2.
|
|
72
|
+
"eslint-plugin-prettier": "5.2.6",
|
|
73
73
|
"eslint-plugin-unused-imports": "4.1.4",
|
|
74
74
|
"husky": "9.1.7",
|
|
75
75
|
"jest": "29.7.0",
|
|
@@ -77,35 +77,35 @@
|
|
|
77
77
|
"node-mocks-http": "1.16.2",
|
|
78
78
|
"rimraf": "6.0.1",
|
|
79
79
|
"stryker-cli": "1.0.2",
|
|
80
|
-
"supertest": "7.
|
|
80
|
+
"supertest": "7.1.0",
|
|
81
81
|
"using-temporary-files": "2.2.1"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@apidevtools/json-schema-ref-parser": "
|
|
84
|
+
"@apidevtools/json-schema-ref-parser": "12.0.1",
|
|
85
85
|
"@hapi/accept": "6.0.3",
|
|
86
86
|
"@types/json-schema": "7.0.15",
|
|
87
87
|
"ast-types": "0.14.2",
|
|
88
88
|
"chokidar": "4.0.3",
|
|
89
|
-
"commander": "13.
|
|
89
|
+
"commander": "13.1.0",
|
|
90
90
|
"debug": "4.4.0",
|
|
91
91
|
"fetch": "1.1.0",
|
|
92
92
|
"fs-extra": "11.3.0",
|
|
93
93
|
"handlebars": "4.7.8",
|
|
94
94
|
"http-terminator": "3.2.0",
|
|
95
95
|
"js-yaml": "4.1.0",
|
|
96
|
-
"json-schema-faker": "0.5.
|
|
96
|
+
"json-schema-faker": "0.5.9",
|
|
97
97
|
"jsonwebtoken": "9.0.2",
|
|
98
|
-
"koa": "2.
|
|
98
|
+
"koa": "2.16.1",
|
|
99
99
|
"koa-bodyparser": "4.4.1",
|
|
100
100
|
"koa-proxies": "0.12.4",
|
|
101
101
|
"koa2-swagger-ui": "5.11.0",
|
|
102
102
|
"lodash": "4.17.21",
|
|
103
103
|
"node-fetch": "3.3.2",
|
|
104
|
-
"open": "10.1.
|
|
104
|
+
"open": "10.1.1",
|
|
105
105
|
"patch-package": "8.0.0",
|
|
106
|
-
"precinct": "12.
|
|
107
|
-
"prettier": "3.
|
|
108
|
-
"recast": "0.23.
|
|
109
|
-
"typescript": "5.
|
|
106
|
+
"precinct": "12.2.0",
|
|
107
|
+
"prettier": "3.5.3",
|
|
108
|
+
"recast": "0.23.11",
|
|
109
|
+
"typescript": "5.8.3"
|
|
110
110
|
}
|
|
111
111
|
}
|