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.
@@ -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
- constructor(registry, contextRegistry, openApiDocument) {
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() ?? "unknown/unknown",
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
- if (isContextModule(endpoint)) {
103
- const loadContext = (path) => this.contextRegistry.find(path);
104
- this.contextRegistry.update(directory,
105
- // @ts-expect-error TS says Context has no constructable signatures but that's not true?
106
- new endpoint.Context({
107
- loadContext,
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
- else {
112
- if (url === "/index")
113
- this.registry.add("/", endpoint);
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
- addModuleToDirectory(directory, segments, module) {
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.files[segment.toLowerCase()] = {
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);
@@ -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
- return await execute(operationArgument);
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
  }
@@ -278,4 +278,5 @@ export type {
278
278
  ResponseBuilder,
279
279
  ResponseBuilderFactory,
280
280
  WideOperationArgument,
281
+ WideResponseBuilder,
281
282
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "counterfact",
3
- "version": "1.1.7",
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.27.11",
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.10.7",
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.14",
60
+ "@types/lodash": "4.17.16",
61
61
  "copyfiles": "2.4.1",
62
- "eslint": "9.18.0",
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.7.0",
65
+ "eslint-import-resolver-typescript": "4.3.2",
66
66
  "eslint-plugin-etc": "2.0.3",
67
- "eslint-plugin-file-progress": "3.0.1",
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.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.0.0",
80
+ "supertest": "7.1.0",
81
81
  "using-temporary-files": "2.2.1"
82
82
  },
83
83
  "dependencies": {
84
- "@apidevtools/json-schema-ref-parser": "11.7.3",
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.0.0",
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.8",
96
+ "json-schema-faker": "0.5.9",
97
97
  "jsonwebtoken": "9.0.2",
98
- "koa": "2.15.3",
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.0",
104
+ "open": "10.1.1",
105
105
  "patch-package": "8.0.0",
106
- "precinct": "12.1.2",
107
- "prettier": "3.4.2",
108
- "recast": "0.23.9",
109
- "typescript": "5.7.3"
106
+ "precinct": "12.2.0",
107
+ "prettier": "3.5.3",
108
+ "recast": "0.23.11",
109
+ "typescript": "5.8.3"
110
110
  }
111
111
  }