counterfact 0.17.0 → 0.19.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.
@@ -18,11 +18,11 @@
18
18
  "cSpell.words": [
19
19
  "bodyparser",
20
20
  "counterfact",
21
+ "counterfactuals",
21
22
  "Deno",
22
23
  "openapi",
23
24
  "Petstore",
24
- "counterfactuals",
25
- "openapi",
25
+ "proxied",
26
26
  "transpiles"
27
27
  ]
28
28
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # counterfact
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 196f7f4: new `--proxyURL <url>` CLI option and `.proxy [on|off]` command in the REPL
8
+
9
+ ## 0.18.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 1244b6e: support for allOf, anyOf, oneOf, and not
14
+
15
+ ### Patch Changes
16
+
17
+ - 1d360bd: OpenAPI 2 -- fall back to top-level produces and consumes
18
+
3
19
  ## 0.17.0
4
20
 
5
21
  ### Minor Changes
@@ -2,13 +2,13 @@
2
2
 
3
3
  // eslint-disable-next-line node/shebang
4
4
  import nodePath from "node:path";
5
- import repl from "node:repl";
6
5
 
7
6
  import { program } from "commander";
8
7
  import open from "open";
9
8
 
10
9
  import { generate } from "../src/typescript-generator/generate.js";
11
10
  import { start } from "../src/server/start.js";
11
+ import { startRepl } from "../src/server/repl.js";
12
12
 
13
13
  const DEFAULT_PORT = 3100;
14
14
 
