@mittwald/api-code-generator 4.466.0 → 4.468.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/dist/esm/generation/dateTime/dateTimeInput.integration.test.js +131 -0
- package/dist/esm/generation/dateTime/dateTimeInput.js +66 -0
- package/dist/esm/generation/dateTime/dateTimeInput.test.js +86 -0
- package/dist/esm/generation/dateTime/dateTimeInputRefs.js +23 -0
- package/dist/esm/generation/dateTime/dateTimeInputSchemaNames.js +103 -0
- package/dist/esm/generation/model/CodeGenerationModel.js +7 -0
- package/dist/esm/generation/model/components/Components.js +5 -0
- package/dist/esm/generation/model/components/Parameters.js +2 -1
- package/dist/esm/generation/model/components/RequestBodies.js +2 -1
- package/dist/esm/generation/model/components/RequestSchemas.js +43 -0
- package/dist/esm/generation/model/global/JSONSchema.js +18 -0
- package/dist/esm/generation/model/paths/operation/RequestParameters.js +5 -4
- package/dist/esm/generation/refs/componentRefsToCustomTypes.js +4 -4
- package/dist/types/generation/dateTime/dateTimeInput.d.ts +23 -0
- package/dist/types/generation/dateTime/dateTimeInput.integration.test.d.ts +1 -0
- package/dist/types/generation/dateTime/dateTimeInput.test.d.ts +1 -0
- package/dist/types/generation/dateTime/dateTimeInputRefs.d.ts +13 -0
- package/dist/types/generation/dateTime/dateTimeInputSchemaNames.d.ts +14 -0
- package/dist/types/generation/model/CodeGenerationModel.d.ts +5 -0
- package/dist/types/generation/model/components/Components.d.ts +2 -0
- package/dist/types/generation/model/components/RequestSchemas.d.ts +25 -0
- package/dist/types/generation/model/global/JSONSchema.d.ts +12 -0
- package/dist/types/generation/refs/componentRefsToCustomTypes.d.ts +7 -1
- package/package.json +2 -2
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { CodeGenerationModel } from "../model/CodeGenerationModel.js";
|
|
2
|
+
const doc = {
|
|
3
|
+
openapi: "3.0.0",
|
|
4
|
+
info: { title: "test", version: "1" },
|
|
5
|
+
components: {
|
|
6
|
+
schemas: {
|
|
7
|
+
Shared: {
|
|
8
|
+
type: "object",
|
|
9
|
+
required: ["createdAt"],
|
|
10
|
+
properties: {
|
|
11
|
+
createdAt: { type: "string", format: "date-time" },
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
SharedWrapper: {
|
|
15
|
+
type: "object",
|
|
16
|
+
required: ["shared"],
|
|
17
|
+
properties: {
|
|
18
|
+
shared: { $ref: "#/components/schemas/Shared" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
ResponseOnly: {
|
|
22
|
+
type: "object",
|
|
23
|
+
required: ["seenAt"],
|
|
24
|
+
properties: {
|
|
25
|
+
seenAt: { type: "string", format: "date-time" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
paths: {
|
|
31
|
+
"/things": {
|
|
32
|
+
post: {
|
|
33
|
+
operationId: "createThing",
|
|
34
|
+
parameters: [
|
|
35
|
+
{
|
|
36
|
+
name: "since",
|
|
37
|
+
in: "query",
|
|
38
|
+
required: false,
|
|
39
|
+
schema: { type: "string", format: "date-time" },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "day",
|
|
43
|
+
in: "query",
|
|
44
|
+
required: false,
|
|
45
|
+
schema: { type: "string", format: "date" },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
requestBody: {
|
|
49
|
+
content: {
|
|
50
|
+
"application/json": {
|
|
51
|
+
schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
required: ["startsAt", "wrapper"],
|
|
54
|
+
properties: {
|
|
55
|
+
startsAt: { type: "string", format: "date-time" },
|
|
56
|
+
dueDay: { type: "string", format: "date" },
|
|
57
|
+
wrapper: { $ref: "#/components/schemas/SharedWrapper" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
responses: {
|
|
64
|
+
"200": {
|
|
65
|
+
description: "ok",
|
|
66
|
+
content: {
|
|
67
|
+
"application/json": {
|
|
68
|
+
schema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
required: ["finishedAt", "shared", "responseOnly"],
|
|
71
|
+
properties: {
|
|
72
|
+
finishedAt: { type: "string", format: "date-time" },
|
|
73
|
+
shared: { $ref: "#/components/schemas/Shared" },
|
|
74
|
+
responseOnly: { $ref: "#/components/schemas/ResponseOnly" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const compile = () => CodeGenerationModel.fromDoc("Test", doc).compileTypes({
|
|
86
|
+
rootNamespace: "Test",
|
|
87
|
+
});
|
|
88
|
+
const lineOf = (types, property) => {
|
|
89
|
+
const lines = types
|
|
90
|
+
.split("\n")
|
|
91
|
+
.filter((l) => l.trim().startsWith(`${property}`));
|
|
92
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
};
|
|
95
|
+
describe("date-time widening", () => {
|
|
96
|
+
let types;
|
|
97
|
+
beforeAll(async () => {
|
|
98
|
+
types = await compile();
|
|
99
|
+
});
|
|
100
|
+
test("request body date-time accepts a Date", () => {
|
|
101
|
+
expect(lineOf(types, "startsAt")).toContain("string | Date");
|
|
102
|
+
});
|
|
103
|
+
test("request body date (YYYY-MM-DD) stays a string", () => {
|
|
104
|
+
expect(lineOf(types, "dueDay")).not.toContain("Date");
|
|
105
|
+
});
|
|
106
|
+
test("query parameter date-time accepts a Date", () => {
|
|
107
|
+
expect(lineOf(types, "since?")).toContain("string | Date");
|
|
108
|
+
});
|
|
109
|
+
test("query parameter date (YYYY-MM-DD) stays a string", () => {
|
|
110
|
+
expect(lineOf(types, "day?")).not.toContain("Date");
|
|
111
|
+
});
|
|
112
|
+
test("response date-time stays a string", () => {
|
|
113
|
+
expect(lineOf(types, "finishedAt")).not.toContain("Date");
|
|
114
|
+
});
|
|
115
|
+
test("shared component schemas stay unchanged", () => {
|
|
116
|
+
// Components.Schemas.Shared is used by the response and must not change
|
|
117
|
+
expect(lineOf(types, "createdAt")).toContain("createdAt: string;");
|
|
118
|
+
});
|
|
119
|
+
test("a widened request variant is emitted for shared schemas", () => {
|
|
120
|
+
expect(types).toContain("namespace RequestSchemas");
|
|
121
|
+
expect(lineOf(types, "createdAt")).toContain("createdAt: string | Date;");
|
|
122
|
+
});
|
|
123
|
+
test("request refs point at the widened variant", () => {
|
|
124
|
+
expect(lineOf(types, "wrapper")).toContain("Test.Components.RequestSchemas.SharedWrapper");
|
|
125
|
+
expect(lineOf(types, "shared")).toContain("Test.Components.RequestSchemas.Shared");
|
|
126
|
+
expect(lineOf(types, "shared")).toContain("Test.Components.Schemas.Shared");
|
|
127
|
+
});
|
|
128
|
+
test("response-only schemas get no request variant", () => {
|
|
129
|
+
expect(types).not.toContain("RequestSchemas.ResponseOnly");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import is from "@sindresorhus/is";
|
|
2
|
+
import cloneDeep from "clone-deep";
|
|
3
|
+
/**
|
|
4
|
+
* The TypeScript type emitted for `string` schemas with `format: date-time` in
|
|
5
|
+
* request position.
|
|
6
|
+
*
|
|
7
|
+
* The parentheses are required: `json-schema-to-typescript` appends `[]` to the
|
|
8
|
+
* `tsType` of array items without wrapping it, so an unparenthesized union
|
|
9
|
+
* would compile to `string | Date[]` instead of `(string | Date)[]`.
|
|
10
|
+
*/
|
|
11
|
+
export const dateTimeInputTsType = "(string | Date)";
|
|
12
|
+
/**
|
|
13
|
+
* Only `date-time` is widened, _not_ `date`.
|
|
14
|
+
*
|
|
15
|
+
* The wire format of `format: date` is `YYYY-MM-DD`, while a `Date` can only be
|
|
16
|
+
* serialized generically (i.e. without knowing the schema) to a full ISO 8601
|
|
17
|
+
* timestamp. Accepting a `Date` there would therefore produce a value the API
|
|
18
|
+
* rejects.
|
|
19
|
+
*/
|
|
20
|
+
const isDateTimeStringSchema = (schema) => schema.format === "date-time" &&
|
|
21
|
+
schema.type === "string" &&
|
|
22
|
+
schema.enum === undefined &&
|
|
23
|
+
schema.const === undefined &&
|
|
24
|
+
schema.tsType === undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Recursively widens every `{ type: "string", format: "date-time" }` sub-schema
|
|
27
|
+
* to `string | Date`, so that callers may pass a JS `Date` instead of a
|
|
28
|
+
* hand-formatted ISO 8601 string.
|
|
29
|
+
*
|
|
30
|
+
* This must only be applied to schemas in _request_ position: widening a
|
|
31
|
+
* response type would be a breaking change for consumers.
|
|
32
|
+
*/
|
|
33
|
+
export const widenDateTimeInputs = (something, clone = true) => {
|
|
34
|
+
if (clone) {
|
|
35
|
+
something = cloneDeep(something);
|
|
36
|
+
}
|
|
37
|
+
if (!is.nonEmptyObject(something)) {
|
|
38
|
+
return something;
|
|
39
|
+
}
|
|
40
|
+
if (is.array(something)) {
|
|
41
|
+
return something.map((item) => widenDateTimeInputs(item, false));
|
|
42
|
+
}
|
|
43
|
+
if (isDateTimeStringSchema(something)) {
|
|
44
|
+
return { ...something, tsType: dateTimeInputTsType };
|
|
45
|
+
}
|
|
46
|
+
return Object.fromEntries(Object.entries(something).map(([key, value]) => [
|
|
47
|
+
key,
|
|
48
|
+
widenDateTimeInputs(value, false),
|
|
49
|
+
]));
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Returns `true` if the given schema contains a `date-time` string _itself_,
|
|
53
|
+
* i.e. without following `$ref`s.
|
|
54
|
+
*/
|
|
55
|
+
export const containsDateTimeInput = (something) => {
|
|
56
|
+
if (!is.nonEmptyObject(something)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (is.array(something)) {
|
|
60
|
+
return something.some((item) => containsDateTimeInput(item));
|
|
61
|
+
}
|
|
62
|
+
if (isDateTimeStringSchema(something)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return Object.entries(something).some(([key, value]) => key !== "$ref" && containsDateTimeInput(value));
|
|
66
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { containsDateTimeInput, dateTimeInputTsType, widenDateTimeInputs, } from "./dateTimeInput.js";
|
|
2
|
+
describe("widenDateTimeInputs", () => {
|
|
3
|
+
test("widens a date-time string", () => {
|
|
4
|
+
expect(widenDateTimeInputs({ type: "string", format: "date-time" })).toEqual({
|
|
5
|
+
type: "string",
|
|
6
|
+
format: "date-time",
|
|
7
|
+
tsType: dateTimeInputTsType,
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
test("does not widen date (YYYY-MM-DD has a different wire format)", () => {
|
|
11
|
+
const schema = { type: "string", format: "date" };
|
|
12
|
+
expect(widenDateTimeInputs(schema)).toEqual(schema);
|
|
13
|
+
});
|
|
14
|
+
test("does not widen plain strings", () => {
|
|
15
|
+
const schema = { type: "string" };
|
|
16
|
+
expect(widenDateTimeInputs(schema)).toEqual(schema);
|
|
17
|
+
});
|
|
18
|
+
test("does not widen enums", () => {
|
|
19
|
+
const schema = {
|
|
20
|
+
type: "string",
|
|
21
|
+
format: "date-time",
|
|
22
|
+
enum: ["2024-01-01T00:00:00Z"],
|
|
23
|
+
};
|
|
24
|
+
expect(widenDateTimeInputs(schema)).toEqual(schema);
|
|
25
|
+
});
|
|
26
|
+
test("does not overwrite an existing tsType", () => {
|
|
27
|
+
const schema = {
|
|
28
|
+
type: "string",
|
|
29
|
+
format: "date-time",
|
|
30
|
+
tsType: "SomethingElse",
|
|
31
|
+
};
|
|
32
|
+
expect(widenDateTimeInputs(schema)).toEqual(schema);
|
|
33
|
+
});
|
|
34
|
+
test("widens nested properties, arrays and compositions", () => {
|
|
35
|
+
const dateTime = { type: "string", format: "date-time" };
|
|
36
|
+
const widened = { ...dateTime, tsType: dateTimeInputTsType };
|
|
37
|
+
expect(widenDateTimeInputs({
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
nested: { type: "object", properties: { at: dateTime } },
|
|
41
|
+
list: { type: "array", items: dateTime },
|
|
42
|
+
composed: { anyOf: [dateTime, { type: "null" }] },
|
|
43
|
+
untouched: { type: "number" },
|
|
44
|
+
},
|
|
45
|
+
})).toEqual({
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
nested: { type: "object", properties: { at: widened } },
|
|
49
|
+
list: { type: "array", items: widened },
|
|
50
|
+
composed: { anyOf: [widened, { type: "null" }] },
|
|
51
|
+
untouched: { type: "number" },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
test("does not mutate the input", () => {
|
|
56
|
+
const schema = {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: { at: { type: "string", format: "date-time" } },
|
|
59
|
+
};
|
|
60
|
+
const before = JSON.stringify(schema);
|
|
61
|
+
widenDateTimeInputs(schema);
|
|
62
|
+
expect(JSON.stringify(schema)).toBe(before);
|
|
63
|
+
});
|
|
64
|
+
test("leaves refs alone", () => {
|
|
65
|
+
const schema = { $ref: "#/components/schemas/Foo" };
|
|
66
|
+
expect(widenDateTimeInputs(schema)).toEqual(schema);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("containsDateTimeInput", () => {
|
|
70
|
+
test.each([
|
|
71
|
+
[{ type: "string", format: "date-time" }, true],
|
|
72
|
+
[{ type: "object", properties: { at: { $ref: "#/x" } } }, false],
|
|
73
|
+
[
|
|
74
|
+
{
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: { at: { type: "string", format: "date-time" } },
|
|
77
|
+
},
|
|
78
|
+
true,
|
|
79
|
+
],
|
|
80
|
+
[{ type: "object", properties: { at: { type: "string" } } }, false],
|
|
81
|
+
[{ type: "string", format: "date" }, false],
|
|
82
|
+
[{}, false],
|
|
83
|
+
])("works for test %#", (schema, expected) => {
|
|
84
|
+
expect(containsDateTimeInput(schema)).toBe(expected);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { refNameToTSName } from "../refs/refNameToTSName.js";
|
|
2
|
+
import { tsNamespaceName } from "../tsNamespaceName.js";
|
|
3
|
+
const schemaRefPrefix = "#/components/schemas/";
|
|
4
|
+
/**
|
|
5
|
+
* Namespace the widened request variants of component schemas are emitted to.
|
|
6
|
+
*
|
|
7
|
+
* A separate namespace (instead of a suffixed name inside `Schemas`) avoids
|
|
8
|
+
* collisions with schemas that happen to be named `…Request`.
|
|
9
|
+
*/
|
|
10
|
+
export const requestSchemasNs = "requestSchemas";
|
|
11
|
+
/**
|
|
12
|
+
* Redirects refs to component schemas that have a widened request variant to
|
|
13
|
+
* that variant; all other refs are resolved as usual.
|
|
14
|
+
*/
|
|
15
|
+
export const dateTimeInputRefTSNameResolver = (variantNames) => (rootNamespace, $ref) => {
|
|
16
|
+
if ($ref.startsWith(schemaRefPrefix)) {
|
|
17
|
+
const schemaName = $ref.slice(schemaRefPrefix.length);
|
|
18
|
+
if (variantNames.has(schemaName)) {
|
|
19
|
+
return tsNamespaceName(rootNamespace, "components", requestSchemasNs, schemaName);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return refNameToTSName(rootNamespace, $ref);
|
|
23
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import is from "@sindresorhus/is";
|
|
2
|
+
import { OpenAPIV3 } from "openapi-types";
|
|
3
|
+
import { containsDateTimeInput } from "./dateTimeInput.js";
|
|
4
|
+
const schemaRefPrefix = "#/components/schemas/";
|
|
5
|
+
const collectSchemaRefs = (something, into) => {
|
|
6
|
+
if (!is.nonEmptyObject(something)) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (is.array(something)) {
|
|
10
|
+
something.forEach((item) => collectSchemaRefs(item, into));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const [key, value] of Object.entries(something)) {
|
|
14
|
+
if (key === "$ref") {
|
|
15
|
+
if (typeof value === "string" && value.startsWith(schemaRefPrefix)) {
|
|
16
|
+
into.add(value.slice(schemaRefPrefix.length));
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
collectSchemaRefs(value, into);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const refsOf = (something) => {
|
|
24
|
+
const refs = new Set();
|
|
25
|
+
collectSchemaRefs(something, refs);
|
|
26
|
+
return refs;
|
|
27
|
+
};
|
|
28
|
+
const httpMethods = Object.values(OpenAPIV3.HttpMethods);
|
|
29
|
+
/**
|
|
30
|
+
* Collects every part of the document that ends up in _request_ position, i.e.
|
|
31
|
+
* request bodies and request parameters.
|
|
32
|
+
*/
|
|
33
|
+
const collectRequestSchemaRoots = (doc) => {
|
|
34
|
+
const roots = new Set();
|
|
35
|
+
for (const pathItem of Object.values(doc.paths ?? {})) {
|
|
36
|
+
if (!pathItem) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
collectSchemaRefs(pathItem.parameters, roots);
|
|
40
|
+
for (const [key, operation] of Object.entries(pathItem)) {
|
|
41
|
+
if (!httpMethods.includes(key) || !is.nonEmptyObject(operation)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const op = operation;
|
|
45
|
+
collectSchemaRefs(op.requestBody, roots);
|
|
46
|
+
collectSchemaRefs(op.parameters, roots);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
collectSchemaRefs(doc.components?.requestBodies, roots);
|
|
50
|
+
collectSchemaRefs(doc.components?.parameters, roots);
|
|
51
|
+
return roots;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Determines for which `components.schemas` entries a widened _request variant_
|
|
55
|
+
* has to be generated.
|
|
56
|
+
*
|
|
57
|
+
* A variant is needed when the schema is reachable from a request body or a
|
|
58
|
+
* request parameter **and** it contains a `format: date-time` string, either
|
|
59
|
+
* directly or through one of its `$ref`s.
|
|
60
|
+
*
|
|
61
|
+
* Restricting this to request-reachable schemas keeps the generated output
|
|
62
|
+
* small; schemas that are only ever used in responses stay untouched, which is
|
|
63
|
+
* what keeps this change backwards compatible.
|
|
64
|
+
*/
|
|
65
|
+
export const dateTimeInputSchemaNames = (doc) => {
|
|
66
|
+
const schemas = doc.components?.schemas ?? {};
|
|
67
|
+
const refs = new Map();
|
|
68
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
69
|
+
refs.set(name, refsOf(schema));
|
|
70
|
+
}
|
|
71
|
+
// 1. schemas containing a date-time themselves
|
|
72
|
+
const containsDateTime = new Set(Object.entries(schemas)
|
|
73
|
+
.filter(([, schema]) => containsDateTimeInput(schema))
|
|
74
|
+
.map(([name]) => name));
|
|
75
|
+
// 2. …plus everything transitively referencing one of them
|
|
76
|
+
for (let changed = true; changed;) {
|
|
77
|
+
changed = false;
|
|
78
|
+
for (const [name, schemaRefs] of refs) {
|
|
79
|
+
if (containsDateTime.has(name)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const ref of schemaRefs) {
|
|
83
|
+
if (containsDateTime.has(ref)) {
|
|
84
|
+
containsDateTime.add(name);
|
|
85
|
+
changed = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 3. …intersected with the schemas reachable from request position
|
|
92
|
+
const reachable = new Set();
|
|
93
|
+
const queue = [...collectRequestSchemaRoots(doc)];
|
|
94
|
+
while (queue.length > 0) {
|
|
95
|
+
const name = queue.pop();
|
|
96
|
+
if (reachable.has(name)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
reachable.add(name);
|
|
100
|
+
queue.push(...(refs.get(name) ?? []));
|
|
101
|
+
}
|
|
102
|
+
return new Set([...containsDateTime].filter((name) => reachable.has(name)));
|
|
103
|
+
};
|
|
@@ -2,15 +2,22 @@ import { Name } from "./global/Name.js";
|
|
|
2
2
|
import { Components } from "./components/Components.js";
|
|
3
3
|
import { Paths } from "./paths/Paths.js";
|
|
4
4
|
import { Tag } from "./tags/Tag.js";
|
|
5
|
+
import { dateTimeInputSchemaNames } from "../dateTime/dateTimeInputSchemaNames.js";
|
|
5
6
|
export class CodeGenerationModel {
|
|
6
7
|
rootNamespace;
|
|
7
8
|
paths;
|
|
8
9
|
components;
|
|
9
10
|
tags;
|
|
10
11
|
doc;
|
|
12
|
+
/**
|
|
13
|
+
* Component schemas that need a widened request variant, because they are
|
|
14
|
+
* used in request position and contain a `format: date-time` string.
|
|
15
|
+
*/
|
|
16
|
+
dateTimeInputSchemaNames;
|
|
11
17
|
constructor(rootNamespace, doc) {
|
|
12
18
|
this.rootNamespace = new Name(rootNamespace);
|
|
13
19
|
this.doc = doc;
|
|
20
|
+
this.dateTimeInputSchemaNames = dateTimeInputSchemaNames(doc);
|
|
14
21
|
this.components = new Components(this);
|
|
15
22
|
this.tags = this.doc.tags?.map((doc) => Tag.fromDoc(doc)) ?? [];
|
|
16
23
|
this.paths = new Paths(this, this.doc.paths);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Name } from "../global/Name.js";
|
|
2
2
|
import { Schemas } from "./Schemas.js";
|
|
3
|
+
import { RequestSchemas } from "./RequestSchemas.js";
|
|
3
4
|
import { Parameters } from "./Parameters.js";
|
|
4
5
|
import { RequestBodies } from "./RequestBodies.js";
|
|
5
6
|
import { Responses } from "./Responses.js";
|
|
@@ -11,6 +12,7 @@ export class Components {
|
|
|
11
12
|
static ns = "Components";
|
|
12
13
|
name;
|
|
13
14
|
schemas;
|
|
15
|
+
requestSchemas;
|
|
14
16
|
securitySchemes;
|
|
15
17
|
parameters;
|
|
16
18
|
requestBodies;
|
|
@@ -20,6 +22,7 @@ export class Components {
|
|
|
20
22
|
this.model = model;
|
|
21
23
|
this.name = new Name(Components.ns, model.rootNamespace);
|
|
22
24
|
this.schemas = new Schemas(this, model.doc.components?.schemas ?? {});
|
|
25
|
+
this.requestSchemas = new RequestSchemas(this, model.doc.components?.schemas ?? {}, model.dateTimeInputSchemaNames);
|
|
23
26
|
this.securitySchemes = new SecuritySchemes(this, model.doc.components?.securitySchemes ?? {});
|
|
24
27
|
this.parameters = new Parameters(this, model.doc.components?.parameters ?? {});
|
|
25
28
|
this.requestBodies = new RequestBodies(this, model.doc.components?.requestBodies ?? {});
|
|
@@ -29,6 +32,7 @@ export class Components {
|
|
|
29
32
|
const t = {
|
|
30
33
|
ns: Components.ns,
|
|
31
34
|
schemas: await this.schemas.compileTypes(opts),
|
|
35
|
+
requestSchemas: await this.requestSchemas.compileTypes(opts),
|
|
32
36
|
parameters: await this.parameters.compileTypes(opts),
|
|
33
37
|
requestBodies: await this.requestBodies.compileTypes(opts),
|
|
34
38
|
responses: await this.responses.compileTypes(opts),
|
|
@@ -37,6 +41,7 @@ export class Components {
|
|
|
37
41
|
return `\
|
|
38
42
|
namespace ${t.ns} {
|
|
39
43
|
${t.schemas}
|
|
44
|
+
${t.requestSchemas}
|
|
40
45
|
${t.parameters}
|
|
41
46
|
${t.requestBodies}
|
|
42
47
|
${t.responses}
|
|
@@ -19,7 +19,8 @@ export class Parameters {
|
|
|
19
19
|
});
|
|
20
20
|
const t = {
|
|
21
21
|
ns: Parameters.ns,
|
|
22
|
-
|
|
22
|
+
// parameters are request-only, so they can be widened in place
|
|
23
|
+
types: await asyncStringJoin(schemas, (schema) => schema.compileAsRequestInput(opts, this.components.model.dateTimeInputSchemaNames)),
|
|
23
24
|
};
|
|
24
25
|
return `\
|
|
25
26
|
namespace ${t.ns} {
|
|
@@ -14,7 +14,8 @@ export class RequestBodies {
|
|
|
14
14
|
async compileTypes(opts) {
|
|
15
15
|
const t = {
|
|
16
16
|
ns: RequestBodies.ns,
|
|
17
|
-
|
|
17
|
+
// request bodies are request-only, so they can be widened in place
|
|
18
|
+
types: await asyncStringJoin(this.schemas, (schema) => schema.compileAsRequestInput(opts, this.components.model.dateTimeInputSchemaNames)),
|
|
18
19
|
};
|
|
19
20
|
return `\
|
|
20
21
|
namespace ${t.ns} {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { JSONSchema } from "../global/JSONSchema.js";
|
|
2
|
+
import { Name } from "../global/Name.js";
|
|
3
|
+
import { asyncStringJoin } from "../../asyncStringJoin.js";
|
|
4
|
+
import { populateNullableTypes } from "../../populateNullableTypes.js";
|
|
5
|
+
import { requestSchemasNs } from "../../dateTime/dateTimeInputRefs.js";
|
|
6
|
+
import { tsTypeName } from "../../tsTypeName.js";
|
|
7
|
+
import cloneDeep from "clone-deep";
|
|
8
|
+
/**
|
|
9
|
+
* Widened _request_ variants of component schemas.
|
|
10
|
+
*
|
|
11
|
+
* Only schemas that are reachable from a request body or request parameter and
|
|
12
|
+
* that (transitively) contain a `format: date-time` string get a variant here;
|
|
13
|
+
* see {@link dateTimeInputSchemaNames}. Everything else keeps referring to
|
|
14
|
+
* `Components.Schemas`.
|
|
15
|
+
*
|
|
16
|
+
* This namespace is purely additive: the original `Components.Schemas` types
|
|
17
|
+
* stay exactly as they were, which is what keeps response types – and therefore
|
|
18
|
+
* existing consumer code – unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export class RequestSchemas {
|
|
21
|
+
static ns = tsTypeName(requestSchemasNs);
|
|
22
|
+
schemas;
|
|
23
|
+
components;
|
|
24
|
+
name;
|
|
25
|
+
constructor(components, schemas, variantNames) {
|
|
26
|
+
this.components = components;
|
|
27
|
+
this.name = new Name(RequestSchemas.ns, components.name);
|
|
28
|
+
this.schemas = Object.entries(schemas ?? {})
|
|
29
|
+
.filter(([schemaName]) => variantNames.has(schemaName))
|
|
30
|
+
.map(([schemaName, schema]) => new JSONSchema(new Name(schemaName, this.name), populateNullableTypes(cloneDeep(schema))));
|
|
31
|
+
}
|
|
32
|
+
async compileTypes(opts) {
|
|
33
|
+
const t = {
|
|
34
|
+
ns: RequestSchemas.ns,
|
|
35
|
+
types: await asyncStringJoin(this.schemas, (schema) => schema.compileAsRequestInput(opts, this.components.model.dateTimeInputSchemaNames)),
|
|
36
|
+
};
|
|
37
|
+
return `\
|
|
38
|
+
namespace ${t.ns} {
|
|
39
|
+
${t.types}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -2,6 +2,8 @@ import { compileJsonSchema } from "../../compileJsonSchema.js";
|
|
|
2
2
|
import { Name } from "./Name.js";
|
|
3
3
|
import cloneDeep from "clone-deep";
|
|
4
4
|
import { componentRefsToCustomTypes } from "../../refs/componentRefsToCustomTypes.js";
|
|
5
|
+
import { widenDateTimeInputs } from "../../dateTime/dateTimeInput.js";
|
|
6
|
+
import { dateTimeInputRefTSNameResolver } from "../../dateTime/dateTimeInputRefs.js";
|
|
5
7
|
export class JSONSchema {
|
|
6
8
|
schemaObject;
|
|
7
9
|
name;
|
|
@@ -13,6 +15,22 @@ export class JSONSchema {
|
|
|
13
15
|
const withCustomRefTypes = componentRefsToCustomTypes(opts.rootNamespace, this.schemaObject);
|
|
14
16
|
return compileJsonSchema(withCustomRefTypes, this.name.tsType);
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Compiles the schema for usage in _request_ position.
|
|
20
|
+
*
|
|
21
|
+
* In contrast to {@link compile}, `format: date-time` strings are widened to
|
|
22
|
+
* `string | Date`, so that a JS `Date` may be passed instead of a
|
|
23
|
+
* hand-formatted ISO 8601 string. Refs to component schemas that have a
|
|
24
|
+
* widened request variant are redirected to that variant.
|
|
25
|
+
*
|
|
26
|
+
* This must never be used for response types: widening them would be a
|
|
27
|
+
* breaking change for consumers.
|
|
28
|
+
*/
|
|
29
|
+
async compileAsRequestInput(opts, dateTimeInputSchemaNames) {
|
|
30
|
+
const widened = widenDateTimeInputs(this.schemaObject);
|
|
31
|
+
const withCustomRefTypes = componentRefsToCustomTypes(opts.rootNamespace, widened, false, dateTimeInputRefTSNameResolver(dateTimeInputSchemaNames));
|
|
32
|
+
return compileJsonSchema(withCustomRefTypes, this.name.tsType);
|
|
33
|
+
}
|
|
16
34
|
clone() {
|
|
17
35
|
return new JSONSchema(new Name(this.name.raw), cloneDeep(this.schemaObject));
|
|
18
36
|
}
|
|
@@ -63,12 +63,13 @@ export class RequestParameters {
|
|
|
63
63
|
}
|
|
64
64
|
async compileTypes(opts) {
|
|
65
65
|
const header = this.header?.cloneWithOptionalProperties(opts.optionalHeaders ?? []);
|
|
66
|
+
const dateTimeInputs = this.operation.path.paths.model.dateTimeInputSchemaNames;
|
|
66
67
|
const t = {
|
|
67
68
|
ns: RequestParameters.ns,
|
|
68
|
-
path: (await this.path?.
|
|
69
|
-
body: (await this.body?.
|
|
70
|
-
header: (await header?.
|
|
71
|
-
query: (await this.query?.
|
|
69
|
+
path: (await this.path?.compileAsRequestInput(opts, dateTimeInputs)) ?? "",
|
|
70
|
+
body: (await this.body?.compileAsRequestInput(opts, dateTimeInputs)) ?? "",
|
|
71
|
+
header: (await header?.compileAsRequestInput(opts, dateTimeInputs)) ?? "",
|
|
72
|
+
query: (await this.query?.compileAsRequestInput(opts, dateTimeInputs)) ?? "",
|
|
72
73
|
};
|
|
73
74
|
return `\
|
|
74
75
|
namespace ${t.ns} {
|
|
@@ -8,7 +8,7 @@ const getComponentRef = (something) => {
|
|
|
8
8
|
return something.$ref;
|
|
9
9
|
}
|
|
10
10
|
};
|
|
11
|
-
export const componentRefsToCustomTypes = (rootNamespace, something, clone = true) => {
|
|
11
|
+
export const componentRefsToCustomTypes = (rootNamespace, something, clone = true, resolveTSName = refNameToTSName) => {
|
|
12
12
|
if (clone) {
|
|
13
13
|
something = cloneDeep(something);
|
|
14
14
|
}
|
|
@@ -16,18 +16,18 @@ export const componentRefsToCustomTypes = (rootNamespace, something, clone = tru
|
|
|
16
16
|
return something;
|
|
17
17
|
}
|
|
18
18
|
if (is.array(something)) {
|
|
19
|
-
return something.map((item) => componentRefsToCustomTypes(rootNamespace, item, false));
|
|
19
|
+
return something.map((item) => componentRefsToCustomTypes(rootNamespace, item, false, resolveTSName));
|
|
20
20
|
}
|
|
21
21
|
const componentRef = getComponentRef(something);
|
|
22
22
|
if (componentRef !== undefined) {
|
|
23
23
|
// see https://github.com/bcherny/json-schema-to-typescript#custom-schema-properties
|
|
24
24
|
return {
|
|
25
|
-
tsType:
|
|
25
|
+
tsType: resolveTSName(rootNamespace, componentRef),
|
|
26
26
|
type: "object",
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
return Object.fromEntries(Object.entries(something).map(([key, value]) => [
|
|
30
30
|
key,
|
|
31
|
-
componentRefsToCustomTypes(rootNamespace, value, false),
|
|
31
|
+
componentRefsToCustomTypes(rootNamespace, value, false, resolveTSName),
|
|
32
32
|
]));
|
|
33
33
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TypeScript type emitted for `string` schemas with `format: date-time` in
|
|
3
|
+
* request position.
|
|
4
|
+
*
|
|
5
|
+
* The parentheses are required: `json-schema-to-typescript` appends `[]` to the
|
|
6
|
+
* `tsType` of array items without wrapping it, so an unparenthesized union
|
|
7
|
+
* would compile to `string | Date[]` instead of `(string | Date)[]`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const dateTimeInputTsType = "(string | Date)";
|
|
10
|
+
/**
|
|
11
|
+
* Recursively widens every `{ type: "string", format: "date-time" }` sub-schema
|
|
12
|
+
* to `string | Date`, so that callers may pass a JS `Date` instead of a
|
|
13
|
+
* hand-formatted ISO 8601 string.
|
|
14
|
+
*
|
|
15
|
+
* This must only be applied to schemas in _request_ position: widening a
|
|
16
|
+
* response type would be a breaking change for consumers.
|
|
17
|
+
*/
|
|
18
|
+
export declare const widenDateTimeInputs: (something: unknown, clone?: boolean) => unknown;
|
|
19
|
+
/**
|
|
20
|
+
* Returns `true` if the given schema contains a `date-time` string _itself_,
|
|
21
|
+
* i.e. without following `$ref`s.
|
|
22
|
+
*/
|
|
23
|
+
export declare const containsDateTimeInput: (something: unknown) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RefTSNameResolver } from "../refs/componentRefsToCustomTypes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Namespace the widened request variants of component schemas are emitted to.
|
|
4
|
+
*
|
|
5
|
+
* A separate namespace (instead of a suffixed name inside `Schemas`) avoids
|
|
6
|
+
* collisions with schemas that happen to be named `…Request`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const requestSchemasNs = "requestSchemas";
|
|
9
|
+
/**
|
|
10
|
+
* Redirects refs to component schemas that have a widened request variant to
|
|
11
|
+
* that variant; all other refs are resolved as usual.
|
|
12
|
+
*/
|
|
13
|
+
export declare const dateTimeInputRefTSNameResolver: (variantNames: ReadonlySet<string>) => RefTSNameResolver;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
/**
|
|
3
|
+
* Determines for which `components.schemas` entries a widened _request variant_
|
|
4
|
+
* has to be generated.
|
|
5
|
+
*
|
|
6
|
+
* A variant is needed when the schema is reachable from a request body or a
|
|
7
|
+
* request parameter **and** it contains a `format: date-time` string, either
|
|
8
|
+
* directly or through one of its `$ref`s.
|
|
9
|
+
*
|
|
10
|
+
* Restricting this to request-reachable schemas keeps the generated output
|
|
11
|
+
* small; schemas that are only ever used in responses stay untouched, which is
|
|
12
|
+
* what keeps this change backwards compatible.
|
|
13
|
+
*/
|
|
14
|
+
export declare const dateTimeInputSchemaNames: (doc: OpenAPIV3.Document) => ReadonlySet<string>;
|
|
@@ -13,6 +13,11 @@ export declare class CodeGenerationModel {
|
|
|
13
13
|
readonly components: Components;
|
|
14
14
|
readonly tags: Tag[];
|
|
15
15
|
readonly doc: OpenAPIV3.Document;
|
|
16
|
+
/**
|
|
17
|
+
* Component schemas that need a widened request variant, because they are
|
|
18
|
+
* used in request position and contain a `format: date-time` string.
|
|
19
|
+
*/
|
|
20
|
+
readonly dateTimeInputSchemaNames: ReadonlySet<string>;
|
|
16
21
|
private constructor();
|
|
17
22
|
static fromDoc(rootNamespace: string, doc: OpenAPIV3.Document): CodeGenerationModel;
|
|
18
23
|
compileTypes(opts: TypeCompilationOptions): Promise<string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CodeGenerationModel, TypeCompilationOptions } from "../CodeGenerationModel.js";
|
|
2
2
|
import { Name } from "../global/Name.js";
|
|
3
3
|
import { Schemas } from "./Schemas.js";
|
|
4
|
+
import { RequestSchemas } from "./RequestSchemas.js";
|
|
4
5
|
import { Parameters } from "./Parameters.js";
|
|
5
6
|
import { RequestBodies } from "./RequestBodies.js";
|
|
6
7
|
import { Responses } from "./Responses.js";
|
|
@@ -13,6 +14,7 @@ export declare class Components {
|
|
|
13
14
|
static readonly ns = "Components";
|
|
14
15
|
name: Name;
|
|
15
16
|
schemas: Schemas;
|
|
17
|
+
requestSchemas: RequestSchemas;
|
|
16
18
|
securitySchemes: SecuritySchemes;
|
|
17
19
|
parameters: Parameters;
|
|
18
20
|
requestBodies: RequestBodies;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSONSchema } from "../global/JSONSchema.js";
|
|
2
|
+
import { Name } from "../global/Name.js";
|
|
3
|
+
import { Components } from "./Components.js";
|
|
4
|
+
import { TypeCompilationOptions } from "../CodeGenerationModel.js";
|
|
5
|
+
import { OpenAPIV3 } from "openapi-types";
|
|
6
|
+
/**
|
|
7
|
+
* Widened _request_ variants of component schemas.
|
|
8
|
+
*
|
|
9
|
+
* Only schemas that are reachable from a request body or request parameter and
|
|
10
|
+
* that (transitively) contain a `format: date-time` string get a variant here;
|
|
11
|
+
* see {@link dateTimeInputSchemaNames}. Everything else keeps referring to
|
|
12
|
+
* `Components.Schemas`.
|
|
13
|
+
*
|
|
14
|
+
* This namespace is purely additive: the original `Components.Schemas` types
|
|
15
|
+
* stay exactly as they were, which is what keeps response types – and therefore
|
|
16
|
+
* existing consumer code – unchanged.
|
|
17
|
+
*/
|
|
18
|
+
export declare class RequestSchemas {
|
|
19
|
+
static readonly ns: string;
|
|
20
|
+
readonly schemas: JSONSchema[];
|
|
21
|
+
readonly components: Components;
|
|
22
|
+
readonly name: Name;
|
|
23
|
+
constructor(components: Components, schemas: OpenAPIV3.ComponentsObject["schemas"], variantNames: ReadonlySet<string>);
|
|
24
|
+
compileTypes(opts: TypeCompilationOptions): Promise<string>;
|
|
25
|
+
}
|
|
@@ -6,6 +6,18 @@ export declare class JSONSchema {
|
|
|
6
6
|
readonly name: Name;
|
|
7
7
|
constructor(name: Name, data?: JSONSchemaObject);
|
|
8
8
|
compile(opts: TypeCompilationOptions): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Compiles the schema for usage in _request_ position.
|
|
11
|
+
*
|
|
12
|
+
* In contrast to {@link compile}, `format: date-time` strings are widened to
|
|
13
|
+
* `string | Date`, so that a JS `Date` may be passed instead of a
|
|
14
|
+
* hand-formatted ISO 8601 string. Refs to component schemas that have a
|
|
15
|
+
* widened request variant are redirected to that variant.
|
|
16
|
+
*
|
|
17
|
+
* This must never be used for response types: widening them would be a
|
|
18
|
+
* breaking change for consumers.
|
|
19
|
+
*/
|
|
20
|
+
compileAsRequestInput(opts: TypeCompilationOptions, dateTimeInputSchemaNames: ReadonlySet<string>): Promise<string>;
|
|
9
21
|
clone(): JSONSchema;
|
|
10
22
|
cloneWithOptionalProperties(optionalProperties: string[]): JSONSchema;
|
|
11
23
|
private setOptionalPropertiesInSchema;
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a `#/components/…` ref to the TypeScript type it should be compiled
|
|
3
|
+
* to. Used to redirect refs to the widened request variant of a component
|
|
4
|
+
* schema.
|
|
5
|
+
*/
|
|
6
|
+
export type RefTSNameResolver = (rootNamespace: string, $ref: string) => string;
|
|
7
|
+
export declare const componentRefsToCustomTypes: (rootNamespace: string, something: unknown, clone?: boolean, resolveTSName?: RefTSNameResolver) => unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/api-code-generator",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.468.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": "https://github.com/mittwald/api-client-js.git",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"@oclif/plugin-plugins"
|
|
85
85
|
]
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "83c2cdc00238d69632f94b07a4b38b80142f733a"
|
|
88
88
|
}
|