counterfact 2.9.0 → 2.11.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/README.md +36 -13
- package/dist/api-runner.js +19 -7
- package/dist/app.js +119 -15
- package/dist/cli/banner.js +1 -1
- package/dist/cli/run.js +10 -4
- package/dist/cli/telemetry.js +1 -6
- package/dist/migrate/update-route-types.js +1 -1
- package/dist/repl/repl.js +1 -4
- package/dist/server/counterfact-types/generic-response-builder.ts +1 -2
- package/dist/server/counterfact-types/open-api-parameters.ts +3 -0
- package/dist/server/dispatcher.js +121 -14
- package/dist/server/request-validator.js +42 -1
- package/dist/server/web-server/admin-api-middleware.js +1 -1
- package/dist/typescript-generator/code-generator.js +4 -2
- package/dist/typescript-generator/coder.js +4 -2
- package/dist/typescript-generator/operation-coder.js +13 -5
- package/dist/typescript-generator/operation-type-coder.js +175 -21
- package/dist/typescript-generator/parameter-export-type-coder.js +2 -2
- package/dist/typescript-generator/parameters-type-coder.js +3 -3
- package/dist/typescript-generator/requirement.js +12 -1
- package/dist/typescript-generator/response-type-coder.js +7 -6
- package/dist/typescript-generator/responses-type-coder.js +11 -5
- package/dist/typescript-generator/schema-coder.js +2 -2
- package/dist/typescript-generator/schema-type-coder.js +7 -5
- package/dist/typescript-generator/script.js +99 -3
- package/dist/typescript-generator/versions-ts-generator.js +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,34 +1,57 @@
|
|
|
1
1
|
<div align="center" markdown="1">
|
|
2
2
|
|
|
3
|
-
<img src="./counterfact.svg" alt="Counterfact" border=0>
|
|
3
|
+
<h1><img src="./counterfact.svg" alt="Counterfact" border=0></h1>
|
|
4
4
|
|
|
5
5
|
<br>
|
|
6
6
|
|
|
7
|
-
 [ [](https://coveralls.io/github/pmcelhaney/counterfact) 
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
<div align="center" markdown="1">
|
|
12
|
+
<h2>Mock servers work—until you need state, failures, or control mid-run.</h2>
|
|
13
|
+
</div
|
|
14
|
+
|
|
15
|
+
Static responses aren’t enough. There’s no shared state. You can’t inject failures. You can’t test real workflows.<br>
|
|
16
|
+
Mock servers make it easy to get started, but hard to keep going.<br>
|
|
17
|
+
Counterfact is an API simulator without those limits.
|
|
18
|
+
|
|
19
|
+
Point it at an [OpenAPI](https://www.openapis.org) document and get a live, stateful API in seconds.
|
|
20
|
+
- Type-safe TypeScript handlers for every endpoint
|
|
21
|
+
- Hot reloading as you edit
|
|
22
|
+
- Shared state across routes
|
|
23
|
+
- **A built-in REPL to control behavior at runtime**
|
|
24
|
+
- Optional proxying to real backends
|
|
25
|
+
|
|
26
|
+
Flexbile for humans. Stable for [AI agents](https://github.com/counterfact/api-simulator/blob/main/docs/patterns/ai-assisted-implementation.md).
|
|
27
|
+
|
|
28
|
+
You’re in control—without restarting.
|
|
29
|
+
|
|
30
|
+
For a *frontend developer* waiting on a backend,<br>
|
|
31
|
+
a *test engineer* who needs clean, reproducible state,<br>
|
|
32
|
+
or an *AI agent* that needs a stable API
|
|
33
|
+
|
|
34
|
+
Real enough to be useful. Fake enough to be usable.
|
|
35
|
+
|
|
12
36
|
|
|
13
|
-
|
|
37
|
+
## Try it now
|
|
14
38
|
|
|
15
39
|
```sh
|
|
16
40
|
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
|
|
17
41
|
```
|
|
18
42
|
|
|
43
|
+
> Starts a local server with a live REPL to inspect and control API behavior
|
|
19
44
|
> Requires Node ≥ 22.0.0
|
|
20
45
|
|
|
21
46
|
## Go deeper
|
|
22
47
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
| [FAQ](./docs/faq.md) | State, types, regeneration |
|
|
31
|
-
| [Petstore example](https://github.com/counterfact/example-petstore) | Full working example |
|
|
48
|
+
- [Getting started](./docs/getting-started.md) – Detailed walkthrough with state, REPL, and proxy
|
|
49
|
+
- [Patterns](./docs/patterns/index.md) – How Counterfact transforms your workflow
|
|
50
|
+
- [Example repo](https://github.com/counterfact/example-petstore) – Using Counterfact to implement the Swagger Petstore
|
|
51
|
+
- [How it compares](./docs/comparison.md) – json-server, WireMock, Prism, Microcks, MSW
|
|
52
|
+
- [Usage](./docs/usage.md) – Explore features and how to use them
|
|
53
|
+
- [Reference](./docs/reference.md) – `$` API, CLI flags, architecture
|
|
54
|
+
- [FAQ](./docs/faq.md) – State, types, regeneration
|
|
32
55
|
|
|
33
56
|
<div align="center" markdown="1">
|
|
34
57
|
|
package/dist/api-runner.js
CHANGED
|
@@ -64,6 +64,11 @@ export class ApiRunner {
|
|
|
64
64
|
* Defaults to `""` (no subdirectory).
|
|
65
65
|
*/
|
|
66
66
|
group;
|
|
67
|
+
/**
|
|
68
|
+
* Optional version label for this runner's spec (e.g. `"v1"`, `"v2"`).
|
|
69
|
+
* Defaults to `""` (unversioned).
|
|
70
|
+
*/
|
|
71
|
+
version;
|
|
67
72
|
/**
|
|
68
73
|
* The subdirectory path segment derived from {@link group}.
|
|
69
74
|
* Returns `""` when `group` is empty, otherwise `"/${group}"`.
|
|
@@ -72,8 +77,9 @@ export class ApiRunner {
|
|
|
72
77
|
return this.group ? `/${this.group}` : "";
|
|
73
78
|
}
|
|
74
79
|
config;
|
|
75
|
-
constructor(config, nativeTs, openApiDocument, group) {
|
|
80
|
+
constructor(config, nativeTs, openApiDocument, group, version = "", versions = []) {
|
|
76
81
|
this.group = group;
|
|
82
|
+
this.version = version;
|
|
77
83
|
const modulesPath = this.group
|
|
78
84
|
? pathJoin(config.basePath, this.group)
|
|
79
85
|
: config.basePath;
|
|
@@ -87,8 +93,8 @@ export class ApiRunner {
|
|
|
87
93
|
this.contextRegistry = new ContextRegistry();
|
|
88
94
|
this.scenarioRegistry = new ScenarioRegistry();
|
|
89
95
|
this.scenarioFileGenerator = new ScenarioFileGenerator(modulesPath);
|
|
90
|
-
this.codeGenerator = new CodeGenerator(this.openApiPath, config.basePath + this.subdirectory, config.generate);
|
|
91
|
-
this.dispatcher = new Dispatcher(this.registry, this.contextRegistry, openApiDocument, config);
|
|
96
|
+
this.codeGenerator = new CodeGenerator(this.openApiPath, config.basePath + this.subdirectory, config.generate, version);
|
|
97
|
+
this.dispatcher = new Dispatcher(this.registry, this.contextRegistry, openApiDocument, config, version, versions);
|
|
92
98
|
this.transpiler = new Transpiler(pathJoin(modulesPath, "routes"), compiledPathsDirectory, "commonjs");
|
|
93
99
|
this.moduleLoader = new ModuleLoader(compiledPathsDirectory, this.registry, this.contextRegistry, pathJoin(modulesPath, "scenarios"), this.scenarioRegistry);
|
|
94
100
|
}
|
|
@@ -101,8 +107,10 @@ export class ApiRunner {
|
|
|
101
107
|
*
|
|
102
108
|
* @param config - Runtime configuration for this runner instance.
|
|
103
109
|
* @param group - Optional group name placing generated code in a subdirectory (default `""`).
|
|
110
|
+
* @param version - Optional version label for this spec (e.g. `"v1"`, `"v2"`).
|
|
111
|
+
* @param versions - Optional ordered list of all version labels in this group (oldest first).
|
|
104
112
|
*/
|
|
105
|
-
static async create(config, group = "") {
|
|
113
|
+
static async create(config, group = "", version = "", versions = []) {
|
|
106
114
|
const nativeTs = await runtimeCanExecuteErasableTs();
|
|
107
115
|
const modulesPath = group
|
|
108
116
|
? pathJoin(config.basePath, group)
|
|
@@ -114,7 +122,7 @@ export class ApiRunner {
|
|
|
114
122
|
const openApiDocument = config.openApiPath === "_"
|
|
115
123
|
? undefined
|
|
116
124
|
: await loadOpenApiDocument(config.openApiPath);
|
|
117
|
-
return new ApiRunner(config, nativeTs, openApiDocument, group);
|
|
125
|
+
return new ApiRunner(config, nativeTs, openApiDocument, group, version, versions);
|
|
118
126
|
}
|
|
119
127
|
/**
|
|
120
128
|
* Generates TypeScript route stubs and type files from the OpenAPI spec.
|
|
@@ -123,11 +131,15 @@ export class ApiRunner {
|
|
|
123
131
|
* - Routes and types are only generated when `config.openApiPath` is not `"_"`.
|
|
124
132
|
* - The scenario context type file is always generated when
|
|
125
133
|
* `config.generate.types` is `true`, even without a spec.
|
|
134
|
+
*
|
|
135
|
+
* @param repository - Optional shared repository. Pass a shared instance
|
|
136
|
+
* when multiple versioned specs in the same group should merge their types
|
|
137
|
+
* into the same output tree.
|
|
126
138
|
*/
|
|
127
|
-
async generate() {
|
|
139
|
+
async generate(repository) {
|
|
128
140
|
if (this.config.openApiPath !== "_" &&
|
|
129
141
|
(this.config.generate.routes || this.config.generate.types)) {
|
|
130
|
-
await this.codeGenerator.generate();
|
|
142
|
+
await this.codeGenerator.generate(repository);
|
|
131
143
|
}
|
|
132
144
|
if (this.config.generate.types) {
|
|
133
145
|
await this.scenarioFileGenerator.generate();
|
package/dist/app.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import nodePath from "node:path";
|
|
1
3
|
import { createHttpTerminator } from "http-terminator";
|
|
2
4
|
import { ApiRunner } from "./api-runner.js";
|
|
3
5
|
import { startRepl as startReplServer } from "./repl/repl.js";
|
|
4
6
|
import { createRouteFunction } from "./repl/route-builder.js";
|
|
5
7
|
import { createKoaApp } from "./server/web-server/create-koa-app.js";
|
|
8
|
+
import { Repository } from "./typescript-generator/repository.js";
|
|
9
|
+
import { ensureDirectoryExists } from "./util/ensure-directory-exists.js";
|
|
10
|
+
import { generateVersionsTsContent } from "./typescript-generator/versions-ts-generator.js";
|
|
6
11
|
export { loadOpenApiDocument } from "./server/load-openapi-document.js";
|
|
7
12
|
export { createMswHandlers, handleMswRequest, } from "./msw.js";
|
|
8
13
|
export async function runStartupScenario(scenarioRegistry, contextRegistry, config, openApiDocument) {
|
|
@@ -18,19 +23,47 @@ export async function runStartupScenario(scenarioRegistry, contextRegistry, conf
|
|
|
18
23
|
};
|
|
19
24
|
await indexModule["startup"](scenario$);
|
|
20
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Derives the URL prefix for a spec entry.
|
|
28
|
+
*
|
|
29
|
+
* Applies the following precedence rules:
|
|
30
|
+
* 1. Explicit `prefix` (even `""`) → returned as-is.
|
|
31
|
+
* 2. `group` + `version` both present → `/<group>/<version>`.
|
|
32
|
+
* 3. `group` present (no `version`) → `/<group>`.
|
|
33
|
+
* 4. Neither → `""` (root).
|
|
34
|
+
*/
|
|
35
|
+
function derivePrefix(spec) {
|
|
36
|
+
if (spec.prefix !== undefined) {
|
|
37
|
+
return spec.prefix;
|
|
38
|
+
}
|
|
39
|
+
if (spec.group && spec.version) {
|
|
40
|
+
return `/${spec.group}/${spec.version}`;
|
|
41
|
+
}
|
|
42
|
+
if (spec.group) {
|
|
43
|
+
return `/${spec.group}`;
|
|
44
|
+
}
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
21
47
|
/**
|
|
22
48
|
* Normalises the spec configuration to an array.
|
|
23
49
|
*
|
|
24
|
-
* When `specs` is provided
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
50
|
+
* When `specs` is provided, each entry's `prefix` is resolved via
|
|
51
|
+
* {@link derivePrefix} so the rest of the code can assume `prefix` is always
|
|
52
|
+
* a string. When `specs` is omitted, a single-entry array is constructed from
|
|
53
|
+
* `config.openApiPath`, `config.prefix`, and `group = ""`.
|
|
28
54
|
*/
|
|
29
55
|
function normalizeSpecs(config, specs) {
|
|
30
56
|
if (specs !== undefined) {
|
|
31
|
-
return specs;
|
|
57
|
+
return specs.map((spec) => ({ ...spec, prefix: derivePrefix(spec) }));
|
|
32
58
|
}
|
|
33
|
-
return [
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
source: config.openApiPath,
|
|
62
|
+
prefix: config.prefix,
|
|
63
|
+
group: "",
|
|
64
|
+
version: "",
|
|
65
|
+
},
|
|
66
|
+
];
|
|
34
67
|
}
|
|
35
68
|
function validateSpecGroups(specs) {
|
|
36
69
|
if (specs.length <= 1) {
|
|
@@ -41,20 +74,26 @@ function validateSpecGroups(specs) {
|
|
|
41
74
|
.filter(({ group }) => group === "")
|
|
42
75
|
.map(({ index }) => String(index + 1));
|
|
43
76
|
if (invalidSpecNumbers.length === 0) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
77
|
+
const seenKeys = new Set();
|
|
78
|
+
const duplicateKeys = new Set();
|
|
46
79
|
for (const spec of specs) {
|
|
47
80
|
const group = spec.group.trim();
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
const version = spec.version?.trim() ?? "";
|
|
82
|
+
// Use group@version as the uniqueness key so that the same group can
|
|
83
|
+
// appear with different versions (e.g. v1 and v2 of the same API).
|
|
84
|
+
// The empty-group case is already rejected above, so `group` is always
|
|
85
|
+
// non-empty here and the `@version` suffix remains unambiguous.
|
|
86
|
+
const key = version ? `${group}@${version}` : group;
|
|
87
|
+
if (seenKeys.has(key)) {
|
|
88
|
+
duplicateKeys.add(key);
|
|
50
89
|
continue;
|
|
51
90
|
}
|
|
52
|
-
|
|
91
|
+
seenKeys.add(key);
|
|
53
92
|
}
|
|
54
|
-
if (
|
|
93
|
+
if (duplicateKeys.size === 0) {
|
|
55
94
|
return;
|
|
56
95
|
}
|
|
57
|
-
throw new Error(`Each spec must define a unique group when multiple APIs are configured (
|
|
96
|
+
throw new Error(`Each spec must define a unique group (and version) when multiple APIs are configured (duplicates: ${[...duplicateKeys].join(", ")}).`);
|
|
58
97
|
}
|
|
59
98
|
throw new Error(`Each spec must define a non-empty group when multiple APIs are configured (invalid spec entries: ${invalidSpecNumbers.join(", ")}).`);
|
|
60
99
|
}
|
|
@@ -81,7 +120,18 @@ function validateSpecGroups(specs) {
|
|
|
81
120
|
export async function counterfact(config, specs) {
|
|
82
121
|
const normalizedSpecs = normalizeSpecs({ openApiPath: config.openApiPath, prefix: config.prefix }, specs);
|
|
83
122
|
validateSpecGroups(normalizedSpecs);
|
|
84
|
-
|
|
123
|
+
// Compute the ordered versions per group (oldest first, as declared in specs).
|
|
124
|
+
// This list is passed to each runner so that $.minVersion() can compare
|
|
125
|
+
// version positions at runtime.
|
|
126
|
+
const versionsByGroup = new Map();
|
|
127
|
+
for (const spec of normalizedSpecs) {
|
|
128
|
+
const version = spec.version ?? "";
|
|
129
|
+
if (version) {
|
|
130
|
+
const existing = versionsByGroup.get(spec.group) ?? [];
|
|
131
|
+
versionsByGroup.set(spec.group, [...existing, version]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const runners = await Promise.all(normalizedSpecs.map((spec) => ApiRunner.create({ ...config, openApiPath: spec.source, prefix: spec.prefix }, spec.group, spec.version ?? "", versionsByGroup.get(spec.group) ?? [])));
|
|
85
135
|
const koaApp = createKoaApp({
|
|
86
136
|
runners,
|
|
87
137
|
config,
|
|
@@ -89,7 +139,61 @@ export async function counterfact(config, specs) {
|
|
|
89
139
|
// The REPL is configured using the first runner.
|
|
90
140
|
const primaryRunner = runners[0];
|
|
91
141
|
async function start(options) {
|
|
92
|
-
|
|
142
|
+
// Serialize generate() calls within each group to avoid concurrent writes
|
|
143
|
+
// to the same output directory. Runners that share a group share the same
|
|
144
|
+
// basePath subdirectory (and therefore the same counterfact-types
|
|
145
|
+
// destination), so running them in parallel would cause a race when both
|
|
146
|
+
// try to create that directory at startup. Different groups are still
|
|
147
|
+
// generated in parallel.
|
|
148
|
+
//
|
|
149
|
+
// When multiple versioned specs share the same group, they also share a
|
|
150
|
+
// single Repository instance so that the shared `types/paths/…` files
|
|
151
|
+
// accumulate all versions into a merged Versioned<…> type instead of each
|
|
152
|
+
// overwriting the previous version's types.
|
|
153
|
+
const runnersByGroup = new Map();
|
|
154
|
+
for (const runner of runners) {
|
|
155
|
+
const bucket = runnersByGroup.get(runner.group) ?? [];
|
|
156
|
+
bucket.push(runner);
|
|
157
|
+
runnersByGroup.set(runner.group, bucket);
|
|
158
|
+
}
|
|
159
|
+
await Promise.all(Array.from(runnersByGroup.values()).map(async (bucket) => {
|
|
160
|
+
const sharedRepository = bucket.length > 1 ? new Repository() : undefined;
|
|
161
|
+
for (const runner of bucket) {
|
|
162
|
+
await runner.generate(sharedRepository);
|
|
163
|
+
}
|
|
164
|
+
}));
|
|
165
|
+
if (options.generate?.types) {
|
|
166
|
+
// Build a per-group map of unique non-empty version strings in
|
|
167
|
+
// declaration order. new Set() preserves insertion order so the first
|
|
168
|
+
// occurrence of each version is kept and duplicates are dropped without
|
|
169
|
+
// reordering.
|
|
170
|
+
const versionsByGroup = new Map();
|
|
171
|
+
for (const spec of normalizedSpecs) {
|
|
172
|
+
const group = spec.group;
|
|
173
|
+
const version = (spec.version ?? "").trim();
|
|
174
|
+
if (version === "") {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const existing = versionsByGroup.get(group) ?? [];
|
|
178
|
+
if (!existing.includes(version)) {
|
|
179
|
+
existing.push(version);
|
|
180
|
+
}
|
|
181
|
+
versionsByGroup.set(group, existing);
|
|
182
|
+
}
|
|
183
|
+
// Write <basePath>/<group>/types/versions.ts for every group that has
|
|
184
|
+
// at least one versioned spec. When the group is empty the path
|
|
185
|
+
// collapses to <basePath>/types/versions.ts (the single-spec case).
|
|
186
|
+
await Promise.all(Array.from(versionsByGroup.entries()).map(async ([group, versions]) => {
|
|
187
|
+
const content = await generateVersionsTsContent(versions);
|
|
188
|
+
const versionsFilePath = group
|
|
189
|
+
? nodePath.join(config.basePath, group, "types", "versions.ts")
|
|
190
|
+
: nodePath.join(config.basePath, "types", "versions.ts");
|
|
191
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- path is derived from the caller-supplied basePath and fixed suffixes. */
|
|
192
|
+
await ensureDirectoryExists(versionsFilePath);
|
|
193
|
+
await fs.writeFile(versionsFilePath, content, "utf8");
|
|
194
|
+
/* eslint-enable security/detect-non-literal-fs-filename */
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
93
197
|
await Promise.all(runners.map((runner) => runner.watch()));
|
|
94
198
|
await Promise.all(runners.map((runner) => runner.start(options)));
|
|
95
199
|
let httpTerminator;
|
package/dist/cli/banner.js
CHANGED
|
@@ -67,7 +67,7 @@ export function createIntroduction(params) {
|
|
|
67
67
|
` API Base URL ${url}`,
|
|
68
68
|
source === "_" ? undefined : ` Swagger UI ${swaggerUrl}`,
|
|
69
69
|
"",
|
|
70
|
-
" Instructions https://
|
|
70
|
+
" Instructions https://github.com/counterfact/api-simulator/blob/main/docs/usage.md",
|
|
71
71
|
" Help/feedback https://github.com/pmcelhaney/counterfact/issues",
|
|
72
72
|
"",
|
|
73
73
|
...telemetryWarning,
|
package/dist/cli/run.js
CHANGED
|
@@ -22,18 +22,23 @@ const DEFAULT_PORT = 3100;
|
|
|
22
22
|
* CLI flag) into an array of {@link SpecConfig} objects, or `undefined` when
|
|
23
23
|
* the option is a plain string (single OpenAPI document path).
|
|
24
24
|
*
|
|
25
|
-
* - **Array**: each entry is mapped to `{source, prefix, group}` with defaults.
|
|
25
|
+
* - **Array**: each entry is mapped to `{source, prefix, group, version}` with defaults.
|
|
26
26
|
* - **Object**: wrapped in a single-element array.
|
|
27
27
|
* - **String / undefined**: returns `undefined` — caller handles the string
|
|
28
28
|
* case (it shifts the positional argument) and the `undefined` case
|
|
29
29
|
* (single spec derived from config).
|
|
30
|
+
*
|
|
31
|
+
* Note: `prefix` is intentionally left `undefined` when not supplied so that
|
|
32
|
+
* `normalizeSpecs` (in `app.ts`) can derive it automatically from
|
|
33
|
+
* `group`/`version`.
|
|
30
34
|
*/
|
|
31
35
|
export function normalizeSpecOption(specOption) {
|
|
32
36
|
if (Array.isArray(specOption)) {
|
|
33
37
|
return specOption.map((entry) => ({
|
|
34
38
|
source: entry.source,
|
|
35
|
-
prefix: entry.prefix
|
|
39
|
+
prefix: entry.prefix,
|
|
36
40
|
group: entry.group ?? "",
|
|
41
|
+
version: entry.version,
|
|
37
42
|
}));
|
|
38
43
|
}
|
|
39
44
|
if (typeof specOption === "object" &&
|
|
@@ -42,8 +47,9 @@ export function normalizeSpecOption(specOption) {
|
|
|
42
47
|
return [
|
|
43
48
|
{
|
|
44
49
|
source: specOption.source,
|
|
45
|
-
prefix: specOption.prefix
|
|
50
|
+
prefix: specOption.prefix,
|
|
46
51
|
group: specOption.group ?? "",
|
|
52
|
+
version: specOption.version,
|
|
47
53
|
},
|
|
48
54
|
];
|
|
49
55
|
}
|
|
@@ -253,7 +259,7 @@ function buildProgram(version, taglines) {
|
|
|
253
259
|
.option("--watch-routes", "generate + watch routes for changes")
|
|
254
260
|
.option("-s, --serve", "start the server")
|
|
255
261
|
.option("-b, --build-cache", "builds the cache of compiled routes and types")
|
|
256
|
-
.option("--
|
|
262
|
+
.option("--admin-api", "enable the admin API at /_counterfact/api/*")
|
|
257
263
|
.option("-r, --repl", "start the REPL")
|
|
258
264
|
.option("--proxy-url <string>", "proxy URL")
|
|
259
265
|
.option("--admin-api-token <string>", "bearer token required for /_counterfact/api/* endpoints (defaults to COUNTERFACT_ADMIN_API_TOKEN)")
|
package/dist/cli/telemetry.js
CHANGED
|
@@ -5,9 +5,7 @@ const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
|
5
5
|
/**
|
|
6
6
|
* Returns `true` when telemetry should be sent.
|
|
7
7
|
*
|
|
8
|
-
* Telemetry is disabled in CI
|
|
9
|
-
* or before the May 2026 rollout date unless the user has explicitly opted
|
|
10
|
-
* in with `COUNTERFACT_TELEMETRY_DISABLED=false`.
|
|
8
|
+
* Telemetry is disabled in CI or when `COUNTERFACT_TELEMETRY_DISABLED=true`.
|
|
11
9
|
*/
|
|
12
10
|
export function isTelemetryEnabled() {
|
|
13
11
|
if (process.env["CI"])
|
|
@@ -15,9 +13,6 @@ export function isTelemetryEnabled() {
|
|
|
15
13
|
const telemetryDisabledEnv = process.env["COUNTERFACT_TELEMETRY_DISABLED"];
|
|
16
14
|
if (telemetryDisabledEnv === "true")
|
|
17
15
|
return false;
|
|
18
|
-
const isBeforeRollout = new Date() < new Date("2026-05-01");
|
|
19
|
-
if (isBeforeRollout && telemetryDisabledEnv !== "false")
|
|
20
|
-
return false;
|
|
21
16
|
return true;
|
|
22
17
|
}
|
|
23
18
|
/**
|
|
@@ -67,7 +67,7 @@ async function buildTypeNameMapping(specification) {
|
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
// Create the type coder to get the correct type name
|
|
70
|
-
const typeCoder = new OperationTypeCoder(operation, requestMethod, securitySchemes);
|
|
70
|
+
const typeCoder = new OperationTypeCoder(operation, "", requestMethod, securitySchemes);
|
|
71
71
|
// Get the type name (first from the names generator)
|
|
72
72
|
const typeName = typeCoder.names().next().value;
|
|
73
73
|
methodMap.set(requestMethod.toUpperCase(), typeName);
|
package/dist/repl/repl.js
CHANGED
|
@@ -194,9 +194,6 @@ export function startRepl(contextRegistry, registry, config, print = printToStdo
|
|
|
194
194
|
}
|
|
195
195
|
seenGroups.add(binding.key);
|
|
196
196
|
}
|
|
197
|
-
if (duplicateGroups.size > 0) {
|
|
198
|
-
throw new Error(`Duplicate API groups are not allowed when multiple APIs are configured (duplicate groups: ${[...duplicateGroups].join(", ")}).`);
|
|
199
|
-
}
|
|
200
197
|
}
|
|
201
198
|
const rootBinding = groupedBindings[0];
|
|
202
199
|
if (rootBinding === undefined) {
|
|
@@ -268,7 +265,7 @@ export function startRepl(contextRegistry, registry, config, print = printToStdo
|
|
|
268
265
|
print("- context: the root context ( same as loadContext('/') )");
|
|
269
266
|
print("- route('/some/path'): create a request builder for the given path");
|
|
270
267
|
print("");
|
|
271
|
-
print("For more information, see https://
|
|
268
|
+
print("For more information, see https://github.com/counterfact/api-simulator/blob/main/docs/usage.md");
|
|
272
269
|
print("");
|
|
273
270
|
this.clearBufferedCommand();
|
|
274
271
|
this.displayPrompt();
|
|
@@ -157,8 +157,7 @@ export type GenericResponseBuilder<
|
|
|
157
157
|
: object extends OmitValueWhenNever<Omit<Response, "examples">>
|
|
158
158
|
? COUNTERFACT_RESPONSE
|
|
159
159
|
: keyof OmitValueWhenNever<Omit<Response, "examples">> extends "headers"
|
|
160
|
-
? {
|
|
161
|
-
ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE;
|
|
160
|
+
? COUNTERFACT_RESPONSE & {
|
|
162
161
|
header: HeaderFunction<Response>;
|
|
163
162
|
}
|
|
164
163
|
: GenericResponseBuilderInner<Response>;
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* argument object.
|
|
6
6
|
*/
|
|
7
7
|
export interface OpenApiParameters {
|
|
8
|
+
explode?: boolean;
|
|
8
9
|
in: "body" | "cookie" | "formData" | "header" | "path" | "query";
|
|
9
10
|
name: string;
|
|
10
11
|
required?: boolean;
|
|
11
12
|
schema?: {
|
|
12
13
|
[key: string]: unknown;
|
|
14
|
+
properties?: Record<string, unknown>;
|
|
13
15
|
type?: string;
|
|
14
16
|
};
|
|
17
|
+
style?: string;
|
|
15
18
|
type?: "string" | "number" | "integer" | "boolean";
|
|
16
19
|
}
|