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.
@@ -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 errorType = unionTypes(errorTypes, "unknown");
580
- const responseSchema = responseContent?.mediaType?.schema;
581
- const errorSchemas = errorResponses
582
- .map(({ response }) => getResponseContent(response)?.mediaType?.schema)
583
- .filter(Boolean);
584
-
585
- const hasNonBodyInput = parameters.length > 0;
586
- const paginated =
587
- Boolean(operationInfo.operation["x-typedapi-pagination"]) ||
588
- /(?:Api)?Pagination(?:Sort)?Response\b|Paged(?:Response|Result)?\b|Paginated(?:Response|Result)?\b/.test(
589
- responseType,
590
- );
591
- const isPurePaginatedQuery =
592
- paginated &&
593
- parametersByLocation.query.length > 0 &&
594
- !parametersByLocation.path.length &&
595
- !parametersByLocation.header.length &&
596
- !parametersByLocation.cookie.length &&
597
- !bodyType;
598
- const parameterTypeNames = isPurePaginatedQuery
599
- ? { query: parameterTypeName(operationTypeNameBase, "query") }
600
- : {};
601
-
602
- return {
603
- operationId,
604
- operationTypeNameBase,
605
- originalOperationId: operationInfo.originalOperationId,
606
- methodName: operationInfo.methodName,
607
- methodNameSource: operationInfo.methodNameSource,
608
- paramsTypeName:
609
- hasNonBodyInput && !isPurePaginatedQuery
610
- ? combinedParameterTypeName(operationTypeNameBase)
611
- : undefined,
612
- parameterTypeNames,
613
- parameters,
614
- parametersByLocation,
615
- allParametersOptional: parameters.every(
616
- (parameter) => parameter.in !== "path" && !parameter.required,
617
- ),
618
- bodyType,
619
- bodyRequired: Boolean(requestBodyContent?.requestBody?.required),
620
- bodyWireSchemaKey: requestBodySchema
621
- ? operationWireSchemaKey(operationId, "body")
622
- : undefined,
623
- responseType,
624
- responseWireSchemaKey: responseSchema
625
- ? operationWireSchemaKey(operationId, "response")
626
- : undefined,
627
- errorType,
628
- errorWireSchemaKey:
629
- errorSchemas.length > 0
630
- ? operationWireSchemaKey(operationId, "error")
631
- : undefined,
632
- contentType: requestBodyContent?.contentType,
633
- responseContentType: responseContent?.contentType,
634
- hasPathParams: parametersByLocation.path.length > 0,
635
- hasQueryParams: parametersByLocation.query.length > 0,
636
- hasHeaderParams: parametersByLocation.header.length > 0,
637
- hasCookieParams: parametersByLocation.cookie.length > 0,
638
- paginationMetadata: operationInfo.operation["x-typedapi-pagination"],
639
- };
640
- }
641
-
642
- export function generateDataContracts(openApi, operations, options = {}) {
643
- const resolver = new TypeResolver(openApi, options);
644
- const declarations = [];
645
- const schemas = openApi.components?.schemas ?? {};
646
-
647
- const entries = Object.entries(schemas).sort(([a], [b]) =>
648
- a.localeCompare(b),
649
- );
650
- for (const [rawName, schemaOrRef] of entries) {
651
- const schema = dereference(
652
- openApi,
653
- schemaOrRef,
654
- "Component schema reference",
655
- );
656
- declarations.push(
657
- resolver.createDeclaration(resolver.componentName(rawName), schema),
658
- );
659
- }
660
-
661
- for (const operationInfo of operations) {
662
- const operationTypes = getOperationTypes(
663
- openApi,
664
- operationInfo,
665
- options,
666
- resolver,
667
- );
668
-
669
- if (operationTypes.paramsTypeName) {
670
- declarations.push(
671
- createParamsInterface(
672
- openApi,
673
- resolver,
674
- operationTypes.paramsTypeName,
675
- operationTypes.parameters,
676
- ),
677
- );
678
- }
679
-
680
- for (const [location, parameters] of Object.entries(
681
- operationTypes.parametersByLocation,
682
- )) {
683
- const typeName = operationTypes.parameterTypeNames[location];
684
- if (typeName)
685
- declarations.push(
686
- createParamsInterface(openApi, resolver, typeName, parameters),
687
- );
688
- }
689
-
690
- const bodyContent = getRequestBodyContent(
691
- openApi,
692
- operationInfo.operation.requestBody,
693
- );
694
- const bodySchema = bodyContent?.mediaType?.schema;
695
- if (
696
- bodySchema &&
697
- !bodySchema.$ref &&
698
- (bodyContent.contentType === "multipart/form-data" ||
699
- hasObjectShape(bodySchema))
700
- ) {
701
- declarations.push(
702
- resolver.createDeclaration(
703
- `${operationTypes.operationTypeNameBase}Payload`,
704
- bodySchema,
705
- ),
706
- );
707
- }
708
- }
709
-
710
- for (const [name, schema] of resolver.inlineTypes.entries()) {
711
- if (!resolver.componentNamesHasValue(name))
712
- declarations.push(resolver.createDeclaration(name, schema));
713
- }
714
-
715
- const wireSchemas = generateWireSchemaRegistry(openApi, operations, options);
716
- declarations.push(
717
- `export const typedApiWireSchemas = ${JSON.stringify(wireSchemas, null, 2)} as const;`,
718
- );
719
-
720
- return `${generatedFileHeader()}\n\n${declarations.join("\n\n")}\n`;
721
- }
722
-
723
- export function generatedFileHeader() {
724
- return `/* eslint-disable */
725
- /* tslint:disable */
726
- /*
727
- * -------------------------------------------------------------------------------------
728
- * ## THIS FILE WAS GENERATED BY typedapi-client-helpers ##
729
- * ## DO NOT EDIT THIS FILE DIRECTLY ##
730
- * ## ##
731
- * ## AUTHOR: Stef Buurman ##
732
- * ## SOURCE: https://github.com/Stef-Buurman/TypedApi/tree/main/npm/typed-api-client ##
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
+ }