@@ -26,12 +26,15 @@ async function main(source, destination) {
26
26
 
27
27
  const guiUrl = `${url}/counterfact/`;
28
28
 
29
- const { contextRegistry } = await start({
29
+ const config = {
30
30
  basePath,
31
31
  port: options.port,
32
32
  openApiPath: source,
33
33
  includeSwaggerUi: true,
34
- });
34
+ proxyUrl: options.proxyUrl,
35
+ };
36
+
37
+ const { contextRegistry } = await start(config);
35
38
 
36
39
  const waysToInteract = [
37
40
  `Call the REST APIs at ${url} (with your front end app, curl, Postman, etc.)`,
@@ -62,35 +65,7 @@ async function main(source, destination) {
62
65
 
63
66
  process.stdout.write("Starting REPL...\n");
64
67
 
65
- const replServer = repl.start("> ");
66
-
67
- replServer.defineCommand("counterfact", {
68
- help: "Get help with Counterfact",
69
-
70
- action() {
71
- process.stdout.write(
72
- "This is a read-eval-print loop (REPL), the same as the one you get when you run node with no arguments.\n"
73
- );
74
- process.stdout.write(
75
- "Except that it's connected to the running server, which you can access with the following globals:\n\n"
76
- );
77
- process.stdout.write(
78
- "- loadContext('/some/path'): to access the context object for a given path\n"
79
- );
80
- process.stdout.write(
81
- "- context: the root context ( same as loadContext('/') )\n"
82
- );
83
- process.stdout.write(
84
- "\nFor more information, see https://counterfact.dev/docs/usage.html\n\n"
85
- );
86
-
87
- this.clearBufferedCommand();
88
- this.displayPrompt();
89
- },
90
- });
91
-
92
- replServer.context.loadContext = (path) => contextRegistry.find(path);
93
- replServer.context.context = replServer.context.loadContext("/");
68
+ startRepl(contextRegistry, config);
94
69
 
95
70
  if (openBrowser) {
96
71
  await open(guiUrl);
@@ -107,5 +82,6 @@ program
107
82
  .option("--port <number>", "server port number", DEFAULT_PORT)
108
83
  .option("--swagger", "include swagger-ui")
109
84
  .option("--open", "open a browser")
85
+ .option("--proxyUrl <string>", "proxy URL")
110
86
  .action(main)
111
87
  .parse(process.argv);
package/docs/usage.md CHANGED
@@ -215,6 +215,25 @@ context.pets.find((pet) => pet.name.startsWith("F"));
215
215
 
216
216
  Using the REPL is a lot faster (and more fun) than wrangling config files and SQL and whatever else it takes to a real back end into the states you need to test your UI flows.
217
217
 
218
+ ## Proxy Peek-a-boo 🫣
219
+
220
+ At some point you're going to want to test your code against a real server. Or maybe you want to use Counterfact for newer endpoints that don't exist in the real server yet and a real server for everything else. Counterfact has a couple of facilities that allow you to _proxy_ to the real server on a case-by-case basis.
221
+
222
+ To proxy an individual endpoint, you can use the `$.proxy()` function.
223
+
224
+ ```ts copy
225
+ // pet/{id}.ts
226
+ export const GET: HTTP_GET ($) => {
227
+ return $.proxy("http://uat.petstore.example.com/pet")
228
+ };
229
+ ```
230
+
231
+ To toggle globally between Counterfact and a proxy server, pass `--proxy-url <url>` in he CLI.
232
+
233
+ Then type `.proxy on` / `.proxy off` in the REPL to turn it on and off. When the global proxy is on, all requests will be sent to the proxy URL instead of the mock implementations in Counterfact.
234
+
235
+ This feature is hot off the presses and somewhat experimental. We have plans to introduce more granular controls over what gets proxied when, but we want to see how this works first. Please send feedback!
236
+
218
237
  ## No Cap Recap 🧢
219
238
 
220
239
  Using convention over configuration and automatically generated types, Counterfact allows front-end developers to quickly build fake REST APIs for prototype and testing purposes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "counterfact",
3
- "version": "0.17.0",
3
+ "version": "0.19.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",
@@ -36,12 +36,12 @@
36
36
  "counterfact": "ts-node --esm --transpileOnly ./bin/counterfact.js"
37
37
  },
38
38
  "devDependencies": {
39
- "@changesets/cli": "2.25.2",
40
- "@stryker-mutator/core": "6.3.0",
41
- "@stryker-mutator/jest-runner": "6.3.0",
39
+ "@changesets/cli": "2.26.0",
40
+ "@stryker-mutator/core": "6.3.1",
41
+ "@stryker-mutator/jest-runner": "6.3.1",
42
42
  "@types/koa": "2.13.5",
43
43
  "@types/koa-static": "^4.0.2",
44
- "eslint": "8.29.0",
44
+ "eslint": "8.32.0",
45
45
  "eslint-config-hardcore": "25.1.0",
46
46
  "eslint-formatter-github-annotations": "0.1.0",
47
47
  "eslint-import-resolver-typescript": "^3.2.5",
@@ -50,7 +50,7 @@
50
50
  "eslint-plugin-import": "^2.26.0",
51
51
  "eslint-plugin-jest": "^27.0.1",
52
52
  "eslint-plugin-no-explicit-type-exports": "^0.12.1",
53
- "husky": "8.0.2",
53
+ "husky": "8.0.3",
54
54
  "jest": "28.1.2",
55
55
  "nodemon": "2.0.20",
56
56
  "stryker-cli": "1.0.2",
@@ -60,14 +60,14 @@
60
60
  "@hapi/accept": "^6.0.0",
61
61
  "@types/json-schema": "^7.0.11",
62
62
  "chokidar": "^3.5.3",
63
- "commander": "^9.4.0",
63
+ "commander": "^10.0.0",
64
64
  "fetch": "^1.1.0",
65
65
  "fs-extra": "^11.0.0",
66
66
  "handlebars": "^4.7.7",
67
67
  "js-yaml": "^4.1.0",
68
68
  "json-schema-faker": "^0.5.0-rcv.44",
69
69
  "json-schema-ref-parser": "^9.0.9",
70
- "jsonwebtoken": "^8.5.1",
70
+ "jsonwebtoken": "^9.0.0",
71
71
  "koa": "^2.13.4",
72
72
  "koa-bodyparser": "^4.3.0",
73
73
  "koa-proxy": "^1.0.0-alpha.3",
@@ -21,7 +21,8 @@ async function loadOpenApiDocument(source) {
21
21
 
22
22
  export async function counterfact(
23
23
  basePath,
24
- openApiPath = nodePath.join(basePath, "../openapi.yaml")
24
+ openApiPath = nodePath.join(basePath, "../openapi.yaml"),
25
+ options = {}
25
26
  ) {
26
27
  const openApiDocument = await loadOpenApiDocument(openApiPath);
27
28
 
@@ -43,7 +44,7 @@ export async function counterfact(
43
44
  await moduleLoader.watch();
44
45
 
45
46
  return {
46
- koaMiddleware: koaMiddleware(dispatcher),
47
+ koaMiddleware: koaMiddleware(dispatcher, options),
47
48
  registry,
48
49
  moduleLoader,
49
50
  contextRegistry,
@@ -6,7 +6,7 @@ export function koaMiddleware(dispatcher, options = {}, proxy = koaProxy) {
6
6
  return async function middleware(ctx, next) {
7
7
  const { method, path, headers, body, query } = ctx.request;
8
8
 
9
- if (options.proxyUrl) {
9
+ if (options.proxyEnabled && options.proxyUrl) {
10
10
  return proxy({ host: options.proxyUrl })(ctx, next);
11
11
  }
12
12
 
@@ -0,0 +1,56 @@
1
+ import repl from "node:repl";
2
+
3
+ export function startRepl(contextRegistry, config) {
4
+ const replServer = repl.start("> ");
5
+
6
+ replServer.defineCommand("counterfact", {
7
+ help: "Get help with Counterfact",
8
+
9
+ action() {
10
+ process.stdout.write(
11
+ "This is a read-eval-print loop (REPL), the same as the one you get when you run node with no arguments.\n"
12
+ );
13
+ process.stdout.write(
14
+ "Except that it's connected to the running server, which you can access with the following globals:\n\n"
15
+ );
16
+ process.stdout.write(
17
+ "- loadContext('/some/path'): to access the context object for a given path\n"
18
+ );
19
+ process.stdout.write(
20
+ "- context: the root context ( same as loadContext('/') )\n"
21
+ );
22
+ process.stdout.write(
23
+ "\nFor more information, see https://counterfact.dev/docs/usage.html\n\n"
24
+ );
25
+
26
+ this.clearBufferedCommand();
27
+ this.displayPrompt();
28
+ },
29
+ });
30
+
31
+ replServer.defineCommand("proxy", {
32
+ help: "proxy [on|off] - turn the proxy on or off; proxy - print proxy info",
33
+
34
+ action(state) {
35
+ if (state === "on") {
36
+ // eslint-disable-next-line no-param-reassign
37
+ config.enableProxy = true;
38
+ }
39
+
40
+ if (state === "off") {
41
+ // eslint-disable-next-line no-param-reassign
42
+ config.enableProxy = false;
43
+ }
44
+
45
+ process.stdout.write(
46
+ `Proxy is ${config.enableProxy ? "on" : "off"}: ${config.proxyUrl}\n`
47
+ );
48
+
49
+ this.clearBufferedCommand();
50
+ this.displayPrompt();
51
+ },
52
+ });
53
+
54
+ replServer.context.loadContext = (path) => contextRegistry.find(path);
55
+ replServer.context.context = replServer.context.loadContext("/");
56
+ }
@@ -70,12 +70,14 @@ export async function start({
70
70
  basePath = process.cwd(),
71
71
  port = DEFAULT_PORT,
72
72
  openApiPath = nodePath.join(basePath, "../openapi.yaml"),
73
+ proxyUrl = undefined,
73
74
  }) {
74
75
  const app = new Koa();
75
76
 
76
77
  const { koaMiddleware, contextRegistry, registry } = await counterfact(
77
78
  basePath,
78
- openApiPath
79
+ openApiPath,
80
+ { proxyUrl }
79
81
  );
80
82
 
81
83
  app.use(openapi(openApiPath, `//localhost:${port}`));
@@ -7,9 +7,9 @@ export async function generate(
7
7
  destination,
8
8
  repository = new Repository()
9
9
  ) {
10
- const specification = new Specification();
10
+ const specification = new Specification(source);
11
11
 
12
- const requirement = await specification.requirementAt(`${source}#/paths`);
12
+ const requirement = await specification.requirementAt("#/paths");
13
13
 
14
14
  requirement.forEach((pathDefinition, key) => {
15
15
  pathDefinition.forEach((operation) => {
@@ -33,9 +33,12 @@ export class OperationTypeCoder extends Coder {
33
33
  }
34
34
 
35
35
  if (response.has("schema")) {
36
- return this.requirement
37
- .get("produces")
38
- .data.map(
36
+ const produces =
37
+ this.requirement?.get("produces")?.data ??
38
+ this.requirement.specification.requirementAt("#/produces").data;
39
+
40
+ return produces
41
+ .map(
39
42
  (contentType) => `{
40
43
  status: ${status},
41
44
  contentType?: "${contentType}",
@@ -55,17 +55,7 @@ export class SchemaTypeCoder extends Coder {
55
55
  )}>`;
56
56
  }
57
57
 
58
- modulePath() {
59
- return `components/${this.requirement.data.$ref.split("/").at(-1)}.ts`;
60
- }
61
-
62
- write(script) {
63
- if (this.requirement.isReference) {
64
- return script.importType(this);
65
- }
66
-
67
- const { type } = this.requirement.data;
68
-
58
+ writeType(script, type) {
69
59
  if (type === "object") {
70
60
  return this.objectSchema(script);
71
61
  }
@@ -78,6 +68,46 @@ export class SchemaTypeCoder extends Coder {
78
68
  return "number";
79
69
  }
80
70
 
81
- return type;
71
+ return type ?? "unknown";
72
+ }
73
+
74
+ writeGroup(script, { allOf, anyOf, oneOf }) {
75
+ function matchingKey() {
76
+ if (allOf) {
77
+ return "allOf";
78
+ }
79
+
80
+ if (anyOf) {
81
+ return "anyOf";
82
+ }
83
+
84
+ return "oneOf";
85
+ }
86
+
87
+ const types = (allOf ?? anyOf ?? oneOf).map((item, index) =>
88
+ new SchemaTypeCoder(this.requirement.get(matchingKey()).get(index)).write(
89
+ script
90
+ )
91
+ );
92
+
93
+ return types.join(allOf ? " & " : " | ");
94
+ }
95
+
96
+ modulePath() {
97
+ return `components/${this.requirement.data.$ref.split("/").at(-1)}.ts`;
98
+ }
99
+
100
+ write(script) {
101
+ if (this.requirement.isReference) {
102
+ return script.importType(this);
103
+ }
104
+
105
+ const { type, allOf, anyOf, oneOf } = this.requirement.data;
106
+
107
+ if (allOf ?? anyOf ?? oneOf) {
108
+ return this.writeGroup(script, { allOf, anyOf, oneOf });
109
+ }
110
+
111
+ return this.writeType(script, type);
82
112
  }
83
113
  }
@@ -7,17 +7,20 @@ import { readFile } from "../util/read-file.js";
7
7
  import { Requirement } from "./requirement.js";
8
8
 
9
9
  export class Specification {
10
- constructor() {
10
+ constructor(rootUrl) {
11
11
  this.cache = new Map();
12
+ this.rootUrl = rootUrl;
12
13
  }
13
14
 
14
15
  async requirementAt(url, fromUrl = "") {
15
16
  const [file, path] = url.split("#");
16
17
 
17
18
  const filePath = nodePath.join(fromUrl.split("#").at(0), file);
18
- const data = await this.loadFile(filePath);
19
19
 
20
- const rootRequirement = new Requirement(data, `${filePath}#`, this);
20
+ const fileUrl = filePath === "." ? this.rootUrl : filePath;
21
+ const data = await this.loadFile(fileUrl);
22
+
23
+ const rootRequirement = new Requirement(data, `${fileUrl}#`, this);
21
24
 
22
25
  return rootRequirement.select(path.slice(1));
23
26
  }
@@ -1,3 +1,5 @@
1
+ import http from "node:http";
2
+
1
3
  import supertest from "supertest";
2
4
  import Koa from "koa";
3
5
 
@@ -91,4 +93,45 @@ describe("integration test", () => {
91
93
  await moduleLoader.stopWatching();
92
94
  });
93
95
  });
96
+
97
+ it("proxies when a proxyUrl is present and the proxy is enabled", async () => {
98
+ const proxyTarget = http.createServer((req, res) => {
99
+ res.writeHead(200, {
100
+ "Content-Type": "text/plain",
101
+ });
102
+
103
+ res.end("I am proxy!\n");
104
+ });
105
+
106
+ proxyTarget.listen(8121);
107
+
108
+ const app = new Koa();
109
+ const request = supertest(app.callback());
110
+ const files = {
111
+ "paths/hello.mjs": `
112
+ export async function GET() {
113
+ return await Promise.resolve({ body: "GET /hello" });
114
+ }
115
+ `,
116
+ };
117
+
118
+ await withTemporaryFiles(files, async (basePath) => {
119
+ const { koaMiddleware, moduleLoader } = await counterfact(
120
+ basePath,
121
+ `${basePath}/openapi.yaml`,
122
+ {
123
+ proxyUrl: `http://localhost:${proxyTarget.address().port}`,
124
+ proxyEnabled: true,
125
+ }
126
+ );
127
+
128
+ app.use(koaMiddleware);
129
+
130
+ const getResponse = await request.get("/hello");
131
+
132
+ expect(getResponse.text).toBe("I am proxy!\n");
133
+
134
+ await moduleLoader.stopWatching();
135
+ });
136
+ });
94
137
  });
@@ -55,7 +55,7 @@ describe("koa middleware", () => {
55
55
  expect(ctx.status).toBe(304);
56
56
  });
57
57
 
58
- it("proxies when the response says to use a proxy", async () => {
58
+ it("proxies when a proxyURL is passed in the options", async () => {
59
59
  const registry = new Registry();
60
60
 
61
61
  registry.add("/proxy", {
@@ -67,7 +67,7 @@ describe("koa middleware", () => {
67
67
  const dispatcher = new Dispatcher(registry, new ContextRegistry());
68
68
  const middleware = koaMiddleware(
69
69
  dispatcher,
70
- { proxyUrl: "https://example.com" },
70
+ { proxyUrl: "https://example.com", proxyEnabled: true },
71
71
  mockKoaProxy
72
72
  );
73
73
  const ctx = {