@appweaver/client 1.3.0 → 1.3.1
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/cjs/commands/generate-command.js +3 -1
- package/cjs/constants.d.ts +9 -0
- package/cjs/constants.js +28 -1
- package/cjs/generators/generate-types.d.ts +3 -1
- package/cjs/generators/generate-types.js +99 -9
- package/cjs/types/generator.d.ts +6 -0
- package/cjs/types/generator.js +2 -0
- package/cjs/types/index.d.ts +1 -0
- package/cjs/types/index.js +1 -0
- package/cjs/utils/enum-util.d.ts +17 -17
- package/cjs/utils/enum-util.js +50 -170
- package/cjs/utils/hoist-util.d.ts +29 -0
- package/cjs/utils/hoist-util.js +338 -0
- package/cjs/utils/index.d.ts +1 -0
- package/cjs/utils/index.js +1 -0
- package/esm/commands/generate-command.js +3 -1
- package/esm/constants.d.ts +9 -0
- package/esm/constants.js +27 -0
- package/esm/generators/generate-types.d.ts +3 -1
- package/esm/generators/generate-types.js +100 -10
- package/esm/types/generator.d.ts +6 -0
- package/esm/types/generator.js +1 -0
- package/esm/types/index.d.ts +1 -0
- package/esm/types/index.js +1 -0
- package/esm/utils/enum-util.d.ts +17 -17
- package/esm/utils/enum-util.js +49 -169
- package/esm/utils/hoist-util.d.ts +29 -0
- package/esm/utils/hoist-util.js +335 -0
- package/esm/utils/index.d.ts +1 -0
- package/esm/utils/index.js +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hoistSharedTypes = hoistSharedTypes;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
/** Keys documenting a schema node rather than describing its structure. They are kept on the
|
|
6
|
+
* property the node is hoisted out of, so no property loses its description along the way. */
|
|
7
|
+
const ANNOTATION_KEYS = ['description', 'example', 'examples', 'deprecated'];
|
|
8
|
+
/**
|
|
9
|
+
* Hoists the schemas an OpenAPI document repeats inline across its definitions into shared
|
|
10
|
+
* definitions of their own, replacing every occurrence with a reference to the hoisted
|
|
11
|
+
* definition.
|
|
12
|
+
*
|
|
13
|
+
* An Appweaver schema declares the same shapes over and over, once per property that accepts
|
|
14
|
+
* them, and each of them would otherwise be spelled out again in the generated types (i.e. the
|
|
15
|
+
* `asc | desc` enum of every sortable field, or the accepted value of every filterable field of
|
|
16
|
+
* every resource). Two kinds of schema are hoisted:
|
|
17
|
+
*
|
|
18
|
+
* - The well known shapes of {@link SHARED_SCHEMA_SHAPES}, matched in the order they are
|
|
19
|
+
* declared so an outer shape is hoisted before the shapes nested inside it.
|
|
20
|
+
* - The enums the definitions repeat, named after {@link SHARED_ENUM_NAMES} when their values
|
|
21
|
+
* are well known, and otherwise after the part the declaring definitions have in common
|
|
22
|
+
* followed by the property name (i.e. `PostCreate` and `PostSingle` declaring `status` give
|
|
23
|
+
* `PostStatus`). Enums whose name cannot be resolved are left inline.
|
|
24
|
+
*
|
|
25
|
+
* Only the structure of a schema decides whether it is hoisted, and every occurrence keeps the
|
|
26
|
+
* documentation it carries, so no property loses its description or example along the way.
|
|
27
|
+
*
|
|
28
|
+
* A schema declared only once, or whose name is already taken, is left inline as well.
|
|
29
|
+
*
|
|
30
|
+
* The given schema is mutated in place.
|
|
31
|
+
*
|
|
32
|
+
* @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared definitions of.
|
|
33
|
+
* @return {string[]} The names of the hoisted definitions, in the order they were created.
|
|
34
|
+
*/
|
|
35
|
+
function hoistSharedTypes(schema) {
|
|
36
|
+
const definitions = schema.components?.schemas;
|
|
37
|
+
if (!definitions) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const takenNames = new Set(Object.keys(definitions));
|
|
41
|
+
for (const definition of Object.values(definitions)) {
|
|
42
|
+
const title = definition?.['title'];
|
|
43
|
+
if (typeof title === 'string') {
|
|
44
|
+
takenNames.add(title);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const hoistedNames = [];
|
|
48
|
+
for (const { name, schema: shape } of constants_1.SHARED_SCHEMA_SHAPES) {
|
|
49
|
+
if (hoistShape(definitions, name, shape, takenNames)) {
|
|
50
|
+
hoistedNames.push(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
hoistedNames.push(...hoistEnums(definitions, takenNames));
|
|
54
|
+
return hoistedNames;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Hoists every occurrence of a well known shape into a definition of the given name. The
|
|
58
|
+
* definition is built from the first occurrence, so it holds the references the shape declares
|
|
59
|
+
* by title as the references of the document.
|
|
60
|
+
*
|
|
61
|
+
* @param {Record<string, unknown>} definitions The definitions of the schema.
|
|
62
|
+
* @param {string} name The name of the definition the shape is hoisted into.
|
|
63
|
+
* @param {unknown} shape The shape to hoist, holding its references as definition titles.
|
|
64
|
+
* @param {Set<string>} takenNames The definition names already in use.
|
|
65
|
+
* @return {boolean} Whether the shape was hoisted.
|
|
66
|
+
*/
|
|
67
|
+
function hoistShape(definitions, name, shape, takenNames) {
|
|
68
|
+
const titles = definitionTitles(definitions);
|
|
69
|
+
const signature = schemaSignature(shape, titles);
|
|
70
|
+
const groups = collectOccurrences(definitions, (node) => schemaSignature(structure(node), titles) === signature ? name : undefined);
|
|
71
|
+
const occurrences = groups.get(name);
|
|
72
|
+
if (!occurrences || occurrences.length < 2 || takenNames.has(name)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
takenNames.add(name);
|
|
76
|
+
const { container, key } = occurrences[0];
|
|
77
|
+
definitions[name] = {
|
|
78
|
+
title: name,
|
|
79
|
+
...structure(container[key])
|
|
80
|
+
};
|
|
81
|
+
for (const occurrence of occurrences) {
|
|
82
|
+
replaceWithReference(occurrence, name);
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Hoists the enums the definitions repeat into shared definitions named after their values.
|
|
88
|
+
*
|
|
89
|
+
* @param {Record<string, unknown>} definitions The definitions of the schema.
|
|
90
|
+
* @param {Set<string>} takenNames The definition names already in use.
|
|
91
|
+
* @return {string[]} The names of the hoisted definitions, in the order they were created.
|
|
92
|
+
*/
|
|
93
|
+
function hoistEnums(definitions, takenNames) {
|
|
94
|
+
// Enums declared by identical schema content, keyed by that content. The
|
|
95
|
+
// documentation of a node is left out of the key, since every occurrence keeps
|
|
96
|
+
// the documentation of its own when it is replaced by a reference
|
|
97
|
+
const groups = collectOccurrences(definitions, (node) => Array.isArray(node['enum']) ? stableStringify(structure(node)) : undefined);
|
|
98
|
+
const hoistedNames = [];
|
|
99
|
+
for (const [content, occurrences] of groups) {
|
|
100
|
+
if (occurrences.length < 2) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const name = resolveEnumName(JSON.parse(content), occurrences);
|
|
104
|
+
if (!name || takenNames.has(name)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
takenNames.add(name);
|
|
108
|
+
hoistedNames.push(name);
|
|
109
|
+
definitions[name] = { title: name, ...JSON.parse(content) };
|
|
110
|
+
for (const occurrence of occurrences) {
|
|
111
|
+
replaceWithReference(occurrence, name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return hoistedNames;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Replaces the schema node of an occurrence with a reference to the given definition, keeping
|
|
118
|
+
* the documentation the node carries.
|
|
119
|
+
*
|
|
120
|
+
* @param {Occurrence} occurrence The occurrence to replace.
|
|
121
|
+
* @param {string} name The name of the definition to reference.
|
|
122
|
+
*/
|
|
123
|
+
function replaceWithReference({ container, key }, name) {
|
|
124
|
+
const node = container[key];
|
|
125
|
+
container[key] = {
|
|
126
|
+
...annotations(node),
|
|
127
|
+
$ref: `#/components/schemas/${name}`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Collects every inline schema the matcher accepts into a map of occurrences, grouped by the
|
|
132
|
+
* key the matcher returns for them. A node the matcher accepts is not descended into, so an
|
|
133
|
+
* outer match wins over the nodes nested inside it.
|
|
134
|
+
*
|
|
135
|
+
* @param {Record<string, unknown>} definitions The definitions of the schema.
|
|
136
|
+
* @param {Matcher} match The matcher deciding which nodes are collected.
|
|
137
|
+
* @return {Map<string, Occurrence[]>} The collected occurrences, grouped by matcher key.
|
|
138
|
+
*/
|
|
139
|
+
function collectOccurrences(definitions, match) {
|
|
140
|
+
const groups = new Map();
|
|
141
|
+
for (const [key, definition] of Object.entries(definitions)) {
|
|
142
|
+
if (definition && typeof definition === 'object') {
|
|
143
|
+
const owner = definition['title'];
|
|
144
|
+
collect(definition, typeof owner === 'string' ? owner : key, '', match, groups);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return groups;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Collects every inline schema declared below the given schema node into the groups map.
|
|
151
|
+
*
|
|
152
|
+
* @param {SchemaContainer} container The schema node to collect the schemas of.
|
|
153
|
+
* @param {string} owner The name of the definition the node belongs to.
|
|
154
|
+
* @param {string} property The name of the property the node is declared under, if any.
|
|
155
|
+
* @param {Matcher} match The matcher deciding which nodes are collected.
|
|
156
|
+
* @param {Map<string, Occurrence[]>} groups The collected occurrences, grouped by matcher key.
|
|
157
|
+
*/
|
|
158
|
+
function collect(container, owner, property, match, groups) {
|
|
159
|
+
const entries = Array.isArray(container)
|
|
160
|
+
? container.map((value, index) => [index, value])
|
|
161
|
+
: Object.entries(container);
|
|
162
|
+
for (const [key, value] of entries) {
|
|
163
|
+
if (!value || typeof value !== 'object') {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// The keys of a `properties` object name the schemas below them, everything
|
|
167
|
+
// else keeps the property name of the node it was reached through
|
|
168
|
+
if (key === 'properties' && !Array.isArray(value)) {
|
|
169
|
+
const properties = value;
|
|
170
|
+
for (const name of Object.keys(properties)) {
|
|
171
|
+
visit(properties, name, owner, name, match, groups);
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
visit(container, key, owner, property, match, groups);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Records the schema held by the container under the given key as an occurrence, or descends
|
|
180
|
+
* into it when the matcher does not accept it.
|
|
181
|
+
*
|
|
182
|
+
* @param {SchemaContainer} container The container holding the schema node.
|
|
183
|
+
* @param {string | number} key The key the schema node is held under.
|
|
184
|
+
* @param {string} owner The name of the definition the node belongs to.
|
|
185
|
+
* @param {string} property The name of the property the node is declared under, if any.
|
|
186
|
+
* @param {Matcher} match The matcher deciding which nodes are collected.
|
|
187
|
+
* @param {Map<string, Occurrence[]>} groups The collected occurrences, grouped by matcher key.
|
|
188
|
+
*/
|
|
189
|
+
function visit(container, key, owner, property, match, groups) {
|
|
190
|
+
const node = container[key];
|
|
191
|
+
const group = match(node);
|
|
192
|
+
if (group !== undefined) {
|
|
193
|
+
const occurrences = groups.get(group) ?? [];
|
|
194
|
+
occurrences.push({ container, key, owner, property });
|
|
195
|
+
groups.set(group, occurrences);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
collect(node, owner, property, match, groups);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Resolves the name of the definition an enum is hoisted into, preferring the name given to
|
|
202
|
+
* its values by {@link SHARED_ENUM_NAMES} and falling back to the common part of the names of
|
|
203
|
+
* the definitions declaring it, followed by the property name they all declare it under.
|
|
204
|
+
*
|
|
205
|
+
* @param {SchemaNode} node The enum schema to resolve the name of.
|
|
206
|
+
* @param {Occurrence[]} occurrences The places the enum is declared in.
|
|
207
|
+
* @return {string | undefined} The resolved name, or undefined when the enum has no name to
|
|
208
|
+
* be hoisted under.
|
|
209
|
+
*/
|
|
210
|
+
function resolveEnumName(node, occurrences) {
|
|
211
|
+
const values = node['enum'];
|
|
212
|
+
const knownName = constants_1.SHARED_ENUM_NAMES[values.join('|')];
|
|
213
|
+
if (knownName) {
|
|
214
|
+
return knownName;
|
|
215
|
+
}
|
|
216
|
+
const { property } = occurrences[0];
|
|
217
|
+
if (!property || occurrences.some((o) => o.property !== property)) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const prefix = commonNamePrefix(occurrences.map((o) => o.owner));
|
|
221
|
+
if (!prefix) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return prefix + property.charAt(0).toUpperCase() + property.slice(1);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Resolves the longest prefix the given names share, cut at a word boundary so the result
|
|
228
|
+
* stays a readable name (i.e. `HealthCheckResponse` and `HealthCheckResult` give
|
|
229
|
+
* `HealthCheck` rather than `HealthCheckRes`).
|
|
230
|
+
*
|
|
231
|
+
* @param {string[]} names The names to find the common prefix of.
|
|
232
|
+
* @return {string} The common prefix, empty when the names start with different words.
|
|
233
|
+
*/
|
|
234
|
+
function commonNamePrefix(names) {
|
|
235
|
+
const wordsPerName = names.map((name) => name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? []);
|
|
236
|
+
const words = [];
|
|
237
|
+
for (let i = 0; i < wordsPerName[0].length; i++) {
|
|
238
|
+
const word = wordsPerName[0][i];
|
|
239
|
+
if (wordsPerName.some((other) => other[i] !== word)) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
words.push(word);
|
|
243
|
+
}
|
|
244
|
+
return words.join('');
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Maps every definition key to the title it is generated under, so a reference can be compared
|
|
248
|
+
* by the name it resolves to rather than by the generated key holding it.
|
|
249
|
+
*
|
|
250
|
+
* @param {Record<string, unknown>} definitions The definitions of the schema.
|
|
251
|
+
* @return {Map<string, string>} The title of every definition, keyed by its definition key.
|
|
252
|
+
*/
|
|
253
|
+
function definitionTitles(definitions) {
|
|
254
|
+
const titles = new Map();
|
|
255
|
+
for (const [key, definition] of Object.entries(definitions)) {
|
|
256
|
+
const title = definition?.['title'];
|
|
257
|
+
titles.set(key, typeof title === 'string' ? title : key);
|
|
258
|
+
}
|
|
259
|
+
return titles;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Returns the documentation keys of a schema node, which the node keeps when it is replaced by
|
|
263
|
+
* a reference to a hoisted definition.
|
|
264
|
+
*
|
|
265
|
+
* @param {SchemaNode} node The schema node to take the documentation of.
|
|
266
|
+
* @return {SchemaNode} The documentation keys of the node.
|
|
267
|
+
*/
|
|
268
|
+
function annotations(node) {
|
|
269
|
+
return Object.fromEntries(Object.entries(node).filter(([key]) => ANNOTATION_KEYS.includes(key)));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Returns the structural keys of a schema node, so two nodes describing the same shape under a
|
|
273
|
+
* different description compare alike.
|
|
274
|
+
*
|
|
275
|
+
* @param {SchemaNode} node The schema node to take the structure of.
|
|
276
|
+
* @return {SchemaNode} The structural keys of the node.
|
|
277
|
+
*/
|
|
278
|
+
function structure(node) {
|
|
279
|
+
return Object.fromEntries(Object.entries(node).filter(([key]) => !ANNOTATION_KEYS.includes(key)));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Serializes a schema with its references resolved to the title of the definition they point
|
|
283
|
+
* to, so a shape declared by {@link SHARED_SCHEMA_SHAPES} compares equal to the same shape of a
|
|
284
|
+
* document, whose references name the generated definition keys instead.
|
|
285
|
+
*
|
|
286
|
+
* @param {unknown} value The schema to serialize.
|
|
287
|
+
* @param {Map<string, string>} titles The title of every definition, keyed by definition key.
|
|
288
|
+
* @return {string} The stable serialization of the schema.
|
|
289
|
+
*/
|
|
290
|
+
function schemaSignature(value, titles) {
|
|
291
|
+
if (Array.isArray(value)) {
|
|
292
|
+
return `[${value.map((item) => schemaSignature(item, titles)).join(',')}]`;
|
|
293
|
+
}
|
|
294
|
+
if (value && typeof value === 'object') {
|
|
295
|
+
const entries = Object.keys(value)
|
|
296
|
+
.sort()
|
|
297
|
+
.map((key) => {
|
|
298
|
+
const nested = value[key];
|
|
299
|
+
const resolved = key === '$ref' && typeof nested === 'string'
|
|
300
|
+
? JSON.stringify(referenceTitle(nested, titles))
|
|
301
|
+
: schemaSignature(nested, titles);
|
|
302
|
+
return `${JSON.stringify(key)}:${resolved}`;
|
|
303
|
+
});
|
|
304
|
+
return `{${entries.join(',')}}`;
|
|
305
|
+
}
|
|
306
|
+
return JSON.stringify(value) ?? 'null';
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Resolves a reference to the title of the definition it points to.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} reference The reference to resolve (i.e. `#/components/schemas/def-1`).
|
|
312
|
+
* @param {Map<string, string>} titles The title of every definition, keyed by definition key.
|
|
313
|
+
* @return {string} The title of the referenced definition, or the reference when it points
|
|
314
|
+
* outside of the definitions of the document.
|
|
315
|
+
*/
|
|
316
|
+
function referenceTitle(reference, titles) {
|
|
317
|
+
const key = reference.replace('#/components/schemas/', '');
|
|
318
|
+
return titles.get(key) ?? reference;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Serializes a value with its object keys sorted, so two schemas holding the same content
|
|
322
|
+
* in a different key order serialize alike.
|
|
323
|
+
*
|
|
324
|
+
* @param {unknown} value The value to serialize.
|
|
325
|
+
* @return {string} The stable JSON serialization of the value.
|
|
326
|
+
*/
|
|
327
|
+
function stableStringify(value) {
|
|
328
|
+
if (Array.isArray(value)) {
|
|
329
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
330
|
+
}
|
|
331
|
+
if (value && typeof value === 'object') {
|
|
332
|
+
const entries = Object.keys(value)
|
|
333
|
+
.sort()
|
|
334
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
335
|
+
return `{${entries.join(',')}}`;
|
|
336
|
+
}
|
|
337
|
+
return JSON.stringify(value) ?? 'null';
|
|
338
|
+
}
|
package/cjs/utils/index.d.ts
CHANGED
package/cjs/utils/index.js
CHANGED
|
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./enum-util"), exports);
|
|
18
|
+
__exportStar(require("./hoist-util"), exports);
|
|
18
19
|
__exportStar(require("./schema-util"), exports);
|
|
@@ -33,7 +33,9 @@ export function generateCommand(program) {
|
|
|
33
33
|
const schemaContent = await readSchemaContent(schemaPath);
|
|
34
34
|
const schemaObject = await toSchemaObject(schemaContent);
|
|
35
35
|
if (!clientOnly && !noTypes) {
|
|
36
|
-
const typesContent = await generateTypes(schemaObject
|
|
36
|
+
const typesContent = await generateTypes(schemaObject, {
|
|
37
|
+
declaration: typesPath.endsWith('.d.ts')
|
|
38
|
+
});
|
|
37
39
|
await formatAndWriteFile(typesPath, typesContent, typesOnly || typesPath !== clientPath);
|
|
38
40
|
console.log(`Generated types to ${path.relative(cwd, typesPath)}`);
|
|
39
41
|
}
|
package/esm/constants.d.ts
CHANGED
|
@@ -8,6 +8,15 @@ export declare const CONFIG_RESOURCE_FIELD = "x-appweaver-resource";
|
|
|
8
8
|
* The key is the enum values joined by a `|`; the value is the name of the shared type generated
|
|
9
9
|
* for them. Enums that are not listed here are named after the definitions declaring them. */
|
|
10
10
|
export declare const SHARED_ENUM_NAMES: Record<string, string>;
|
|
11
|
+
/** Shapes an Appweaver schema repeats inline across its definitions, and the name of the shared
|
|
12
|
+
* definition each of them is hoisted into. A `$ref` names the title of the definition it points
|
|
13
|
+
* to, since the keys the definitions are generated under vary per document. The shapes are
|
|
14
|
+
* matched in the order they are declared, so an outer shape is hoisted before the shapes nested
|
|
15
|
+
* inside it (i.e. `QueryFilterValue` before the `QueryFilterScalar` it is built from). */
|
|
16
|
+
export declare const SHARED_SCHEMA_SHAPES: {
|
|
17
|
+
name: string;
|
|
18
|
+
schema: unknown;
|
|
19
|
+
}[];
|
|
11
20
|
/** Suffix used when generating the TypeScript module type name for a resource. */
|
|
12
21
|
export declare const RESOURCE_MODULE_TYPE = "ResourceModuleType";
|
|
13
22
|
/** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
|
package/esm/constants.js
CHANGED
|
@@ -13,6 +13,33 @@ export const CONFIG_RESOURCE_FIELD = 'x-appweaver-resource';
|
|
|
13
13
|
export const SHARED_ENUM_NAMES = {
|
|
14
14
|
'asc|desc': 'SortDirection'
|
|
15
15
|
};
|
|
16
|
+
/** The primitive types a plain filter value takes, as an Appweaver schema declares them. */
|
|
17
|
+
const FILTER_SCALAR = {
|
|
18
|
+
anyOf: [
|
|
19
|
+
{ type: 'string' },
|
|
20
|
+
{ type: 'number' },
|
|
21
|
+
{ type: 'boolean' },
|
|
22
|
+
{ type: 'null' }
|
|
23
|
+
]
|
|
24
|
+
};
|
|
25
|
+
/** Shapes an Appweaver schema repeats inline across its definitions, and the name of the shared
|
|
26
|
+
* definition each of them is hoisted into. A `$ref` names the title of the definition it points
|
|
27
|
+
* to, since the keys the definitions are generated under vary per document. The shapes are
|
|
28
|
+
* matched in the order they are declared, so an outer shape is hoisted before the shapes nested
|
|
29
|
+
* inside it (i.e. `QueryFilterValue` before the `QueryFilterScalar` it is built from). */
|
|
30
|
+
export const SHARED_SCHEMA_SHAPES = [
|
|
31
|
+
{
|
|
32
|
+
name: 'QueryFilterValue',
|
|
33
|
+
schema: {
|
|
34
|
+
anyOf: [
|
|
35
|
+
FILTER_SCALAR,
|
|
36
|
+
{ type: 'array', items: FILTER_SCALAR },
|
|
37
|
+
{ $ref: 'QueryCondition' }
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{ name: 'QueryFilterScalar', schema: FILTER_SCALAR }
|
|
42
|
+
];
|
|
16
43
|
/** Suffix used when generating the TypeScript module type name for a resource. */
|
|
17
44
|
export const RESOURCE_MODULE_TYPE = 'ResourceModuleType';
|
|
18
45
|
/** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { OpenAPI3 } from 'openapi-typescript';
|
|
2
|
+
import { GenerateTypesOptions } from '../types/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Generates TypeScript types based on an OpenAPI V3 schema content.
|
|
4
5
|
*
|
|
5
6
|
* @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
|
|
6
7
|
* representing JSON or YAML format, or an already parsed OpenAPI3 object.
|
|
8
|
+
* @param {GenerateTypesOptions} [options] - Options controlling how the types are emitted.
|
|
7
9
|
* @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
|
|
8
10
|
*/
|
|
9
|
-
export declare function generateTypes(schema: string | OpenAPI3): Promise<string>;
|
|
11
|
+
export declare function generateTypes(schema: string | OpenAPI3, options?: GenerateTypesOptions): Promise<string>;
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import openapiTS, { astToString } from 'openapi-typescript';
|
|
2
2
|
import ts from 'typescript';
|
|
3
|
-
import {
|
|
3
|
+
import { hoistSharedTypes, rewriteEnumsAsObjects, toSchemaObject } from '../utils/index.js';
|
|
4
4
|
import { ACCOUNT_MODULE_TYPE, ACCOUNT_TYPES, AUTH_MODULE_TYPE, AUTH_TYPES, CONFIG_FIELD, HEALTH_MODULE_TYPE, HEALTH_TYPES, RESOURCE_MODULE_TYPE, RESOURCE_TYPES } from '../constants.js';
|
|
5
5
|
/**
|
|
6
6
|
* Generates TypeScript types based on an OpenAPI V3 schema content.
|
|
7
7
|
*
|
|
8
8
|
* @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
|
|
9
9
|
* representing JSON or YAML format, or an already parsed OpenAPI3 object.
|
|
10
|
+
* @param {GenerateTypesOptions} [options] - Options controlling how the types are emitted.
|
|
10
11
|
* @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
|
|
11
12
|
*/
|
|
12
|
-
export async function generateTypes(schema) {
|
|
13
|
-
// The schema is copied before the shared
|
|
14
|
-
// caller keeps the schema it passed in unchanged
|
|
13
|
+
export async function generateTypes(schema, options = {}) {
|
|
14
|
+
// The schema is copied before the shared definitions are hoisted into it, so
|
|
15
|
+
// the caller keeps the schema it passed in unchanged
|
|
15
16
|
const schemaObject = typeof schema === 'string'
|
|
16
17
|
? await toSchemaObject(schema)
|
|
17
18
|
: structuredClone(schema);
|
|
18
|
-
const
|
|
19
|
+
const sharedTypes = hoistSharedTypes(schemaObject);
|
|
19
20
|
const ast = await openapiTS(schemaObject, {
|
|
20
21
|
exportType: true,
|
|
21
22
|
emptyObjectsUnknown: true,
|
|
@@ -52,10 +53,11 @@ export async function generateTypes(schema) {
|
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
55
|
let typesContent = astToString(deduplicateUnionConstituents(ast));
|
|
55
|
-
typesContent = extractSchemaTypes(typesContent,
|
|
56
|
+
typesContent = extractSchemaTypes(typesContent, sharedTypes);
|
|
56
57
|
typesContent = combineModuleTypes(typesContent, schemaObject);
|
|
57
58
|
typesContent = deduplicateExportedTypes(typesContent);
|
|
58
|
-
|
|
59
|
+
typesContent = replaceFileUploadTypes(typesContent);
|
|
60
|
+
return rewriteEnumsAsObjects(typesContent, options.declaration);
|
|
59
61
|
}
|
|
60
62
|
/**
|
|
61
63
|
* Extracts inline schema types from the generated `schemas` block.
|
|
@@ -69,12 +71,16 @@ export async function generateTypes(schema) {
|
|
|
69
71
|
*
|
|
70
72
|
* And replaces the inline body in `schemas` with a reference to the new type.
|
|
71
73
|
*
|
|
74
|
+
* The definitions holding the schemas shared between the other definitions are lifted the
|
|
75
|
+
* same way, whether they hold an object or, as the hoisted enums and filter values do, a
|
|
76
|
+
* union of their own.
|
|
77
|
+
*
|
|
72
78
|
* @param {string} typeContent - The generated TypeScript type content as a string.
|
|
73
|
-
* @param {string[]}
|
|
79
|
+
* @param {string[]} sharedTypes - The names of the definitions holding the schemas shared
|
|
74
80
|
* between the other definitions, whose references are replaced by the name alone.
|
|
75
81
|
* @return {string} The transformed types content with extracted schema types.
|
|
76
82
|
*/
|
|
77
|
-
function extractSchemaTypes(typeContent,
|
|
83
|
+
function extractSchemaTypes(typeContent, sharedTypes = []) {
|
|
78
84
|
const extractedTypes = [];
|
|
79
85
|
const typeNames = new Set();
|
|
80
86
|
const entries = [];
|
|
@@ -140,8 +146,11 @@ function extractSchemaTypes(typeContent, sharedEnums = []) {
|
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
updatedBaseContent += normalizedTypes.slice(cursor);
|
|
149
|
+
// Lift the shared definitions holding a union rather than an object, which the
|
|
150
|
+
// entries above leave in place (i.e. `QueryFilterValue: QueryFilterScalar | ...;`)
|
|
151
|
+
updatedBaseContent = extractSharedTypes(updatedBaseContent, sharedTypes, extractedTypes, typeNames);
|
|
143
152
|
// Replace all cross-references like components["schemas"]["def-89"] with the type name
|
|
144
|
-
const references = new Map(
|
|
153
|
+
const references = new Map(sharedTypes.map((name) => [`"${name}"`, name]));
|
|
145
154
|
for (const [defKey, typeName] of defToTypeName) {
|
|
146
155
|
references.set(defKey, typeName);
|
|
147
156
|
}
|
|
@@ -176,6 +185,87 @@ function extractSchemaTypes(typeContent, sharedEnums = []) {
|
|
|
176
185
|
const mergedExtractedTypes = stripFormat(joined.replace(doubleCommentsRegex, '$1*'));
|
|
177
186
|
return updatedBaseContent + '\n' + mergedExtractedTypes;
|
|
178
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Lifts the shared definitions holding a type of their own out of the generated `schemas`
|
|
190
|
+
* block, so the type they hold is declared once and referenced by name everywhere else.
|
|
191
|
+
*
|
|
192
|
+
* Finds entries like:
|
|
193
|
+
* QueryFilterValue: QueryFilterScalar | QueryFilterScalar[] | QueryCondition;
|
|
194
|
+
*
|
|
195
|
+
* Lifts each one into:
|
|
196
|
+
* export type QueryFilterValue = QueryFilterScalar | QueryFilterScalar[] | QueryCondition;
|
|
197
|
+
*
|
|
198
|
+
* The definitions holding nothing but a reference to a type declared elsewhere, as the hoisted
|
|
199
|
+
* enums do, are left alone.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} content - The generated types, with the object definitions already extracted.
|
|
202
|
+
* @param {string[]} names - The names of the shared definitions to lift.
|
|
203
|
+
* @param {string[]} extractedTypes - The extracted types, appended to for every lifted entry.
|
|
204
|
+
* @param {Set<string>} typeNames - The names already extracted, added to for every lifted entry.
|
|
205
|
+
* @return {string} The types with the body of every lifted entry replaced by its name.
|
|
206
|
+
*/
|
|
207
|
+
function extractSharedTypes(content, names, extractedTypes, typeNames) {
|
|
208
|
+
// The definitions live in the components block, so a property named after one of them
|
|
209
|
+
// elsewhere in the types is never mistaken for its declaration
|
|
210
|
+
const componentsStart = content.indexOf('export type components');
|
|
211
|
+
if (componentsStart < 0) {
|
|
212
|
+
return content;
|
|
213
|
+
}
|
|
214
|
+
for (const name of names) {
|
|
215
|
+
if (typeNames.has(name)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// The entry starts on a line of its own, optionally preceded by the JSDoc header
|
|
219
|
+
// holding the name of the definition
|
|
220
|
+
const entry = new RegExp(String.raw `\n[ \t]*(?:\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*)?${name}: `).exec(content.slice(componentsStart));
|
|
221
|
+
if (!entry) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const start = componentsStart + entry.index + entry[0].length;
|
|
225
|
+
const end = findTypeEnd(content, start);
|
|
226
|
+
const body = content.slice(start, end).trim();
|
|
227
|
+
// A definition referencing a type declared elsewhere holds nothing to lift
|
|
228
|
+
if (!body || body === name) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
typeNames.add(name);
|
|
232
|
+
extractedTypes.push(`export type ${name} = ${body};\n`);
|
|
233
|
+
content = content.slice(0, start) + name + content.slice(end);
|
|
234
|
+
}
|
|
235
|
+
return content;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Finds the end of the type starting at the given index, which is the first semicolon that is
|
|
239
|
+
* not nested inside braces, brackets, parentheses or a string literal.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} content - The content holding the type.
|
|
242
|
+
* @param {number} start - The index the type starts at.
|
|
243
|
+
* @return {number} The index of the semicolon terminating the type.
|
|
244
|
+
*/
|
|
245
|
+
function findTypeEnd(content, start) {
|
|
246
|
+
const closing = { '{': '}', '[': ']', '(': ')' };
|
|
247
|
+
const stack = [];
|
|
248
|
+
for (let i = start; i < content.length; i++) {
|
|
249
|
+
const character = content[i];
|
|
250
|
+
if (character === '"' || character === "'") {
|
|
251
|
+
i = content.indexOf(character, i + 1);
|
|
252
|
+
if (i < 0) {
|
|
253
|
+
return content.length;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (closing[character]) {
|
|
258
|
+
stack.push(closing[character]);
|
|
259
|
+
}
|
|
260
|
+
else if (character === stack[stack.length - 1]) {
|
|
261
|
+
stack.pop();
|
|
262
|
+
}
|
|
263
|
+
else if (character === ';' && stack.length === 0) {
|
|
264
|
+
return i;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return content.length;
|
|
268
|
+
}
|
|
179
269
|
/**
|
|
180
270
|
* Deduplicates and simplifies union type constituents in the provided TypeScript Abstract Syntax Tree (AST).
|
|
181
271
|
* Ensures that duplicate constituents are removed and shared types across parenthesized union branches are hoisted.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Options controlling how the TypeScript types are generated from an OpenAPI schema. */
|
|
2
|
+
export type GenerateTypesOptions = {
|
|
3
|
+
/** Whether the types are written into a declaration (`.d.ts`) file, which holds no runtime
|
|
4
|
+
* values and so declares the enum constants instead of initializing them. (default: false) */
|
|
5
|
+
declaration?: boolean;
|
|
6
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/esm/types/index.d.ts
CHANGED
package/esm/types/index.js
CHANGED
package/esm/utils/enum-util.d.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { OpenAPI3 } from 'openapi-typescript';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
* of their
|
|
2
|
+
* Rewrites the TypeScript enums of the generated types into a constant object holding the
|
|
3
|
+
* members and a type alias of their values, so both forms are accepted wherever the enum is
|
|
4
|
+
* used (i.e. `SortDirection.asc` and the plain `'asc'` literal alike).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A TypeScript `enum` declares a nominal type, which rejects the very literals it is built
|
|
7
|
+
* from, forcing users of the generated client to import the enum for a value the API
|
|
8
|
+
* documents as a string. The rewritten declaration keeps the member access working while
|
|
9
|
+
* typing the property as the union of its values:
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
|
|
15
|
-
* resolved, or whose name is already taken, are left inline.
|
|
11
|
+
* ```ts
|
|
12
|
+
* export const SortDirection = { asc: 'asc', desc: 'desc' } as const;
|
|
13
|
+
* export type SortDirection = (typeof SortDirection)[keyof typeof SortDirection];
|
|
14
|
+
* ```
|
|
16
15
|
*
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* @param {string} content The generated TypeScript types to rewrite the enums of.
|
|
17
|
+
* @param {boolean} [declaration=false] Whether the types are emitted into a declaration
|
|
18
|
+
* (`.d.ts`) file, which holds no runtime values and so declares the constant instead of
|
|
19
|
+
* initializing it.
|
|
20
|
+
* @return {string} The types with every enum declaration rewritten.
|
|
21
21
|
*/
|
|
22
|
-
export declare function
|
|
22
|
+
export declare function rewriteEnumsAsObjects(content: string, declaration?: boolean): string;
|