typedapi-client-helpers 0.3.0 → 0.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/CHANGELOG.md +72 -68
- package/README.md +20 -10
- package/dist/cjs/utils/handleApiResponse.js +9 -1
- package/dist/cjs/utils/handleApiResponse.js.map +1 -1
- package/dist/esm/utils/handleApiResponse.js +8 -1
- package/dist/esm/utils/handleApiResponse.js.map +1 -1
- package/dist/types/interfaces/ApiResult.d.ts +9 -0
- package/dist/types/interfaces/ApiResult.d.ts.map +1 -1
- package/dist/types/utils/handleApiResponse.d.ts +3 -2
- package/dist/types/utils/handleApiResponse.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/openapi-generator/index.mjs +0 -9
- package/scripts/openapi-generator/method-template.mjs +405 -397
- package/scripts/openapi-generator/schema.mjs +737 -735
|
@@ -1,735 +1,737 @@
|
|
|
1
|
-
import {
|
|
2
|
-
dereference,
|
|
3
|
-
getErrorResponses,
|
|
4
|
-
getParameterSchema,
|
|
5
|
-
getRequestBodyContent,
|
|
6
|
-
getResponseContent,
|
|
7
|
-
getSuccessResponse,
|
|
8
|
-
} from "./openapi-utils.mjs";
|
|
9
|
-
import {
|
|
10
|
-
operationTypeName,
|
|
11
|
-
pascalCase,
|
|
12
|
-
propertyKey,
|
|
13
|
-
refName,
|
|
14
|
-
sanitizeIdentifier,
|
|
15
|
-
typePropertyName,
|
|
16
|
-
uniqueName,
|
|
17
|
-
} from "./names.mjs";
|
|
18
|
-
|
|
19
|
-
function jsDocFromSchema(schema, indent = "") {
|
|
20
|
-
const lines = [];
|
|
21
|
-
if (schema?.description)
|
|
22
|
-
lines.push(...String(schema.description).split(/\r?\n/));
|
|
23
|
-
if (schema?.format) lines.push(`@format ${schema.format}`);
|
|
24
|
-
if (Object.prototype.hasOwnProperty.call(schema ?? {}, "default")) {
|
|
25
|
-
lines.push(`@default ${JSON.stringify(schema.default)}`);
|
|
26
|
-
}
|
|
27
|
-
if (schema?.readOnly) lines.push("@readonly");
|
|
28
|
-
if (schema?.writeOnly) lines.push("@writeOnly");
|
|
29
|
-
if (schema?.deprecated) lines.push("@deprecated");
|
|
30
|
-
if (lines.length === 0) return "";
|
|
31
|
-
if (lines.length === 1) return `${indent}/** ${lines[0]} */\n`;
|
|
32
|
-
return `${indent}/**\n${lines.map((line) => `${indent} * ${line}`).join("\n")}\n${indent} */\n`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function unionTypes(types, fallback = "unknown") {
|
|
36
|
-
const unique = [...new Set(types.filter(Boolean))];
|
|
37
|
-
return unique.length > 0 ? unique.join(" | ") : fallback;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function intersectionTypes(types, fallback = "Record<string, unknown>") {
|
|
41
|
-
const unique = [...new Set(types.filter(Boolean))];
|
|
42
|
-
return unique.length > 0 ? unique.join(" & ") : fallback;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function hasObjectShape(schema) {
|
|
46
|
-
return Boolean(
|
|
47
|
-
schema?.properties ||
|
|
48
|
-
schema?.additionalProperties ||
|
|
49
|
-
schema?.type === "object",
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function schemaWithoutComposition(schema) {
|
|
54
|
-
const { allOf, oneOf, anyOf, nullable, ...local } = schema ?? {};
|
|
55
|
-
return local;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function assertUniqueTypePropertyNames(entries, contextName) {
|
|
59
|
-
const seen = new Map();
|
|
60
|
-
for (const rawName of entries) {
|
|
61
|
-
const localName = typePropertyName(rawName);
|
|
62
|
-
const existing = seen.get(localName);
|
|
63
|
-
if (existing !== undefined && existing !== rawName) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`OpenAPI members ${JSON.stringify(existing)} and ${JSON.stringify(rawName)} in ${contextName} ` +
|
|
66
|
-
`both become TypeScript property ${JSON.stringify(localName)} when only the first character is lowercased.`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
seen.set(localName, rawName);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function escapeJsonPointerPart(value) {
|
|
74
|
-
return String(value).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function componentWireSchemaKey(rawName) {
|
|
78
|
-
return `#/components/schemas/${escapeJsonPointerPart(rawName)}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function operationWireSchemaKey(operationId, kind) {
|
|
82
|
-
return `operation:${operationId}:${kind}`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function wireSchemaDescriptor(openApi, schemaOrRef) {
|
|
86
|
-
if (!schemaOrRef) return { kind: "identity" };
|
|
87
|
-
if (schemaOrRef.$ref) return { kind: "ref", ref: String(schemaOrRef.$ref) };
|
|
88
|
-
|
|
89
|
-
const schema =
|
|
90
|
-
dereference(openApi, schemaOrRef, "Wire schema reference") ?? schemaOrRef;
|
|
91
|
-
|
|
92
|
-
if (schema.allOf) {
|
|
93
|
-
const schemas = schema.allOf.map((item) =>
|
|
94
|
-
wireSchemaDescriptor(openApi, item),
|
|
95
|
-
);
|
|
96
|
-
const localSchema = schemaWithoutComposition(schema);
|
|
97
|
-
if (hasObjectShape(localSchema))
|
|
98
|
-
schemas.push(wireSchemaDescriptor(openApi, localSchema));
|
|
99
|
-
return { kind: "allOf", schemas };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (schema.oneOf || schema.anyOf) {
|
|
103
|
-
return {
|
|
104
|
-
kind: "union",
|
|
105
|
-
schemas: (schema.oneOf ?? schema.anyOf).map((item) =>
|
|
106
|
-
wireSchemaDescriptor(openApi, item),
|
|
107
|
-
),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const schemaType = Array.isArray(schema.type)
|
|
112
|
-
? schema.type.find((item) => item !== "null")
|
|
113
|
-
: schema.type;
|
|
114
|
-
|
|
115
|
-
if (schemaType === "array") {
|
|
116
|
-
return {
|
|
117
|
-
kind: "array",
|
|
118
|
-
items: wireSchemaDescriptor(openApi, schema.items ?? {}),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
schemaType === "object" ||
|
|
124
|
-
schema.properties ||
|
|
125
|
-
schema.additionalProperties
|
|
126
|
-
) {
|
|
127
|
-
const rawPropertyNames = Object.keys(schema.properties ?? {});
|
|
128
|
-
assertUniqueTypePropertyNames(rawPropertyNames, "an OpenAPI object schema");
|
|
129
|
-
const properties = {};
|
|
130
|
-
|
|
131
|
-
for (const [wireName, propertySchema] of Object.entries(
|
|
132
|
-
schema.properties ?? {},
|
|
133
|
-
)) {
|
|
134
|
-
const localName = typePropertyName(wireName);
|
|
135
|
-
const nested = wireSchemaDescriptor(openApi, propertySchema);
|
|
136
|
-
properties[localName] = {
|
|
137
|
-
wireName,
|
|
138
|
-
...(nested.kind === "identity" ? {} : { schema: nested }),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let additionalProperties;
|
|
143
|
-
if (schema.additionalProperties === true) additionalProperties = true;
|
|
144
|
-
else if (schema.additionalProperties) {
|
|
145
|
-
additionalProperties = wireSchemaDescriptor(
|
|
146
|
-
openApi,
|
|
147
|
-
schema.additionalProperties,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
kind: "object",
|
|
153
|
-
...(Object.keys(properties).length > 0 ? { properties } : {}),
|
|
154
|
-
...(additionalProperties !== undefined ? { additionalProperties } : {}),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return { kind: "identity" };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function generateWireSchemaRegistry(openApi, operations, options = {}) {
|
|
162
|
-
const registry = {};
|
|
163
|
-
|
|
164
|
-
for (const [rawName, schema] of Object.entries(
|
|
165
|
-
openApi.components?.schemas ?? {},
|
|
166
|
-
)) {
|
|
167
|
-
registry[componentWireSchemaKey(rawName)] = wireSchemaDescriptor(
|
|
168
|
-
openApi,
|
|
169
|
-
schema,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (const operationInfo of operations) {
|
|
174
|
-
const operationId = operationInfo.operationId;
|
|
175
|
-
const requestBody = getRequestBodyContent(
|
|
176
|
-
openApi,
|
|
177
|
-
operationInfo.operation.requestBody,
|
|
178
|
-
);
|
|
179
|
-
const requestSchema = requestBody?.mediaType?.schema;
|
|
180
|
-
if (requestSchema) {
|
|
181
|
-
registry[operationWireSchemaKey(operationId, "body")] =
|
|
182
|
-
wireSchemaDescriptor(openApi, requestSchema);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const successResponse = getSuccessResponse(
|
|
186
|
-
openApi,
|
|
187
|
-
operationInfo.operation,
|
|
188
|
-
options.defaultResponseAsSuccess,
|
|
189
|
-
);
|
|
190
|
-
const responseSchema = getResponseContent(successResponse?.response)
|
|
191
|
-
?.mediaType?.schema;
|
|
192
|
-
if (responseSchema) {
|
|
193
|
-
registry[operationWireSchemaKey(operationId, "response")] =
|
|
194
|
-
wireSchemaDescriptor(openApi, responseSchema);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const errorSchemas = getErrorResponses(openApi, operationInfo.operation)
|
|
198
|
-
.map(({ response }) => getResponseContent(response)?.mediaType?.schema)
|
|
199
|
-
.filter(Boolean)
|
|
200
|
-
.map((schema) => wireSchemaDescriptor(openApi, schema));
|
|
201
|
-
if (errorSchemas.length > 0) {
|
|
202
|
-
registry[operationWireSchemaKey(operationId, "error")] =
|
|
203
|
-
errorSchemas.length === 1
|
|
204
|
-
? errorSchemas[0]
|
|
205
|
-
: { kind: "union", schemas: errorSchemas };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return registry;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export class TypeResolver {
|
|
213
|
-
constructor(openApi, options = {}) {
|
|
214
|
-
this.openApi = openApi;
|
|
215
|
-
this.options = options;
|
|
216
|
-
this.inlineTypes = new Map();
|
|
217
|
-
this.componentNames = new Map();
|
|
218
|
-
const usedNames = new Set();
|
|
219
|
-
|
|
220
|
-
for (const rawName of Object.keys(openApi.components?.schemas ?? {})) {
|
|
221
|
-
const name = uniqueName(operationTypeName(rawName), usedNames);
|
|
222
|
-
this.componentNames.set(rawName, name);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
componentName(rawName) {
|
|
227
|
-
return this.componentNames.get(rawName) ?? operationTypeName(rawName);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
resolve(schemaOrRef, contextName = "Anonymous", options = {}) {
|
|
231
|
-
if (!schemaOrRef) return "unknown";
|
|
232
|
-
|
|
233
|
-
if (schemaOrRef.$ref) {
|
|
234
|
-
dereference(this.openApi, schemaOrRef, "Schema reference");
|
|
235
|
-
return this.componentName(refName(schemaOrRef.$ref));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const schema =
|
|
239
|
-
dereference(this.openApi, schemaOrRef, "Schema reference") ?? schemaOrRef;
|
|
240
|
-
const nullable =
|
|
241
|
-
schema.nullable ||
|
|
242
|
-
(Array.isArray(schema.type) && schema.type.includes("null"));
|
|
243
|
-
let type = this.resolveNonNullable(schema, contextName, options);
|
|
244
|
-
if (nullable && !/(^|\W)null(\W|$)/.test(type)) type = `${type} | null`;
|
|
245
|
-
return type;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
resolveNonNullable(schema, contextName, options = {}) {
|
|
249
|
-
if (schema.const !== undefined) return JSON.stringify(schema.const);
|
|
250
|
-
|
|
251
|
-
if (Array.isArray(schema.enum)) {
|
|
252
|
-
if (options.inlineEnumAsUnion) {
|
|
253
|
-
return unionTypes(
|
|
254
|
-
this.enumLiteralValues(schema).map((value) => JSON.stringify(value)),
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
return operationTypeName(contextName);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (schema.oneOf || schema.anyOf) {
|
|
261
|
-
const variants = schema.oneOf ?? schema.anyOf;
|
|
262
|
-
return unionTypes(
|
|
263
|
-
variants.map((item, index) =>
|
|
264
|
-
this.resolve(item, `${contextName}${index + 1}`, options),
|
|
265
|
-
),
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (schema.allOf) {
|
|
270
|
-
const parts = schema.allOf.map((item, index) =>
|
|
271
|
-
this.resolve(item, `${contextName}${index + 1}`, options),
|
|
272
|
-
);
|
|
273
|
-
const localSchema = schemaWithoutComposition(schema);
|
|
274
|
-
if (hasObjectShape(localSchema)) {
|
|
275
|
-
parts.push(
|
|
276
|
-
this.createInlineObjectType(
|
|
277
|
-
localSchema,
|
|
278
|
-
`${contextName}Own`,
|
|
279
|
-
options,
|
|
280
|
-
),
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
return intersectionTypes(parts);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const schemaType = Array.isArray(schema.type)
|
|
287
|
-
? schema.type.find((item) => item !== "null")
|
|
288
|
-
: schema.type;
|
|
289
|
-
|
|
290
|
-
switch (schemaType) {
|
|
291
|
-
case "integer":
|
|
292
|
-
case "number":
|
|
293
|
-
return "number";
|
|
294
|
-
case "boolean":
|
|
295
|
-
return "boolean";
|
|
296
|
-
case "string":
|
|
297
|
-
if (schema.format === "binary")
|
|
298
|
-
return options.binaryAsBlob ? "Blob" : "File";
|
|
299
|
-
return "string";
|
|
300
|
-
case "array": {
|
|
301
|
-
const itemType = this.resolve(
|
|
302
|
-
schema.items ?? {},
|
|
303
|
-
`${contextName}Item`,
|
|
304
|
-
options,
|
|
305
|
-
);
|
|
306
|
-
return /^[A-Za-z_$][\w$]*(?:<.*>)?$/.test(itemType) ||
|
|
307
|
-
itemType.endsWith("[]")
|
|
308
|
-
? `${itemType}[]`
|
|
309
|
-
: `(${itemType})[]`;
|
|
310
|
-
}
|
|
311
|
-
case "object":
|
|
312
|
-
default:
|
|
313
|
-
if (schema.properties)
|
|
314
|
-
return this.createInlineObjectType(schema, contextName, options);
|
|
315
|
-
if (schema.additionalProperties) {
|
|
316
|
-
const valueType =
|
|
317
|
-
schema.additionalProperties === true
|
|
318
|
-
? "unknown"
|
|
319
|
-
: this.resolve(
|
|
320
|
-
schema.additionalProperties,
|
|
321
|
-
`${contextName}Value`,
|
|
322
|
-
options,
|
|
323
|
-
);
|
|
324
|
-
return `Record<string, ${valueType}>`;
|
|
325
|
-
}
|
|
326
|
-
return schemaType === "object" ? "Record<string, unknown>" : "unknown";
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
createInlineObjectType(schema, contextName) {
|
|
331
|
-
const base = operationTypeName(contextName);
|
|
332
|
-
let name = base;
|
|
333
|
-
let counter = 2;
|
|
334
|
-
while (
|
|
335
|
-
this.componentNamesHasValue(name) ||
|
|
336
|
-
(this.inlineTypes.has(name) && this.inlineTypes.get(name) !== schema)
|
|
337
|
-
) {
|
|
338
|
-
name = `${base}${counter++}`;
|
|
339
|
-
}
|
|
340
|
-
if (!this.inlineTypes.has(name)) this.inlineTypes.set(name, schema);
|
|
341
|
-
return name;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
componentNamesHasValue(value) {
|
|
345
|
-
return [...this.componentNames.values()].includes(value);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
objectMembers(name, schema) {
|
|
349
|
-
const required = new Set(schema.required ?? []);
|
|
350
|
-
const lines = [];
|
|
351
|
-
|
|
352
|
-
const propertyEntries = Object.entries(schema.properties ?? {});
|
|
353
|
-
assertUniqueTypePropertyNames(
|
|
354
|
-
propertyEntries.map(([propertyName]) => propertyName),
|
|
355
|
-
name,
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
for (const [propertyName, propertySchema] of propertyEntries) {
|
|
359
|
-
const localPropertyName = typePropertyName(propertyName);
|
|
360
|
-
const optionalToken = required.has(propertyName) ? "" : "?";
|
|
361
|
-
const propertyType = this.resolve(
|
|
362
|
-
propertySchema,
|
|
363
|
-
`${name}${pascalCase(propertyName)}`,
|
|
364
|
-
{
|
|
365
|
-
inlineEnumAsUnion: true,
|
|
366
|
-
},
|
|
367
|
-
);
|
|
368
|
-
const resolvedSchema = dereference(
|
|
369
|
-
this.openApi,
|
|
370
|
-
propertySchema,
|
|
371
|
-
"Property schema reference",
|
|
372
|
-
);
|
|
373
|
-
lines.push(
|
|
374
|
-
jsDocFromSchema(resolvedSchema, " ") +
|
|
375
|
-
` ${propertyKey(localPropertyName)}${optionalToken}: ${propertyType};`,
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (schema.additionalProperties) {
|
|
380
|
-
const valueType =
|
|
381
|
-
schema.additionalProperties === true
|
|
382
|
-
? "unknown"
|
|
383
|
-
: this.resolve(schema.additionalProperties, `${name}Value`, {
|
|
384
|
-
inlineEnumAsUnion: true,
|
|
385
|
-
});
|
|
386
|
-
lines.push(` [key: string]: ${valueType};`);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return lines;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
createInterface(name, schema) {
|
|
393
|
-
return [
|
|
394
|
-
`export interface ${name} {`,
|
|
395
|
-
...this.objectMembers(name, schema),
|
|
396
|
-
"}",
|
|
397
|
-
].join("\n");
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
createObjectTypeLiteral(name, schema) {
|
|
401
|
-
return ["{", ...this.objectMembers(name, schema), "}"].join("\n");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
createDeclaration(name, schema) {
|
|
405
|
-
if (Array.isArray(schema.enum)) return this.createEnum(name, schema);
|
|
406
|
-
|
|
407
|
-
if (schema.allOf) {
|
|
408
|
-
const parts = schema.allOf.map((item, index) =>
|
|
409
|
-
this.resolve(item, `${name}${index + 1}`, { inlineEnumAsUnion: true }),
|
|
410
|
-
);
|
|
411
|
-
const localSchema = schemaWithoutComposition(schema);
|
|
412
|
-
if (hasObjectShape(localSchema))
|
|
413
|
-
parts.push(this.createObjectTypeLiteral(`${name}Own`, localSchema));
|
|
414
|
-
return `export type ${name} = ${intersectionTypes(parts)};`;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (hasObjectShape(schema)) return this.createInterface(name, schema);
|
|
418
|
-
return `export type ${name} = ${this.resolve(schema, name, { inlineEnumAsUnion: true })};`;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
createEnum(name, schema) {
|
|
422
|
-
if (this.options.generateUnionEnums) {
|
|
423
|
-
return `export type ${name} = ${unionTypes(
|
|
424
|
-
this.enumLiteralValues(schema).map((value) => JSON.stringify(value)),
|
|
425
|
-
)};`;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const lines = [`export const ${name} = {`];
|
|
429
|
-
for (const [index, value] of (schema.enum ?? []).entries()) {
|
|
430
|
-
const memberName = this.enumMemberName(schema, value, index);
|
|
431
|
-
const enumValue = this.enumRuntimeValue(schema, value, index);
|
|
432
|
-
lines.push(` ${propertyKey(memberName)}: ${JSON.stringify(enumValue)},`);
|
|
433
|
-
}
|
|
434
|
-
lines.push("} as const;");
|
|
435
|
-
lines.push(`export type ${name} = (typeof ${name})[keyof typeof ${name}];`);
|
|
436
|
-
return lines.join("\n");
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
enumMetadataNames(schema) {
|
|
440
|
-
const names =
|
|
441
|
-
schema?.["x-enumNames"] ??
|
|
442
|
-
schema?.["x-enum-varnames"] ??
|
|
443
|
-
schema?.["x-enumVarnames"];
|
|
444
|
-
return Array.isArray(names) ? names : [];
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
enumLiteralValues(schema) {
|
|
448
|
-
const values = schema.enum ?? [];
|
|
449
|
-
const names = this.enumMetadataNames(schema);
|
|
450
|
-
return this.options.enumNamesAsValues && names.length > 0
|
|
451
|
-
? values.map((value, index) => names[index] ?? value)
|
|
452
|
-
: values;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
enumRuntimeValue(schema, value, index) {
|
|
456
|
-
const names = this.enumMetadataNames(schema);
|
|
457
|
-
return this.options.enumNamesAsValues && names.length > 0
|
|
458
|
-
? (names[index] ?? value)
|
|
459
|
-
: value;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
enumMemberName(schema, value, index) {
|
|
463
|
-
const rawName = this.enumMetadataNames(schema)[index] ?? value;
|
|
464
|
-
return operationTypeName(String(rawName) || "Value");
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function parameterTypeName(operationId, location) {
|
|
469
|
-
return `${operationId}${pascalCase(location)}Params`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function combinedParameterTypeName(operationId) {
|
|
473
|
-
return `${operationId}Params`;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function createParamsInterface(openApi, resolver, name, parameters) {
|
|
477
|
-
const lines = [`export interface ${name} {`];
|
|
478
|
-
assertUniqueTypePropertyNames(
|
|
479
|
-
parameters.map((parameter) => parameter.name),
|
|
480
|
-
name,
|
|
481
|
-
);
|
|
482
|
-
for (const parameter of parameters) {
|
|
483
|
-
const parameterSchema = getParameterSchema(openApi, parameter);
|
|
484
|
-
const optionalToken =
|
|
485
|
-
parameter.required || parameter.in === "path" ? "" : "?";
|
|
486
|
-
const parameterType = resolver.resolve(
|
|
487
|
-
parameterSchema,
|
|
488
|
-
`${name}${pascalCase(parameter.name)}`,
|
|
489
|
-
{
|
|
490
|
-
inlineEnumAsUnion: true,
|
|
491
|
-
},
|
|
492
|
-
);
|
|
493
|
-
lines.push(
|
|
494
|
-
jsDocFromSchema(parameterSchema, " ") +
|
|
495
|
-
` ${propertyKey(typePropertyName(parameter.name))}${optionalToken}: ${parameterType};`,
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
lines.push("}");
|
|
499
|
-
return lines.join("\n");
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function resolveResponseType(
|
|
503
|
-
resolver,
|
|
504
|
-
response,
|
|
505
|
-
contextName,
|
|
506
|
-
binaryAsBlob = false,
|
|
507
|
-
) {
|
|
508
|
-
const content = getResponseContent(response);
|
|
509
|
-
const schema = content?.mediaType?.schema;
|
|
510
|
-
if (!schema) return undefined;
|
|
511
|
-
return resolver.resolve(schema, contextName, {
|
|
512
|
-
binaryAsBlob:
|
|
513
|
-
binaryAsBlob || content?.contentType === "application/octet-stream",
|
|
514
|
-
inlineEnumAsUnion: true,
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
export function getOperationTypes(
|
|
519
|
-
openApi,
|
|
520
|
-
operationInfo,
|
|
521
|
-
options = {},
|
|
522
|
-
resolver = new TypeResolver(openApi, options),
|
|
523
|
-
) {
|
|
524
|
-
const operationId = operationInfo.operationId;
|
|
525
|
-
const operationTypeNameBase = operationInfo.typeName ?? operationId;
|
|
526
|
-
const parameters = operationInfo.parameters ?? [];
|
|
527
|
-
const parametersByLocation = Object.fromEntries(
|
|
528
|
-
["path", "query", "header", "cookie"].map((location) => [
|
|
529
|
-
location,
|
|
530
|
-
parameters.filter((parameter) => parameter.in === location),
|
|
531
|
-
]),
|
|
532
|
-
);
|
|
533
|
-
|
|
534
|
-
const requestBodyContent = getRequestBodyContent(
|
|
535
|
-
openApi,
|
|
536
|
-
operationInfo.operation.requestBody,
|
|
537
|
-
);
|
|
538
|
-
const requestBodySchema = requestBodyContent?.mediaType?.schema;
|
|
539
|
-
let bodyType;
|
|
540
|
-
if (requestBodySchema) {
|
|
541
|
-
if (requestBodySchema.$ref) {
|
|
542
|
-
bodyType = resolver.resolve(requestBodySchema, `${operationTypeNameBase}Payload`);
|
|
543
|
-
} else if (
|
|
544
|
-
requestBodyContent?.contentType === "multipart/form-data" ||
|
|
545
|
-
hasObjectShape(requestBodySchema)
|
|
546
|
-
) {
|
|
547
|
-
bodyType = `${operationTypeNameBase}Payload`;
|
|
548
|
-
} else {
|
|
549
|
-
bodyType = resolver.resolve(requestBodySchema, `${operationTypeNameBase}Payload`, {
|
|
550
|
-
inlineEnumAsUnion: true,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const successResponse = getSuccessResponse(
|
|
556
|
-
openApi,
|
|
557
|
-
operationInfo.operation,
|
|
558
|
-
options.defaultResponseAsSuccess,
|
|
559
|
-
);
|
|
560
|
-
const responseContent = getResponseContent(successResponse?.response);
|
|
561
|
-
const responseType = successResponse
|
|
562
|
-
? (resolveResponseType(
|
|
563
|
-
resolver,
|
|
564
|
-
successResponse.response,
|
|
565
|
-
`${operationTypeNameBase}Response`,
|
|
566
|
-
) ?? "void")
|
|
567
|
-
: "void";
|
|
568
|
-
|
|
569
|
-
const errorResponses = getErrorResponses(openApi, operationInfo.operation);
|
|
570
|
-
const errorTypes = errorResponses
|
|
571
|
-
.map(({ statusCode, response }) =>
|
|
572
|
-
resolveResponseType(
|
|
573
|
-
resolver,
|
|
574
|
-
response,
|
|
575
|
-
`${operationTypeNameBase}Error${statusCode}`,
|
|
576
|
-
),
|
|
577
|
-
)
|
|
578
|
-
.filter(Boolean);
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
!parametersByLocation.
|
|
596
|
-
!parametersByLocation.
|
|
597
|
-
!
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
*
|
|
730
|
-
* ##
|
|
731
|
-
* ##
|
|
732
|
-
* ##
|
|
733
|
-
*
|
|
734
|
-
|
|
735
|
-
|
|
1
|
+
import {
|
|
2
|
+
dereference,
|
|
3
|
+
getErrorResponses,
|
|
4
|
+
getParameterSchema,
|
|
5
|
+
getRequestBodyContent,
|
|
6
|
+
getResponseContent,
|
|
7
|
+
getSuccessResponse,
|
|
8
|
+
} from "./openapi-utils.mjs";
|
|
9
|
+
import {
|
|
10
|
+
operationTypeName,
|
|
11
|
+
pascalCase,
|
|
12
|
+
propertyKey,
|
|
13
|
+
refName,
|
|
14
|
+
sanitizeIdentifier,
|
|
15
|
+
typePropertyName,
|
|
16
|
+
uniqueName,
|
|
17
|
+
} from "./names.mjs";
|
|
18
|
+
|
|
19
|
+
function jsDocFromSchema(schema, indent = "") {
|
|
20
|
+
const lines = [];
|
|
21
|
+
if (schema?.description)
|
|
22
|
+
lines.push(...String(schema.description).split(/\r?\n/));
|
|
23
|
+
if (schema?.format) lines.push(`@format ${schema.format}`);
|
|
24
|
+
if (Object.prototype.hasOwnProperty.call(schema ?? {}, "default")) {
|
|
25
|
+
lines.push(`@default ${JSON.stringify(schema.default)}`);
|
|
26
|
+
}
|
|
27
|
+
if (schema?.readOnly) lines.push("@readonly");
|
|
28
|
+
if (schema?.writeOnly) lines.push("@writeOnly");
|
|
29
|
+
if (schema?.deprecated) lines.push("@deprecated");
|
|
30
|
+
if (lines.length === 0) return "";
|
|
31
|
+
if (lines.length === 1) return `${indent}/** ${lines[0]} */\n`;
|
|
32
|
+
return `${indent}/**\n${lines.map((line) => `${indent} * ${line}`).join("\n")}\n${indent} */\n`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function unionTypes(types, fallback = "unknown") {
|
|
36
|
+
const unique = [...new Set(types.filter(Boolean))];
|
|
37
|
+
return unique.length > 0 ? unique.join(" | ") : fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function intersectionTypes(types, fallback = "Record<string, unknown>") {
|
|
41
|
+
const unique = [...new Set(types.filter(Boolean))];
|
|
42
|
+
return unique.length > 0 ? unique.join(" & ") : fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasObjectShape(schema) {
|
|
46
|
+
return Boolean(
|
|
47
|
+
schema?.properties ||
|
|
48
|
+
schema?.additionalProperties ||
|
|
49
|
+
schema?.type === "object",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function schemaWithoutComposition(schema) {
|
|
54
|
+
const { allOf, oneOf, anyOf, nullable, ...local } = schema ?? {};
|
|
55
|
+
return local;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertUniqueTypePropertyNames(entries, contextName) {
|
|
59
|
+
const seen = new Map();
|
|
60
|
+
for (const rawName of entries) {
|
|
61
|
+
const localName = typePropertyName(rawName);
|
|
62
|
+
const existing = seen.get(localName);
|
|
63
|
+
if (existing !== undefined && existing !== rawName) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`OpenAPI members ${JSON.stringify(existing)} and ${JSON.stringify(rawName)} in ${contextName} ` +
|
|
66
|
+
`both become TypeScript property ${JSON.stringify(localName)} when only the first character is lowercased.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
seen.set(localName, rawName);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function escapeJsonPointerPart(value) {
|
|
74
|
+
return String(value).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function componentWireSchemaKey(rawName) {
|
|
78
|
+
return `#/components/schemas/${escapeJsonPointerPart(rawName)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function operationWireSchemaKey(operationId, kind) {
|
|
82
|
+
return `operation:${operationId}:${kind}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function wireSchemaDescriptor(openApi, schemaOrRef) {
|
|
86
|
+
if (!schemaOrRef) return { kind: "identity" };
|
|
87
|
+
if (schemaOrRef.$ref) return { kind: "ref", ref: String(schemaOrRef.$ref) };
|
|
88
|
+
|
|
89
|
+
const schema =
|
|
90
|
+
dereference(openApi, schemaOrRef, "Wire schema reference") ?? schemaOrRef;
|
|
91
|
+
|
|
92
|
+
if (schema.allOf) {
|
|
93
|
+
const schemas = schema.allOf.map((item) =>
|
|
94
|
+
wireSchemaDescriptor(openApi, item),
|
|
95
|
+
);
|
|
96
|
+
const localSchema = schemaWithoutComposition(schema);
|
|
97
|
+
if (hasObjectShape(localSchema))
|
|
98
|
+
schemas.push(wireSchemaDescriptor(openApi, localSchema));
|
|
99
|
+
return { kind: "allOf", schemas };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (schema.oneOf || schema.anyOf) {
|
|
103
|
+
return {
|
|
104
|
+
kind: "union",
|
|
105
|
+
schemas: (schema.oneOf ?? schema.anyOf).map((item) =>
|
|
106
|
+
wireSchemaDescriptor(openApi, item),
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const schemaType = Array.isArray(schema.type)
|
|
112
|
+
? schema.type.find((item) => item !== "null")
|
|
113
|
+
: schema.type;
|
|
114
|
+
|
|
115
|
+
if (schemaType === "array") {
|
|
116
|
+
return {
|
|
117
|
+
kind: "array",
|
|
118
|
+
items: wireSchemaDescriptor(openApi, schema.items ?? {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
schemaType === "object" ||
|
|
124
|
+
schema.properties ||
|
|
125
|
+
schema.additionalProperties
|
|
126
|
+
) {
|
|
127
|
+
const rawPropertyNames = Object.keys(schema.properties ?? {});
|
|
128
|
+
assertUniqueTypePropertyNames(rawPropertyNames, "an OpenAPI object schema");
|
|
129
|
+
const properties = {};
|
|
130
|
+
|
|
131
|
+
for (const [wireName, propertySchema] of Object.entries(
|
|
132
|
+
schema.properties ?? {},
|
|
133
|
+
)) {
|
|
134
|
+
const localName = typePropertyName(wireName);
|
|
135
|
+
const nested = wireSchemaDescriptor(openApi, propertySchema);
|
|
136
|
+
properties[localName] = {
|
|
137
|
+
wireName,
|
|
138
|
+
...(nested.kind === "identity" ? {} : { schema: nested }),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let additionalProperties;
|
|
143
|
+
if (schema.additionalProperties === true) additionalProperties = true;
|
|
144
|
+
else if (schema.additionalProperties) {
|
|
145
|
+
additionalProperties = wireSchemaDescriptor(
|
|
146
|
+
openApi,
|
|
147
|
+
schema.additionalProperties,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
kind: "object",
|
|
153
|
+
...(Object.keys(properties).length > 0 ? { properties } : {}),
|
|
154
|
+
...(additionalProperties !== undefined ? { additionalProperties } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { kind: "identity" };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function generateWireSchemaRegistry(openApi, operations, options = {}) {
|
|
162
|
+
const registry = {};
|
|
163
|
+
|
|
164
|
+
for (const [rawName, schema] of Object.entries(
|
|
165
|
+
openApi.components?.schemas ?? {},
|
|
166
|
+
)) {
|
|
167
|
+
registry[componentWireSchemaKey(rawName)] = wireSchemaDescriptor(
|
|
168
|
+
openApi,
|
|
169
|
+
schema,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const operationInfo of operations) {
|
|
174
|
+
const operationId = operationInfo.operationId;
|
|
175
|
+
const requestBody = getRequestBodyContent(
|
|
176
|
+
openApi,
|
|
177
|
+
operationInfo.operation.requestBody,
|
|
178
|
+
);
|
|
179
|
+
const requestSchema = requestBody?.mediaType?.schema;
|
|
180
|
+
if (requestSchema) {
|
|
181
|
+
registry[operationWireSchemaKey(operationId, "body")] =
|
|
182
|
+
wireSchemaDescriptor(openApi, requestSchema);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const successResponse = getSuccessResponse(
|
|
186
|
+
openApi,
|
|
187
|
+
operationInfo.operation,
|
|
188
|
+
options.defaultResponseAsSuccess,
|
|
189
|
+
);
|
|
190
|
+
const responseSchema = getResponseContent(successResponse?.response)
|
|
191
|
+
?.mediaType?.schema;
|
|
192
|
+
if (responseSchema) {
|
|
193
|
+
registry[operationWireSchemaKey(operationId, "response")] =
|
|
194
|
+
wireSchemaDescriptor(openApi, responseSchema);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const errorSchemas = getErrorResponses(openApi, operationInfo.operation)
|
|
198
|
+
.map(({ response }) => getResponseContent(response)?.mediaType?.schema)
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.map((schema) => wireSchemaDescriptor(openApi, schema));
|
|
201
|
+
if (errorSchemas.length > 0) {
|
|
202
|
+
registry[operationWireSchemaKey(operationId, "error")] =
|
|
203
|
+
errorSchemas.length === 1
|
|
204
|
+
? errorSchemas[0]
|
|
205
|
+
: { kind: "union", schemas: errorSchemas };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return registry;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export class TypeResolver {
|
|
213
|
+
constructor(openApi, options = {}) {
|
|
214
|
+
this.openApi = openApi;
|
|
215
|
+
this.options = options;
|
|
216
|
+
this.inlineTypes = new Map();
|
|
217
|
+
this.componentNames = new Map();
|
|
218
|
+
const usedNames = new Set();
|
|
219
|
+
|
|
220
|
+
for (const rawName of Object.keys(openApi.components?.schemas ?? {})) {
|
|
221
|
+
const name = uniqueName(operationTypeName(rawName), usedNames);
|
|
222
|
+
this.componentNames.set(rawName, name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
componentName(rawName) {
|
|
227
|
+
return this.componentNames.get(rawName) ?? operationTypeName(rawName);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
resolve(schemaOrRef, contextName = "Anonymous", options = {}) {
|
|
231
|
+
if (!schemaOrRef) return "unknown";
|
|
232
|
+
|
|
233
|
+
if (schemaOrRef.$ref) {
|
|
234
|
+
dereference(this.openApi, schemaOrRef, "Schema reference");
|
|
235
|
+
return this.componentName(refName(schemaOrRef.$ref));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const schema =
|
|
239
|
+
dereference(this.openApi, schemaOrRef, "Schema reference") ?? schemaOrRef;
|
|
240
|
+
const nullable =
|
|
241
|
+
schema.nullable ||
|
|
242
|
+
(Array.isArray(schema.type) && schema.type.includes("null"));
|
|
243
|
+
let type = this.resolveNonNullable(schema, contextName, options);
|
|
244
|
+
if (nullable && !/(^|\W)null(\W|$)/.test(type)) type = `${type} | null`;
|
|
245
|
+
return type;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
resolveNonNullable(schema, contextName, options = {}) {
|
|
249
|
+
if (schema.const !== undefined) return JSON.stringify(schema.const);
|
|
250
|
+
|
|
251
|
+
if (Array.isArray(schema.enum)) {
|
|
252
|
+
if (options.inlineEnumAsUnion) {
|
|
253
|
+
return unionTypes(
|
|
254
|
+
this.enumLiteralValues(schema).map((value) => JSON.stringify(value)),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return operationTypeName(contextName);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (schema.oneOf || schema.anyOf) {
|
|
261
|
+
const variants = schema.oneOf ?? schema.anyOf;
|
|
262
|
+
return unionTypes(
|
|
263
|
+
variants.map((item, index) =>
|
|
264
|
+
this.resolve(item, `${contextName}${index + 1}`, options),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (schema.allOf) {
|
|
270
|
+
const parts = schema.allOf.map((item, index) =>
|
|
271
|
+
this.resolve(item, `${contextName}${index + 1}`, options),
|
|
272
|
+
);
|
|
273
|
+
const localSchema = schemaWithoutComposition(schema);
|
|
274
|
+
if (hasObjectShape(localSchema)) {
|
|
275
|
+
parts.push(
|
|
276
|
+
this.createInlineObjectType(
|
|
277
|
+
localSchema,
|
|
278
|
+
`${contextName}Own`,
|
|
279
|
+
options,
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return intersectionTypes(parts);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const schemaType = Array.isArray(schema.type)
|
|
287
|
+
? schema.type.find((item) => item !== "null")
|
|
288
|
+
: schema.type;
|
|
289
|
+
|
|
290
|
+
switch (schemaType) {
|
|
291
|
+
case "integer":
|
|
292
|
+
case "number":
|
|
293
|
+
return "number";
|
|
294
|
+
case "boolean":
|
|
295
|
+
return "boolean";
|
|
296
|
+
case "string":
|
|
297
|
+
if (schema.format === "binary")
|
|
298
|
+
return options.binaryAsBlob ? "Blob" : "File";
|
|
299
|
+
return "string";
|
|
300
|
+
case "array": {
|
|
301
|
+
const itemType = this.resolve(
|
|
302
|
+
schema.items ?? {},
|
|
303
|
+
`${contextName}Item`,
|
|
304
|
+
options,
|
|
305
|
+
);
|
|
306
|
+
return /^[A-Za-z_$][\w$]*(?:<.*>)?$/.test(itemType) ||
|
|
307
|
+
itemType.endsWith("[]")
|
|
308
|
+
? `${itemType}[]`
|
|
309
|
+
: `(${itemType})[]`;
|
|
310
|
+
}
|
|
311
|
+
case "object":
|
|
312
|
+
default:
|
|
313
|
+
if (schema.properties)
|
|
314
|
+
return this.createInlineObjectType(schema, contextName, options);
|
|
315
|
+
if (schema.additionalProperties) {
|
|
316
|
+
const valueType =
|
|
317
|
+
schema.additionalProperties === true
|
|
318
|
+
? "unknown"
|
|
319
|
+
: this.resolve(
|
|
320
|
+
schema.additionalProperties,
|
|
321
|
+
`${contextName}Value`,
|
|
322
|
+
options,
|
|
323
|
+
);
|
|
324
|
+
return `Record<string, ${valueType}>`;
|
|
325
|
+
}
|
|
326
|
+
return schemaType === "object" ? "Record<string, unknown>" : "unknown";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
createInlineObjectType(schema, contextName) {
|
|
331
|
+
const base = operationTypeName(contextName);
|
|
332
|
+
let name = base;
|
|
333
|
+
let counter = 2;
|
|
334
|
+
while (
|
|
335
|
+
this.componentNamesHasValue(name) ||
|
|
336
|
+
(this.inlineTypes.has(name) && this.inlineTypes.get(name) !== schema)
|
|
337
|
+
) {
|
|
338
|
+
name = `${base}${counter++}`;
|
|
339
|
+
}
|
|
340
|
+
if (!this.inlineTypes.has(name)) this.inlineTypes.set(name, schema);
|
|
341
|
+
return name;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
componentNamesHasValue(value) {
|
|
345
|
+
return [...this.componentNames.values()].includes(value);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
objectMembers(name, schema) {
|
|
349
|
+
const required = new Set(schema.required ?? []);
|
|
350
|
+
const lines = [];
|
|
351
|
+
|
|
352
|
+
const propertyEntries = Object.entries(schema.properties ?? {});
|
|
353
|
+
assertUniqueTypePropertyNames(
|
|
354
|
+
propertyEntries.map(([propertyName]) => propertyName),
|
|
355
|
+
name,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
for (const [propertyName, propertySchema] of propertyEntries) {
|
|
359
|
+
const localPropertyName = typePropertyName(propertyName);
|
|
360
|
+
const optionalToken = required.has(propertyName) ? "" : "?";
|
|
361
|
+
const propertyType = this.resolve(
|
|
362
|
+
propertySchema,
|
|
363
|
+
`${name}${pascalCase(propertyName)}`,
|
|
364
|
+
{
|
|
365
|
+
inlineEnumAsUnion: true,
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
const resolvedSchema = dereference(
|
|
369
|
+
this.openApi,
|
|
370
|
+
propertySchema,
|
|
371
|
+
"Property schema reference",
|
|
372
|
+
);
|
|
373
|
+
lines.push(
|
|
374
|
+
jsDocFromSchema(resolvedSchema, " ") +
|
|
375
|
+
` ${propertyKey(localPropertyName)}${optionalToken}: ${propertyType};`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (schema.additionalProperties) {
|
|
380
|
+
const valueType =
|
|
381
|
+
schema.additionalProperties === true
|
|
382
|
+
? "unknown"
|
|
383
|
+
: this.resolve(schema.additionalProperties, `${name}Value`, {
|
|
384
|
+
inlineEnumAsUnion: true,
|
|
385
|
+
});
|
|
386
|
+
lines.push(` [key: string]: ${valueType};`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return lines;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
createInterface(name, schema) {
|
|
393
|
+
return [
|
|
394
|
+
`export interface ${name} {`,
|
|
395
|
+
...this.objectMembers(name, schema),
|
|
396
|
+
"}",
|
|
397
|
+
].join("\n");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
createObjectTypeLiteral(name, schema) {
|
|
401
|
+
return ["{", ...this.objectMembers(name, schema), "}"].join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
createDeclaration(name, schema) {
|
|
405
|
+
if (Array.isArray(schema.enum)) return this.createEnum(name, schema);
|
|
406
|
+
|
|
407
|
+
if (schema.allOf) {
|
|
408
|
+
const parts = schema.allOf.map((item, index) =>
|
|
409
|
+
this.resolve(item, `${name}${index + 1}`, { inlineEnumAsUnion: true }),
|
|
410
|
+
);
|
|
411
|
+
const localSchema = schemaWithoutComposition(schema);
|
|
412
|
+
if (hasObjectShape(localSchema))
|
|
413
|
+
parts.push(this.createObjectTypeLiteral(`${name}Own`, localSchema));
|
|
414
|
+
return `export type ${name} = ${intersectionTypes(parts)};`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (hasObjectShape(schema)) return this.createInterface(name, schema);
|
|
418
|
+
return `export type ${name} = ${this.resolve(schema, name, { inlineEnumAsUnion: true })};`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
createEnum(name, schema) {
|
|
422
|
+
if (this.options.generateUnionEnums) {
|
|
423
|
+
return `export type ${name} = ${unionTypes(
|
|
424
|
+
this.enumLiteralValues(schema).map((value) => JSON.stringify(value)),
|
|
425
|
+
)};`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const lines = [`export const ${name} = {`];
|
|
429
|
+
for (const [index, value] of (schema.enum ?? []).entries()) {
|
|
430
|
+
const memberName = this.enumMemberName(schema, value, index);
|
|
431
|
+
const enumValue = this.enumRuntimeValue(schema, value, index);
|
|
432
|
+
lines.push(` ${propertyKey(memberName)}: ${JSON.stringify(enumValue)},`);
|
|
433
|
+
}
|
|
434
|
+
lines.push("} as const;");
|
|
435
|
+
lines.push(`export type ${name} = (typeof ${name})[keyof typeof ${name}];`);
|
|
436
|
+
return lines.join("\n");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
enumMetadataNames(schema) {
|
|
440
|
+
const names =
|
|
441
|
+
schema?.["x-enumNames"] ??
|
|
442
|
+
schema?.["x-enum-varnames"] ??
|
|
443
|
+
schema?.["x-enumVarnames"];
|
|
444
|
+
return Array.isArray(names) ? names : [];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
enumLiteralValues(schema) {
|
|
448
|
+
const values = schema.enum ?? [];
|
|
449
|
+
const names = this.enumMetadataNames(schema);
|
|
450
|
+
return this.options.enumNamesAsValues && names.length > 0
|
|
451
|
+
? values.map((value, index) => names[index] ?? value)
|
|
452
|
+
: values;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
enumRuntimeValue(schema, value, index) {
|
|
456
|
+
const names = this.enumMetadataNames(schema);
|
|
457
|
+
return this.options.enumNamesAsValues && names.length > 0
|
|
458
|
+
? (names[index] ?? value)
|
|
459
|
+
: value;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
enumMemberName(schema, value, index) {
|
|
463
|
+
const rawName = this.enumMetadataNames(schema)[index] ?? value;
|
|
464
|
+
return operationTypeName(String(rawName) || "Value");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function parameterTypeName(operationId, location) {
|
|
469
|
+
return `${operationId}${pascalCase(location)}Params`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function combinedParameterTypeName(operationId) {
|
|
473
|
+
return `${operationId}Params`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function createParamsInterface(openApi, resolver, name, parameters) {
|
|
477
|
+
const lines = [`export interface ${name} {`];
|
|
478
|
+
assertUniqueTypePropertyNames(
|
|
479
|
+
parameters.map((parameter) => parameter.name),
|
|
480
|
+
name,
|
|
481
|
+
);
|
|
482
|
+
for (const parameter of parameters) {
|
|
483
|
+
const parameterSchema = getParameterSchema(openApi, parameter);
|
|
484
|
+
const optionalToken =
|
|
485
|
+
parameter.required || parameter.in === "path" ? "" : "?";
|
|
486
|
+
const parameterType = resolver.resolve(
|
|
487
|
+
parameterSchema,
|
|
488
|
+
`${name}${pascalCase(parameter.name)}`,
|
|
489
|
+
{
|
|
490
|
+
inlineEnumAsUnion: true,
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
lines.push(
|
|
494
|
+
jsDocFromSchema(parameterSchema, " ") +
|
|
495
|
+
` ${propertyKey(typePropertyName(parameter.name))}${optionalToken}: ${parameterType};`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
lines.push("}");
|
|
499
|
+
return lines.join("\n");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function resolveResponseType(
|
|
503
|
+
resolver,
|
|
504
|
+
response,
|
|
505
|
+
contextName,
|
|
506
|
+
binaryAsBlob = false,
|
|
507
|
+
) {
|
|
508
|
+
const content = getResponseContent(response);
|
|
509
|
+
const schema = content?.mediaType?.schema;
|
|
510
|
+
if (!schema) return undefined;
|
|
511
|
+
return resolver.resolve(schema, contextName, {
|
|
512
|
+
binaryAsBlob:
|
|
513
|
+
binaryAsBlob || content?.contentType === "application/octet-stream",
|
|
514
|
+
inlineEnumAsUnion: true,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function getOperationTypes(
|
|
519
|
+
openApi,
|
|
520
|
+
operationInfo,
|
|
521
|
+
options = {},
|
|
522
|
+
resolver = new TypeResolver(openApi, options),
|
|
523
|
+
) {
|
|
524
|
+
const operationId = operationInfo.operationId;
|
|
525
|
+
const operationTypeNameBase = operationInfo.typeName ?? operationId;
|
|
526
|
+
const parameters = operationInfo.parameters ?? [];
|
|
527
|
+
const parametersByLocation = Object.fromEntries(
|
|
528
|
+
["path", "query", "header", "cookie"].map((location) => [
|
|
529
|
+
location,
|
|
530
|
+
parameters.filter((parameter) => parameter.in === location),
|
|
531
|
+
]),
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const requestBodyContent = getRequestBodyContent(
|
|
535
|
+
openApi,
|
|
536
|
+
operationInfo.operation.requestBody,
|
|
537
|
+
);
|
|
538
|
+
const requestBodySchema = requestBodyContent?.mediaType?.schema;
|
|
539
|
+
let bodyType;
|
|
540
|
+
if (requestBodySchema) {
|
|
541
|
+
if (requestBodySchema.$ref) {
|
|
542
|
+
bodyType = resolver.resolve(requestBodySchema, `${operationTypeNameBase}Payload`);
|
|
543
|
+
} else if (
|
|
544
|
+
requestBodyContent?.contentType === "multipart/form-data" ||
|
|
545
|
+
hasObjectShape(requestBodySchema)
|
|
546
|
+
) {
|
|
547
|
+
bodyType = `${operationTypeNameBase}Payload`;
|
|
548
|
+
} else {
|
|
549
|
+
bodyType = resolver.resolve(requestBodySchema, `${operationTypeNameBase}Payload`, {
|
|
550
|
+
inlineEnumAsUnion: true,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const successResponse = getSuccessResponse(
|
|
556
|
+
openApi,
|
|
557
|
+
operationInfo.operation,
|
|
558
|
+
options.defaultResponseAsSuccess,
|
|
559
|
+
);
|
|
560
|
+
const responseContent = getResponseContent(successResponse?.response);
|
|
561
|
+
const responseType = successResponse
|
|
562
|
+
? (resolveResponseType(
|
|
563
|
+
resolver,
|
|
564
|
+
successResponse.response,
|
|
565
|
+
`${operationTypeNameBase}Response`,
|
|
566
|
+
) ?? "void")
|
|
567
|
+
: "void";
|
|
568
|
+
|
|
569
|
+
const errorResponses = getErrorResponses(openApi, operationInfo.operation);
|
|
570
|
+
const errorTypes = errorResponses
|
|
571
|
+
.map(({ statusCode, response }) =>
|
|
572
|
+
resolveResponseType(
|
|
573
|
+
resolver,
|
|
574
|
+
response,
|
|
575
|
+
`${operationTypeNameBase}Error${statusCode}`,
|
|
576
|
+
),
|
|
577
|
+
)
|
|
578
|
+
.filter(Boolean);
|
|
579
|
+
const usesHttpErrorFallback = errorTypes.length === 0;
|
|
580
|
+
const errorType = unionTypes(errorTypes, "ApiHttpError");
|
|
581
|
+
const responseSchema = responseContent?.mediaType?.schema;
|
|
582
|
+
const errorSchemas = errorResponses
|
|
583
|
+
.map(({ response }) => getResponseContent(response)?.mediaType?.schema)
|
|
584
|
+
.filter(Boolean);
|
|
585
|
+
|
|
586
|
+
const hasNonBodyInput = parameters.length > 0;
|
|
587
|
+
const paginated =
|
|
588
|
+
Boolean(operationInfo.operation["x-typedapi-pagination"]) ||
|
|
589
|
+
/(?:Api)?Pagination(?:Sort)?Response\b|Paged(?:Response|Result)?\b|Paginated(?:Response|Result)?\b/.test(
|
|
590
|
+
responseType,
|
|
591
|
+
);
|
|
592
|
+
const isPurePaginatedQuery =
|
|
593
|
+
paginated &&
|
|
594
|
+
parametersByLocation.query.length > 0 &&
|
|
595
|
+
!parametersByLocation.path.length &&
|
|
596
|
+
!parametersByLocation.header.length &&
|
|
597
|
+
!parametersByLocation.cookie.length &&
|
|
598
|
+
!bodyType;
|
|
599
|
+
const parameterTypeNames = isPurePaginatedQuery
|
|
600
|
+
? { query: parameterTypeName(operationTypeNameBase, "query") }
|
|
601
|
+
: {};
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
operationId,
|
|
605
|
+
operationTypeNameBase,
|
|
606
|
+
originalOperationId: operationInfo.originalOperationId,
|
|
607
|
+
methodName: operationInfo.methodName,
|
|
608
|
+
methodNameSource: operationInfo.methodNameSource,
|
|
609
|
+
paramsTypeName:
|
|
610
|
+
hasNonBodyInput && !isPurePaginatedQuery
|
|
611
|
+
? combinedParameterTypeName(operationTypeNameBase)
|
|
612
|
+
: undefined,
|
|
613
|
+
parameterTypeNames,
|
|
614
|
+
parameters,
|
|
615
|
+
parametersByLocation,
|
|
616
|
+
allParametersOptional: parameters.every(
|
|
617
|
+
(parameter) => parameter.in !== "path" && !parameter.required,
|
|
618
|
+
),
|
|
619
|
+
bodyType,
|
|
620
|
+
bodyRequired: Boolean(requestBodyContent?.requestBody?.required),
|
|
621
|
+
bodyWireSchemaKey: requestBodySchema
|
|
622
|
+
? operationWireSchemaKey(operationId, "body")
|
|
623
|
+
: undefined,
|
|
624
|
+
responseType,
|
|
625
|
+
responseWireSchemaKey: responseSchema
|
|
626
|
+
? operationWireSchemaKey(operationId, "response")
|
|
627
|
+
: undefined,
|
|
628
|
+
errorType,
|
|
629
|
+
usesHttpErrorFallback,
|
|
630
|
+
errorWireSchemaKey:
|
|
631
|
+
errorSchemas.length > 0
|
|
632
|
+
? operationWireSchemaKey(operationId, "error")
|
|
633
|
+
: undefined,
|
|
634
|
+
contentType: requestBodyContent?.contentType,
|
|
635
|
+
responseContentType: responseContent?.contentType,
|
|
636
|
+
hasPathParams: parametersByLocation.path.length > 0,
|
|
637
|
+
hasQueryParams: parametersByLocation.query.length > 0,
|
|
638
|
+
hasHeaderParams: parametersByLocation.header.length > 0,
|
|
639
|
+
hasCookieParams: parametersByLocation.cookie.length > 0,
|
|
640
|
+
paginationMetadata: operationInfo.operation["x-typedapi-pagination"],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export function generateDataContracts(openApi, operations, options = {}) {
|
|
645
|
+
const resolver = new TypeResolver(openApi, options);
|
|
646
|
+
const declarations = [];
|
|
647
|
+
const schemas = openApi.components?.schemas ?? {};
|
|
648
|
+
|
|
649
|
+
const entries = Object.entries(schemas).sort(([a], [b]) =>
|
|
650
|
+
a.localeCompare(b),
|
|
651
|
+
);
|
|
652
|
+
for (const [rawName, schemaOrRef] of entries) {
|
|
653
|
+
const schema = dereference(
|
|
654
|
+
openApi,
|
|
655
|
+
schemaOrRef,
|
|
656
|
+
"Component schema reference",
|
|
657
|
+
);
|
|
658
|
+
declarations.push(
|
|
659
|
+
resolver.createDeclaration(resolver.componentName(rawName), schema),
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (const operationInfo of operations) {
|
|
664
|
+
const operationTypes = getOperationTypes(
|
|
665
|
+
openApi,
|
|
666
|
+
operationInfo,
|
|
667
|
+
options,
|
|
668
|
+
resolver,
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
if (operationTypes.paramsTypeName) {
|
|
672
|
+
declarations.push(
|
|
673
|
+
createParamsInterface(
|
|
674
|
+
openApi,
|
|
675
|
+
resolver,
|
|
676
|
+
operationTypes.paramsTypeName,
|
|
677
|
+
operationTypes.parameters,
|
|
678
|
+
),
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for (const [location, parameters] of Object.entries(
|
|
683
|
+
operationTypes.parametersByLocation,
|
|
684
|
+
)) {
|
|
685
|
+
const typeName = operationTypes.parameterTypeNames[location];
|
|
686
|
+
if (typeName)
|
|
687
|
+
declarations.push(
|
|
688
|
+
createParamsInterface(openApi, resolver, typeName, parameters),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const bodyContent = getRequestBodyContent(
|
|
693
|
+
openApi,
|
|
694
|
+
operationInfo.operation.requestBody,
|
|
695
|
+
);
|
|
696
|
+
const bodySchema = bodyContent?.mediaType?.schema;
|
|
697
|
+
if (
|
|
698
|
+
bodySchema &&
|
|
699
|
+
!bodySchema.$ref &&
|
|
700
|
+
(bodyContent.contentType === "multipart/form-data" ||
|
|
701
|
+
hasObjectShape(bodySchema))
|
|
702
|
+
) {
|
|
703
|
+
declarations.push(
|
|
704
|
+
resolver.createDeclaration(
|
|
705
|
+
`${operationTypes.operationTypeNameBase}Payload`,
|
|
706
|
+
bodySchema,
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
for (const [name, schema] of resolver.inlineTypes.entries()) {
|
|
713
|
+
if (!resolver.componentNamesHasValue(name))
|
|
714
|
+
declarations.push(resolver.createDeclaration(name, schema));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const wireSchemas = generateWireSchemaRegistry(openApi, operations, options);
|
|
718
|
+
declarations.push(
|
|
719
|
+
`export const typedApiWireSchemas = ${JSON.stringify(wireSchemas, null, 2)} as const;`,
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
return `${generatedFileHeader()}\n\n${declarations.join("\n\n")}\n`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export function generatedFileHeader() {
|
|
726
|
+
return `/* eslint-disable */
|
|
727
|
+
/* tslint:disable */
|
|
728
|
+
/*
|
|
729
|
+
* -------------------------------------------------------------------------------------
|
|
730
|
+
* ## THIS FILE WAS GENERATED BY typedapi-client-helpers ##
|
|
731
|
+
* ## DO NOT EDIT THIS FILE DIRECTLY ##
|
|
732
|
+
* ## ##
|
|
733
|
+
* ## AUTHOR: Stef Buurman ##
|
|
734
|
+
* ## SOURCE: https://github.com/Stef-Buurman/TypedApi/tree/main/npm/typed-api-client ##
|
|
735
|
+
* -------------------------------------------------------------------------------------
|
|
736
|
+
*/`;
|
|
737
|
+
}
|