Skip to content

Commit

Permalink
Merge pull request #913 from pmcelhaney/per-endpoint-proxy
Browse files Browse the repository at this point in the history
per endpoint proxy
  • Loading branch information
pmcelhaney authored May 23, 2024
2 parents 3250f70 + ce4ddfe commit 6ddbc69
Show file tree
Hide file tree
Showing 19 changed files with 1,627 additions and 5,543 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-ravens-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"counterfact": minor
---

turn the proxy on or off for individual paths
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x
cache: yarn
id: setup-node
- name: Get Node Version
Expand Down
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[tools]
node = "20"
node = "22"
6 changes: 3 additions & 3 deletions bin/counterfact.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { program } from "commander";
import createDebug from "debug";
import open from "open";

import { counterfact } from "../dist/server/app.js";
import { counterfact } from "../dist/app.js";

const MIN_NODE_VERSION = 17;

Expand Down Expand Up @@ -151,8 +151,8 @@ async function main(source, destination) {
includeSwaggerUi: true,
openApiPath: source,
port: options.port,
proxyEnabled: Boolean(options.proxyUrl),
proxyUrl: options.proxyUrl,
proxyPaths: new Map([["", Boolean(options.proxyUrl)]]),
proxyUrl: options.proxyUrl ?? "",
routePrefix: options.prefix,
startRepl: options.repl,
startServer: options.serve,
Expand Down
6 changes: 2 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,9 @@ To proxy an individual endpoint, you can use the `$.proxy()` function.
};
```

To toggle globally between Counterfact and a proxy server, pass `--proxy-url <url>` in the CLI.
To set up a proxy for the entire API, add `--proxy <url>` in the CLI.

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.

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!
From there, you can switch back and forth between the proxy and mocks by typing `.proxy [on|off] <path-prefix>`. Type `.proxy help` for detailed information on using the `.proxy` command.

## No Cap Recap 🧢

Expand Down
22 changes: 11 additions & 11 deletions src/server/app.ts → src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { createHttpTerminator, type HttpTerminator } from "http-terminator";
import yaml from "js-yaml";
import $RefParser from "json-schema-ref-parser";

import { CodeGenerator } from "../typescript-generator/code-generator.js";
import { readFile } from "../util/read-file.js";
import type { Config } from "./config.js";
import { ContextRegistry } from "./context-registry.js";
import { createKoaApp } from "./create-koa-app.js";
import { Dispatcher, type OpenApiDocument } from "./dispatcher.js";
import { koaMiddleware } from "./koa-middleware.js";
import { ModuleLoader } from "./module-loader.js";
import { Registry } from "./registry.js";
import { startRepl } from "./repl.js";
import { Transpiler } from "./transpiler.js";
import { startRepl } from "./repl/repl.js";
import type { Config } from "./server/config.js";
import { ContextRegistry } from "./server/context-registry.js";
import { createKoaApp } from "./server/create-koa-app.js";
import { Dispatcher, type OpenApiDocument } from "./server/dispatcher.js";
import { koaMiddleware } from "./server/koa-middleware.js";
import { ModuleLoader } from "./server/module-loader.js";
import { Registry } from "./server/registry.js";
import { Transpiler } from "./server/transpiler.js";
import { CodeGenerator } from "./typescript-generator/code-generator.js";
import { readFile } from "./util/read-file.js";

async function loadOpenApiDocument(source: string) {
try {
Expand Down
130 changes: 130 additions & 0 deletions src/repl/repl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import repl from "node:repl";

import type { Config } from "../server/config.js";
import type { ContextRegistry } from "../server/context-registry.js";

function printToStdout(line: string) {
process.stdout.write(`${line}\n`);
}

export function startRepl(
contextRegistry: ContextRegistry,
config: Config,
print = printToStdout,
) {
// eslint-disable-next-line max-statements
function printProxyStatus() {
if (config.proxyUrl === "") {
print("The proxy URL is not set.");
print('To set it, type ".proxy url <url>');
return;
}

print("Proxy Configuration:");
print("");
print(`The proxy URL is ${config.proxyUrl}`);
print("");
print("Paths prefixed with [+] will be proxied.");
print("Paths prefixed with [-] will not be proxied.");
print("");

// eslint-disable-next-line array-func/prefer-array-from
const entries = [...config.proxyPaths.entries()].sort(([path1], [path2]) =>
path1 < path2 ? -1 : 1,
);

for (const [path, state] of entries) {
print(`${state ? "[+]" : "[-]"} ${path}/`);
}
}

function setProxyUrl(url: string | undefined) {
if (url === undefined) {
print("usage: .proxy url <url>");
return;
}

config.proxyUrl = url;
print(`proxy URL is set to ${url}`);
}

function turnProxyOnOrOff(text: string) {
const [command, endpoint] = text.split(" ");

const printEndpoint =
endpoint === undefined || endpoint === "" ? "/" : endpoint;

config.proxyPaths.set(
(endpoint ?? "").replace(/\/$/u, ""),
command === "on",
);

if (command === "on") {
print(
`Requests to ${printEndpoint} will be proxied to ${
config.proxyUrl || "<proxy URL>"
}${printEndpoint}`,
);
}

if (command === "off") {
print(`Requests to ${printEndpoint} will be handled by local code`);
}
}

const replServer = repl.start({ prompt: "🤖> " });

replServer.defineCommand("counterfact", {
action() {
print(
"This is a read-eval-print loop (REPL), the same as the one you get when you run node with no arguments.",
);
print(
"Except that it's connected to the running server, which you can access with the following globals:",
);
print("");
print(
"- loadContext('/some/path'): to access the context object for a given path",
);
print("- context: the root context ( same as loadContext('/') )");
print("");
print(
"For more information, see https://counterfact.dev/docs/usage.html",
);
print("");

this.clearBufferedCommand();
this.displayPrompt();
},

help: "Get help with Counterfact",
});

replServer.defineCommand("proxy", {
action(text) {
if (text === "help" || text === "") {
print(".proxy [on|off] - turn the proxy on/off at the root level");
print(".proxy [on|off] <path-prefix> - turn the proxy on for a path");
print(".proxy status - show the proxy status");
print(".proxy help - show this message");
} else if (text.startsWith("url")) {
setProxyUrl(text.split(" ")[1]);
} else if (text === "status") {
printProxyStatus();
} else {
turnProxyOnOrOff(text);
}

this.clearBufferedCommand();
this.displayPrompt();
},

help: 'proxy configuration (".proxy help" for details)',
});

replServer.context.loadContext = (path: string) => contextRegistry.find(path);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
replServer.context.context = replServer.context.loadContext("/");

return replServer;
}
2 changes: 1 addition & 1 deletion src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Config {
};
openApiPath: string;
port: number;
proxyEnabled: boolean;
proxyPaths: Map<string, boolean>;
proxyUrl: string;
routePrefix: string;
startRepl: boolean;
Expand Down
20 changes: 20 additions & 0 deletions src/server/is-proxy-enabled-for-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
interface ProxyConfig {
proxyPaths: Map<string, boolean>;
}

export function isProxyEnabledForPath(
path: string,
config: ProxyConfig,
): boolean {
if (config.proxyPaths.has(path)) {
return config.proxyPaths.get(path) ?? false;
}

if (path === "") {
return false;
}

const parentPath = path.slice(0, Math.max(0, path.lastIndexOf("/")));

return isProxyEnabledForPath(parentPath, config);
}
5 changes: 3 additions & 2 deletions src/server/koa-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import koaProxy from "koa-proxy";

import type { Config } from "./config.js";
import type { Dispatcher } from "./dispatcher.js";
import { isProxyEnabledForPath } from "./is-proxy-enabled-for-path.js";
import type { HttpMethods } from "./registry.js";

const debug = createDebug("counterfact:server:create-koa-app");
Expand Down Expand Up @@ -59,7 +60,7 @@ export function koaMiddleware(
): Koa.Middleware {
// eslint-disable-next-line max-statements
return async function middleware(ctx, next) {
const { proxyEnabled, proxyUrl, routePrefix } = config;
const { proxyUrl, routePrefix } = config;

debug("middleware running for path: %s", ctx.request.path);
debug("routePrefix: %s", routePrefix);
Expand All @@ -79,7 +80,7 @@ export function koaMiddleware(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const method = ctx.request.method as HttpMethods;

if (proxyEnabled && proxyUrl) {
if (isProxyEnabledForPath(path, config) && proxyUrl) {
/* @ts-expect-error the body comes from koa-bodyparser, not sure how to fix this */
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return proxy({ host: proxyUrl })(ctx, next);
Expand Down
8 changes: 4 additions & 4 deletions src/server/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ type HttpMethods =
| "TRACE";

interface RequestData {
auth?: {
password?: string;
username?: string;
};
context: unknown;
headers: { [key: string]: number | string };
matchedPath?: string;
Expand All @@ -30,10 +34,6 @@ interface RequestData {
query: { [key: string]: number | string };
response: ResponseBuilderFactory;
tools: Tools;
user?: {
password?: string;
username?: string;
};
}

interface RequestDataWithBody extends RequestData {
Expand Down
60 changes: 0 additions & 60 deletions src/server/repl.ts

This file was deleted.

8 changes: 4 additions & 4 deletions src/util/wait-for-event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EventEmitter } from "koa";
import { EventEmitter } from "node:events";

/**
* Creates a promise that resolves when a specified event is fired on the given EventTarget.
Expand All @@ -19,10 +19,10 @@ export async function waitForEvent(
resolve(event);
};

if (target instanceof EventTarget) {
target.addEventListener(eventName, handler);
} else {
if (target instanceof EventEmitter) {
target.once(eventName, handler);
} else {
target.addEventListener(eventName, handler);
}
});
}
Loading

0 comments on commit 6ddbc69

Please sign in to comment.