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
|
@@ -10,6 +10,15 @@ export class Requirement {
|
|
|
10
10
|
data;
|
|
11
11
|
url;
|
|
12
12
|
specification;
|
|
13
|
+
/**
|
|
14
|
+
* The requirement that produced this one via a `get()` call, or `undefined`
|
|
15
|
+
* for root requirements that were constructed directly.
|
|
16
|
+
*
|
|
17
|
+
* For path-traversal purposes this is the "logical" parent: when a `$ref` is
|
|
18
|
+
* followed, the parent is the resolved reference target rather than the
|
|
19
|
+
* `$ref` node itself.
|
|
20
|
+
*/
|
|
21
|
+
parent;
|
|
13
22
|
constructor(data, url = "", specification = undefined) {
|
|
14
23
|
this.data = data;
|
|
15
24
|
this.url = url;
|
|
@@ -53,7 +62,9 @@ export class Requirement {
|
|
|
53
62
|
if (!this.has(key)) {
|
|
54
63
|
return undefined;
|
|
55
64
|
}
|
|
56
|
-
|
|
65
|
+
const child = new Requirement(this.data[key], `${this.url}/${this.escapeJsonPointer(key)}`, this.specification);
|
|
66
|
+
child.parent = this;
|
|
67
|
+
return child;
|
|
57
68
|
}
|
|
58
69
|
/**
|
|
59
70
|
* Navigates to a descendant node using a slash-delimited JSON Pointer path.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { printObject } from "./printers.js";
|
|
2
2
|
import { SchemaTypeCoder } from "./schema-type-coder.js";
|
|
3
3
|
import { TypeCoder } from "./type-coder.js";
|
|
4
|
+
import { pathJoin } from "../util/forward-slash-path.js";
|
|
4
5
|
export class ResponseTypeCoder extends TypeCoder {
|
|
5
6
|
openApi2MediaTypes;
|
|
6
|
-
constructor(requirement, openApi2MediaTypes = []) {
|
|
7
|
-
super(requirement);
|
|
7
|
+
constructor(requirement, version = "", openApi2MediaTypes = []) {
|
|
8
|
+
super(requirement, version);
|
|
8
9
|
this.openApi2MediaTypes = openApi2MediaTypes;
|
|
9
10
|
}
|
|
10
11
|
names() {
|
|
@@ -17,14 +18,14 @@ export class ResponseTypeCoder extends TypeCoder {
|
|
|
17
18
|
.map((content, mediaType) => [
|
|
18
19
|
mediaType,
|
|
19
20
|
`{
|
|
20
|
-
schema: ${content.has("schema") ? new SchemaTypeCoder(content.get("schema")).write(script) : "unknown"}
|
|
21
|
+
schema: ${content.has("schema") ? new SchemaTypeCoder(content.get("schema"), this.version).write(script) : "unknown"}
|
|
21
22
|
}`,
|
|
22
23
|
]);
|
|
23
24
|
}
|
|
24
25
|
return this.openApi2MediaTypes.map((mediaType) => [
|
|
25
26
|
mediaType,
|
|
26
27
|
`{
|
|
27
|
-
schema: ${new SchemaTypeCoder(response.get("schema")).write(script)}
|
|
28
|
+
schema: ${new SchemaTypeCoder(response.get("schema"), this.version).write(script)}
|
|
28
29
|
}`,
|
|
29
30
|
]);
|
|
30
31
|
}
|
|
@@ -39,7 +40,7 @@ export class ResponseTypeCoder extends TypeCoder {
|
|
|
39
40
|
.get("headers")
|
|
40
41
|
.map((value, name) => [
|
|
41
42
|
name,
|
|
42
|
-
`{ schema: ${new SchemaTypeCoder(value.get("schema") ?? value).write(script)}}`,
|
|
43
|
+
`{ schema: ${new SchemaTypeCoder(value.get("schema") ?? value, this.version).write(script)}}`,
|
|
43
44
|
]);
|
|
44
45
|
}
|
|
45
46
|
printHeaders(script, response) {
|
|
@@ -77,7 +78,7 @@ export class ResponseTypeCoder extends TypeCoder {
|
|
|
77
78
|
return printObject(exampleNames.map((name) => [name, "unknown"]));
|
|
78
79
|
}
|
|
79
80
|
modulePath() {
|
|
80
|
-
return
|
|
81
|
+
return pathJoin("types", this.version, this.requirement.data["$ref"] + ".ts");
|
|
81
82
|
}
|
|
82
83
|
writeCode(script) {
|
|
83
84
|
return `{
|
|
@@ -3,8 +3,8 @@ import { ResponseTypeCoder } from "./response-type-coder.js";
|
|
|
3
3
|
import { TypeCoder } from "./type-coder.js";
|
|
4
4
|
export class ResponsesTypeCoder extends TypeCoder {
|
|
5
5
|
openApi2MediaTypes;
|
|
6
|
-
constructor(requirement, openApi2MediaTypes = []) {
|
|
7
|
-
super(requirement);
|
|
6
|
+
constructor(requirement, version = "", openApi2MediaTypes = []) {
|
|
7
|
+
super(requirement, version);
|
|
8
8
|
this.openApi2MediaTypes = openApi2MediaTypes;
|
|
9
9
|
}
|
|
10
10
|
typeForDefaultStatusCode(listedStatusCodes) {
|
|
@@ -21,10 +21,16 @@ export class ResponsesTypeCoder extends TypeCoder {
|
|
|
21
21
|
return statusCode;
|
|
22
22
|
}
|
|
23
23
|
buildResponseObjectType(script) {
|
|
24
|
-
|
|
24
|
+
const entries = this.requirement.map((response, responseCode) => [
|
|
25
25
|
this.normalizeStatusCode(responseCode),
|
|
26
|
-
new ResponseTypeCoder(response, this.openApi2MediaTypes).write(script),
|
|
27
|
-
])
|
|
26
|
+
new ResponseTypeCoder(response, this.version, this.openApi2MediaTypes).write(script),
|
|
27
|
+
]);
|
|
28
|
+
const explicitEntries = entries.filter(([key]) => !key.startsWith("["));
|
|
29
|
+
const mappedEntries = entries.filter(([key]) => key.startsWith("["));
|
|
30
|
+
if (explicitEntries.length > 0 && mappedEntries.length > 0) {
|
|
31
|
+
return `${printObjectWithoutQuotes(explicitEntries)} & ${printObjectWithoutQuotes(mappedEntries)}`;
|
|
32
|
+
}
|
|
33
|
+
return printObjectWithoutQuotes(entries);
|
|
28
34
|
}
|
|
29
35
|
writeCode(script) {
|
|
30
36
|
script.importSharedType("ResponseBuilderFactory");
|
|
@@ -13,7 +13,7 @@ export class SchemaCoder extends Coder {
|
|
|
13
13
|
objectSchema(script) {
|
|
14
14
|
const { properties, required } = this.requirement.data;
|
|
15
15
|
const propertyLines = Object.keys(properties ?? {}).map((name) => {
|
|
16
|
-
const schemaCoder = new SchemaCoder(this.requirement.select(`properties/${name}`));
|
|
16
|
+
const schemaCoder = new SchemaCoder(this.requirement.select(`properties/${name}`), this.version);
|
|
17
17
|
return `"${name}": ${schemaCoder.write(script)}`;
|
|
18
18
|
});
|
|
19
19
|
return `
|
|
@@ -27,7 +27,7 @@ export class SchemaCoder extends Coder {
|
|
|
27
27
|
arraySchema(script) {
|
|
28
28
|
return `{
|
|
29
29
|
type: "array",
|
|
30
|
-
items: ${new SchemaCoder(this.requirement.get("items")).write(script)}
|
|
30
|
+
items: ${new SchemaCoder(this.requirement.get("items"), this.version).write(script)}
|
|
31
31
|
}`;
|
|
32
32
|
}
|
|
33
33
|
typeDeclaration(_namespace, script) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildJsDoc } from "./jsdoc.js";
|
|
2
2
|
import { TypeCoder } from "./type-coder.js";
|
|
3
|
+
import { pathJoin } from "../util/forward-slash-path.js";
|
|
3
4
|
export class SchemaTypeCoder extends TypeCoder {
|
|
4
5
|
names() {
|
|
5
6
|
return super.names(this.requirement.data["$ref"]?.split("/").at(-1));
|
|
@@ -16,7 +17,7 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
16
17
|
return "unknown";
|
|
17
18
|
}
|
|
18
19
|
const requirement = this.requirement.get("additionalProperties");
|
|
19
|
-
return new SchemaTypeCoder(requirement).write(script);
|
|
20
|
+
return new SchemaTypeCoder(requirement, this.version).write(script);
|
|
20
21
|
}
|
|
21
22
|
objectSchema(script) {
|
|
22
23
|
const { data } = this.requirement;
|
|
@@ -28,7 +29,8 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
28
29
|
const optionalFlag = isRequired ? "" : "?";
|
|
29
30
|
const comment = buildJsDoc(property.data);
|
|
30
31
|
const commentPrefix = comment ? `\n${comment}` : "";
|
|
31
|
-
|
|
32
|
+
const propertyType = new SchemaTypeCoder(property, this.version).write(script);
|
|
33
|
+
return `${commentPrefix}"${name}"${optionalFlag}: ${propertyType}`;
|
|
32
34
|
});
|
|
33
35
|
if (typedData.additionalProperties) {
|
|
34
36
|
properties.push(`[key: string]: ${this.additionalPropertiesType(script)}`);
|
|
@@ -36,7 +38,7 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
36
38
|
return `{${properties.join(",")}}`;
|
|
37
39
|
}
|
|
38
40
|
arraySchema(script) {
|
|
39
|
-
return `Array<${new SchemaTypeCoder(this.requirement.get("items")).write(script)}>`;
|
|
41
|
+
return `Array<${new SchemaTypeCoder(this.requirement.get("items"), this.version).write(script)}>`;
|
|
40
42
|
}
|
|
41
43
|
writePrimitive(value) {
|
|
42
44
|
if (typeof value === "string") {
|
|
@@ -79,7 +81,7 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
79
81
|
}
|
|
80
82
|
const key = matchingKey();
|
|
81
83
|
const items = (allOf ?? anyOf ?? oneOf);
|
|
82
|
-
const types = items.map((_item, index) => new SchemaTypeCoder(this.requirement.get(key).get(index)).write(script));
|
|
84
|
+
const types = items.map((_item, index) => new SchemaTypeCoder(this.requirement.get(key).get(index), this.version).write(script));
|
|
83
85
|
return types.join(allOf ? " & " : " | ");
|
|
84
86
|
}
|
|
85
87
|
writeEnum(_script, requirement) {
|
|
@@ -88,7 +90,7 @@ export class SchemaTypeCoder extends TypeCoder {
|
|
|
88
90
|
.join(" | ");
|
|
89
91
|
}
|
|
90
92
|
modulePath() {
|
|
91
|
-
return
|
|
93
|
+
return pathJoin("types", this.version, this.requirement.data["$ref"].replace(/^#\//u, "") + ".ts");
|
|
92
94
|
}
|
|
93
95
|
writeCode(script) {
|
|
94
96
|
const { allOf, anyOf, oneOf, type, format } = this.requirement.data;
|
|
@@ -17,6 +17,8 @@ export class Script {
|
|
|
17
17
|
repository;
|
|
18
18
|
comments;
|
|
19
19
|
exports;
|
|
20
|
+
versions;
|
|
21
|
+
versionFormatters;
|
|
20
22
|
imports;
|
|
21
23
|
externalImport;
|
|
22
24
|
cache;
|
|
@@ -26,6 +28,8 @@ export class Script {
|
|
|
26
28
|
this.repository = repository;
|
|
27
29
|
this.comments = [];
|
|
28
30
|
this.exports = new Map();
|
|
31
|
+
this.versions = new Map();
|
|
32
|
+
this.versionFormatters = new Map();
|
|
29
33
|
this.imports = new Map();
|
|
30
34
|
this.externalImport = new Map();
|
|
31
35
|
this.cache = new Map();
|
|
@@ -173,16 +177,77 @@ export class Script {
|
|
|
173
177
|
importSharedType(name) {
|
|
174
178
|
return this.importExternal(name, pathJoin(this.relativePathToBase, "counterfact-types/index.ts"), true);
|
|
175
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Imports a type from the generated `types/versions.ts` module,
|
|
182
|
+
* resolving the path relative to this script's location in the repository.
|
|
183
|
+
*
|
|
184
|
+
* @param name - The type name to import (e.g. `"Versioned"`).
|
|
185
|
+
*/
|
|
186
|
+
importVersionsType(name) {
|
|
187
|
+
return this.importExternal(name, pathJoin(this.relativePathToBase, "types/versions.ts"), true);
|
|
188
|
+
}
|
|
176
189
|
exportType(coder) {
|
|
177
190
|
return this.export(coder, true);
|
|
178
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Registers a formatter function for the merged versioned type emitted under
|
|
194
|
+
* `name` by {@link versionsTypeStatements}.
|
|
195
|
+
*
|
|
196
|
+
* When a formatter is present for a name, `versionsTypeStatements` delegates
|
|
197
|
+
* the entire type declaration to it instead of generating the default
|
|
198
|
+
* `Versions` object type. The formatter receives a `Map<version, importAlias>`
|
|
199
|
+
* and must return the complete TypeScript source for that operation type.
|
|
200
|
+
*/
|
|
201
|
+
setVersionFormatter(name, formatter) {
|
|
202
|
+
this.versionFormatters.set(name, formatter);
|
|
203
|
+
}
|
|
204
|
+
declareVersion(coder, name) {
|
|
205
|
+
const version = coder.version;
|
|
206
|
+
const versions = this.versions.get(name) ?? new Map();
|
|
207
|
+
this.versions.set(name, versions);
|
|
208
|
+
if (versions.has(version)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const versionStatement = {
|
|
212
|
+
beforeExport: "",
|
|
213
|
+
done: false,
|
|
214
|
+
id: coder.id,
|
|
215
|
+
isDefault: false,
|
|
216
|
+
isType: true,
|
|
217
|
+
jsdoc: "",
|
|
218
|
+
typeDeclaration: "",
|
|
219
|
+
};
|
|
220
|
+
versionStatement.promise = coder
|
|
221
|
+
.delegate()
|
|
222
|
+
.then((availableCoder) => {
|
|
223
|
+
versionStatement.code = availableCoder.write(this);
|
|
224
|
+
return availableCoder;
|
|
225
|
+
})
|
|
226
|
+
.catch((error) => {
|
|
227
|
+
versionStatement.code = `unknown /* error declaring version "${name}" (${version}) for ${this.path}: ${error.message} */`;
|
|
228
|
+
versionStatement.error = error;
|
|
229
|
+
return undefined;
|
|
230
|
+
})
|
|
231
|
+
.finally(() => {
|
|
232
|
+
versionStatement.done = true;
|
|
233
|
+
});
|
|
234
|
+
versions.set(version, versionStatement);
|
|
235
|
+
}
|
|
179
236
|
/** `true` while at least one export promise is still pending. */
|
|
180
237
|
isInProgress() {
|
|
181
|
-
return Array.from(this.exports.values()).some((exportStatement) => !exportStatement.done)
|
|
238
|
+
return (Array.from(this.exports.values()).some((exportStatement) => !exportStatement.done) ||
|
|
239
|
+
Array.from(this.versions.values())
|
|
240
|
+
.flatMap((versions) => Array.from(versions.values()))
|
|
241
|
+
.some((versionStatement) => !versionStatement.done));
|
|
182
242
|
}
|
|
183
243
|
/** Returns a promise that resolves when all pending export promises settle. */
|
|
184
244
|
finished() {
|
|
185
|
-
return Promise.all(
|
|
245
|
+
return Promise.all([
|
|
246
|
+
...Array.from(this.exports.values(), (value) => value.promise),
|
|
247
|
+
...Array.from(this.versions.values())
|
|
248
|
+
.flatMap((versions) => Array.from(versions.values()))
|
|
249
|
+
.map((value) => value.promise),
|
|
250
|
+
]);
|
|
186
251
|
}
|
|
187
252
|
externalImportStatements() {
|
|
188
253
|
return Array.from(this.externalImport, ([name, { isDefault, isType, modulePath }]) => `import${isType ? " type" : ""} ${isDefault ? name : `{ ${name} }`} from "${modulePath}";`);
|
|
@@ -208,18 +273,49 @@ export class Script {
|
|
|
208
273
|
return `${jsdoc}${beforeExport}export ${keyword} ${name ?? ""}${typeAnnotation} = ${code};`;
|
|
209
274
|
});
|
|
210
275
|
}
|
|
276
|
+
versionsTypeStatements() {
|
|
277
|
+
if (this.versions.size === 0) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
const statements = [];
|
|
281
|
+
const unformatted = [];
|
|
282
|
+
for (const [name, versions] of this.versions) {
|
|
283
|
+
const formatter = this.versionFormatters.get(name);
|
|
284
|
+
if (formatter) {
|
|
285
|
+
const versionCodes = new Map(Array.from(versions, ([version, stmt]) => [
|
|
286
|
+
version,
|
|
287
|
+
stmt.code,
|
|
288
|
+
]));
|
|
289
|
+
statements.push(formatter(versionCodes));
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
unformatted.push([name, versions]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (unformatted.length > 0) {
|
|
296
|
+
const names = unformatted.map(([name, versions]) => {
|
|
297
|
+
const mappedVersions = Array.from(versions, ([version, versionStatement]) => `"${version}": ${versionStatement.code}`);
|
|
298
|
+
return `"${name}": { ${mappedVersions.join(", ")} }`;
|
|
299
|
+
});
|
|
300
|
+
statements.push(`export type Versions = { ${names.join(", ")} };`);
|
|
301
|
+
}
|
|
302
|
+
return statements;
|
|
303
|
+
}
|
|
211
304
|
/**
|
|
212
305
|
* Formats the fully assembled script source with Prettier and returns it.
|
|
213
306
|
*
|
|
214
307
|
* All pending export promises are awaited before formatting.
|
|
215
308
|
*/
|
|
216
|
-
contents() {
|
|
309
|
+
async contents() {
|
|
310
|
+
await this.finished();
|
|
217
311
|
return format([
|
|
218
312
|
this.comments.map((comment) => `// ${comment}`).join("\n"),
|
|
219
313
|
this.comments.length > 0 ? "\n\n" : "",
|
|
220
314
|
this.externalImportStatements().join("\n"),
|
|
221
315
|
this.importStatements().join("\n"),
|
|
222
316
|
"\n\n",
|
|
317
|
+
this.versionsTypeStatements().join("\n"),
|
|
318
|
+
this.versions.size > 0 ? "\n\n" : "",
|
|
223
319
|
this.exportStatements().join("\n\n"),
|
|
224
320
|
].join(""), { parser: "typescript" });
|
|
225
321
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { format } from "prettier";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the `VersionsGTE` map: for each version V at index i,
|
|
4
|
+
* maps V to all versions at indices >= i (i.e. V and all later-declared
|
|
5
|
+
* versions, where "later" means "newer").
|
|
6
|
+
*/
|
|
7
|
+
function buildVersionsGTE(versions) {
|
|
8
|
+
const result = new Map();
|
|
9
|
+
versions.forEach((version, index) => {
|
|
10
|
+
result.set(version, versions.slice(index));
|
|
11
|
+
});
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Generates the TypeScript source text for `types/versions.ts`.
|
|
16
|
+
*
|
|
17
|
+
* Returns an empty string when `versions` is empty.
|
|
18
|
+
* The returned string is formatted with Prettier.
|
|
19
|
+
*
|
|
20
|
+
* @param versions - Ordered list of unique, non-empty version strings.
|
|
21
|
+
* The first entry is the oldest version.
|
|
22
|
+
*/
|
|
23
|
+
export async function generateVersionsTsContent(versions) {
|
|
24
|
+
if (versions.length === 0) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
const versionsUnion = versions.map((v) => `"${v}"`).join(" | ");
|
|
28
|
+
const versionsGTE = buildVersionsGTE(versions);
|
|
29
|
+
const versionsGTEBody = Array.from(versionsGTE, ([v, gte]) => ` "${v}": ${gte.map((g) => `"${g}"`).join(" | ")};`).join("\n");
|
|
30
|
+
const source = [
|
|
31
|
+
"// This file is auto-generated by Counterfact. Do not edit.",
|
|
32
|
+
"",
|
|
33
|
+
`export type Versions = ${versionsUnion};`,
|
|
34
|
+
"",
|
|
35
|
+
"/**",
|
|
36
|
+
" * Maps each version to the set of versions that are greater than or equal to it.",
|
|
37
|
+
" * Used by `Versioned.minVersion()` to narrow which versions a handler must support.",
|
|
38
|
+
" */",
|
|
39
|
+
"export type VersionsGTE = {",
|
|
40
|
+
versionsGTEBody,
|
|
41
|
+
"};",
|
|
42
|
+
"",
|
|
43
|
+
"type VersionMap = Partial<Record<Versions, object>>;",
|
|
44
|
+
"",
|
|
45
|
+
"export type Versioned<",
|
|
46
|
+
" T extends VersionMap,",
|
|
47
|
+
" V extends keyof T & Versions = keyof T & Versions,",
|
|
48
|
+
"> = T[V] & {",
|
|
49
|
+
" version: V;",
|
|
50
|
+
" minVersion<M extends keyof T & Versions>(",
|
|
51
|
+
" min: M,",
|
|
52
|
+
" ): this is Versioned<T, Extract<V, VersionsGTE[M]>>;",
|
|
53
|
+
"};",
|
|
54
|
+
"",
|
|
55
|
+
].join("\n");
|
|
56
|
+
return format(source, { parser: "typescript" });
|
|
57
|
+
}
|
package/package.json
CHANGED