typedapi-client-helpers 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE +21 -0
  3. package/README.md +221 -220
  4. package/dist/cjs/httpClient.js +258 -0
  5. package/dist/cjs/httpClient.js.map +1 -0
  6. package/dist/cjs/index.js +32 -0
  7. package/dist/cjs/index.js.map +1 -0
  8. package/dist/cjs/interfaces/ApiResult.js +3 -0
  9. package/dist/cjs/interfaces/ApiResult.js.map +1 -0
  10. package/dist/cjs/interfaces/Filter.js +3 -0
  11. package/dist/cjs/interfaces/Filter.js.map +1 -0
  12. package/dist/cjs/interfaces/SortType.js +3 -0
  13. package/dist/cjs/interfaces/SortType.js.map +1 -0
  14. package/dist/cjs/package.json +3 -0
  15. package/dist/cjs/types/ApiCallOptions.js +3 -0
  16. package/dist/cjs/types/ApiCallOptions.js.map +1 -0
  17. package/dist/cjs/types/ApiMethodArguments.js +3 -0
  18. package/dist/cjs/types/ApiMethodArguments.js.map +1 -0
  19. package/dist/cjs/types/ApiMethodOptions.js +3 -0
  20. package/dist/cjs/types/ApiMethodOptions.js.map +1 -0
  21. package/dist/cjs/types/ApiMethodTypes.js +3 -0
  22. package/dist/cjs/types/ApiMethodTypes.js.map +1 -0
  23. package/dist/cjs/types/HttpResponse.js +3 -0
  24. package/dist/cjs/types/HttpResponse.js.map +1 -0
  25. package/dist/cjs/types/httpClientTypes.js +3 -0
  26. package/dist/cjs/types/httpClientTypes.js.map +1 -0
  27. package/dist/cjs/utils/SortTypeFromSortDirection.js +43 -0
  28. package/dist/cjs/utils/SortTypeFromSortDirection.js.map +1 -0
  29. package/dist/cjs/utils/buildQuery.js +96 -0
  30. package/dist/cjs/utils/buildQuery.js.map +1 -0
  31. package/dist/cjs/utils/extractArgsCallbacksAndParams.js +51 -0
  32. package/dist/cjs/utils/extractArgsCallbacksAndParams.js.map +1 -0
  33. package/dist/cjs/utils/extractArgsToastsAndParams.js +49 -0
  34. package/dist/cjs/utils/extractArgsToastsAndParams.js.map +1 -0
  35. package/dist/cjs/utils/handleApiResponse.js +124 -0
  36. package/dist/cjs/utils/handleApiResponse.js.map +1 -0
  37. package/dist/cjs/utils/toFormData.js +58 -0
  38. package/dist/cjs/utils/toFormData.js.map +1 -0
  39. package/dist/cjs/utils/wireMapping.js +123 -0
  40. package/dist/cjs/utils/wireMapping.js.map +1 -0
  41. package/dist/esm/httpClient.js +251 -0
  42. package/dist/esm/httpClient.js.map +1 -0
  43. package/dist/esm/index.js +16 -0
  44. package/dist/esm/index.js.map +1 -0
  45. package/dist/esm/interfaces/ApiResult.js +2 -0
  46. package/dist/esm/interfaces/ApiResult.js.map +1 -0
  47. package/dist/esm/interfaces/Filter.js +2 -0
  48. package/dist/esm/interfaces/Filter.js.map +1 -0
  49. package/dist/esm/interfaces/SortType.js +2 -0
  50. package/dist/esm/interfaces/SortType.js.map +1 -0
  51. package/dist/esm/types/ApiCallOptions.js +2 -0
  52. package/dist/esm/types/ApiCallOptions.js.map +1 -0
  53. package/dist/esm/types/ApiMethodArguments.js +2 -0
  54. package/dist/esm/types/ApiMethodArguments.js.map +1 -0
  55. package/dist/esm/types/ApiMethodOptions.js +2 -0
  56. package/dist/esm/types/ApiMethodOptions.js.map +1 -0
  57. package/dist/esm/types/ApiMethodTypes.js +2 -0
  58. package/dist/esm/types/ApiMethodTypes.js.map +1 -0
  59. package/dist/esm/types/HttpResponse.js +2 -0
  60. package/dist/esm/types/HttpResponse.js.map +1 -0
  61. package/dist/esm/types/httpClientTypes.js +2 -0
  62. package/dist/esm/types/httpClientTypes.js.map +1 -0
  63. package/dist/esm/utils/SortTypeFromSortDirection.js +38 -0
  64. package/dist/esm/utils/SortTypeFromSortDirection.js.map +1 -0
  65. package/dist/esm/utils/buildQuery.js +93 -0
  66. package/dist/esm/utils/buildQuery.js.map +1 -0
  67. package/dist/esm/utils/extractArgsCallbacksAndParams.js +48 -0
  68. package/dist/esm/utils/extractArgsCallbacksAndParams.js.map +1 -0
  69. package/dist/esm/utils/extractArgsToastsAndParams.js +46 -0
  70. package/dist/esm/utils/extractArgsToastsAndParams.js.map +1 -0
  71. package/dist/esm/utils/handleApiResponse.js +121 -0
  72. package/dist/esm/utils/handleApiResponse.js.map +1 -0
  73. package/dist/esm/utils/toFormData.js +55 -0
  74. package/dist/esm/utils/toFormData.js.map +1 -0
  75. package/dist/esm/utils/wireMapping.js +119 -0
  76. package/dist/esm/utils/wireMapping.js.map +1 -0
  77. package/dist/types/httpClient.d.ts +63 -0
  78. package/dist/types/httpClient.d.ts.map +1 -0
  79. package/dist/types/index.d.ts +16 -0
  80. package/dist/types/index.d.ts.map +1 -0
  81. package/dist/types/interfaces/ApiResult.d.ts +32 -0
  82. package/dist/types/interfaces/ApiResult.d.ts.map +1 -0
  83. package/dist/types/interfaces/Filter.d.ts +31 -0
  84. package/dist/types/interfaces/Filter.d.ts.map +1 -0
  85. package/dist/types/interfaces/SortType.d.ts +7 -0
  86. package/dist/types/interfaces/SortType.d.ts.map +1 -0
  87. package/dist/types/types/ApiCallOptions.d.ts +26 -0
  88. package/dist/types/types/ApiCallOptions.d.ts.map +1 -0
  89. package/dist/types/types/ApiMethodArguments.d.ts +6 -0
  90. package/dist/types/types/ApiMethodArguments.d.ts.map +1 -0
  91. package/dist/types/types/ApiMethodOptions.d.ts +8 -0
  92. package/dist/types/types/ApiMethodOptions.d.ts.map +1 -0
  93. package/dist/types/types/ApiMethodTypes.d.ts +47 -0
  94. package/dist/types/types/ApiMethodTypes.d.ts.map +1 -0
  95. package/dist/types/types/HttpResponse.d.ts +2 -0
  96. package/dist/types/types/HttpResponse.d.ts.map +1 -0
  97. package/dist/types/types/httpClientTypes.d.ts +2 -0
  98. package/dist/types/types/httpClientTypes.d.ts.map +1 -0
  99. package/dist/types/utils/SortTypeFromSortDirection.d.ts +13 -0
  100. package/dist/types/utils/SortTypeFromSortDirection.d.ts.map +1 -0
  101. package/dist/types/utils/buildQuery.d.ts +16 -0
  102. package/dist/types/utils/buildQuery.d.ts.map +1 -0
  103. package/dist/types/utils/extractArgsCallbacksAndParams.d.ts +22 -0
  104. package/dist/types/utils/extractArgsCallbacksAndParams.d.ts.map +1 -0
  105. package/dist/types/utils/extractArgsToastsAndParams.d.ts +20 -0
  106. package/dist/types/utils/extractArgsToastsAndParams.d.ts.map +1 -0
  107. package/dist/types/utils/handleApiResponse.d.ts +11 -0
  108. package/dist/types/utils/handleApiResponse.d.ts.map +1 -0
  109. package/dist/types/utils/toFormData.d.ts +10 -0
  110. package/dist/types/utils/toFormData.d.ts.map +1 -0
  111. package/dist/types/utils/wireMapping.d.ts +30 -0
  112. package/dist/types/utils/wireMapping.d.ts.map +1 -0
  113. package/package.json +90 -82
  114. package/scripts/build.mjs +74 -0
  115. package/scripts/generate-api-minimal.mjs +568 -476
  116. package/scripts/openapi-generator/controller-template.mjs +1 -1
  117. package/scripts/openapi-generator/index.mjs +357 -113
  118. package/scripts/openapi-generator/method-template.mjs +201 -244
  119. package/scripts/openapi-generator/names.mjs +37 -86
  120. package/scripts/openapi-generator/openapi-utils.mjs +411 -147
  121. package/scripts/openapi-generator/schema.mjs +518 -238
  122. package/dist/index.cjs +0 -536
  123. package/dist/index.d.cts +0 -285
  124. package/dist/index.d.ts +0 -285
  125. package/dist/index.js +0 -495
@@ -1,44 +1,212 @@
1
1
  import {
2
2
  dereference,
3
+ getErrorResponses,
4
+ getParameterSchema,
3
5
  getRequestBodyContent,
4
6
  getResponseContent,
5
7
  getSuccessResponse,
6
8
  } from "./openapi-utils.mjs";
7
- import { camelCase, pascalCase, propertyKey, refName } from "./names.mjs";
9
+ import {
10
+ operationTypeName,
11
+ pascalCase,
12
+ propertyKey,
13
+ refName,
14
+ sanitizeIdentifier,
15
+ typePropertyName,
16
+ uniqueName,
17
+ } from "./names.mjs";
8
18
 
9
19
  function jsDocFromSchema(schema, indent = "") {
10
20
  const lines = [];
11
-
12
- if (schema?.description) {
21
+ if (schema?.description)
13
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)}`);
14
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
+ }
15
57
 
16
- if (schema?.format) {
17
- lines.push(`@format ${schema.format}`);
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);
18
70
  }
71
+ }
19
72
 
20
- if (Object.prototype.hasOwnProperty.call(schema ?? {}, "default")) {
21
- lines.push(`@default ${JSON.stringify(schema.default)}`);
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 };
22
100
  }
23
101
 
24
- if (schema?.deprecated) {
25
- lines.push("@deprecated");
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
+ };
26
109
  }
27
110
 
28
- if (lines.length === 0) {
29
- return "";
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
+ };
30
120
  }
31
121
 
32
- if (lines.length === 1) {
33
- return `${indent}/** ${lines[0]} */\n`;
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
+ };
34
156
  }
35
157
 
36
- return `${indent}/**\n${lines.map((line) => `${indent} * ${line}`).join("\n")}\n${indent} */\n`;
158
+ return { kind: "identity" };
37
159
  }
38
160
 
39
- function unionTypes(types) {
40
- const unique = [...new Set(types.filter(Boolean))];
41
- return unique.length > 0 ? unique.join(" | ") : "any";
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;
42
210
  }
43
211
 
44
212
  export class TypeResolver {
@@ -46,34 +214,39 @@ export class TypeResolver {
46
214
  this.openApi = openApi;
47
215
  this.options = options;
48
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);
49
228
  }
50
229
 
51
230
  resolve(schemaOrRef, contextName = "Anonymous", options = {}) {
52
- if (!schemaOrRef) {
53
- return "any";
54
- }
231
+ if (!schemaOrRef) return "unknown";
55
232
 
56
233
  if (schemaOrRef.$ref) {
57
- return refName(schemaOrRef.$ref);
234
+ dereference(this.openApi, schemaOrRef, "Schema reference");
235
+ return this.componentName(refName(schemaOrRef.$ref));
58
236
  }
59
237
 
60
- const schema = dereference(this.openApi, schemaOrRef) ?? schemaOrRef;
238
+ const schema =
239
+ dereference(this.openApi, schemaOrRef, "Schema reference") ?? schemaOrRef;
61
240
  const nullable =
62
241
  schema.nullable ||
63
242
  (Array.isArray(schema.type) && schema.type.includes("null"));
64
243
  let type = this.resolveNonNullable(schema, contextName, options);
65
-
66
- if (nullable && !type.includes("null")) {
67
- type = `${type} | null`;
68
- }
69
-
244
+ if (nullable && !/(^|\W)null(\W|$)/.test(type)) type = `${type} | null`;
70
245
  return type;
71
246
  }
72
247
 
73
248
  resolveNonNullable(schema, contextName, options = {}) {
74
- if (schema.const !== undefined) {
75
- return JSON.stringify(schema.const);
76
- }
249
+ if (schema.const !== undefined) return JSON.stringify(schema.const);
77
250
 
78
251
  if (Array.isArray(schema.enum)) {
79
252
  if (options.inlineEnumAsUnion) {
@@ -81,8 +254,7 @@ export class TypeResolver {
81
254
  this.enumLiteralValues(schema).map((value) => JSON.stringify(value)),
82
255
  );
83
256
  }
84
-
85
- return pascalCase(contextName);
257
+ return operationTypeName(contextName);
86
258
  }
87
259
 
88
260
  if (schema.oneOf || schema.anyOf) {
@@ -95,11 +267,20 @@ export class TypeResolver {
95
267
  }
96
268
 
97
269
  if (schema.allOf) {
98
- return schema.allOf
99
- .map((item, index) =>
100
- this.resolve(item, `${contextName}${index + 1}`, options),
101
- )
102
- .join(" & ");
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);
103
284
  }
104
285
 
105
286
  const schemaType = Array.isArray(schema.type)
@@ -113,9 +294,8 @@ export class TypeResolver {
113
294
  case "boolean":
114
295
  return "boolean";
115
296
  case "string":
116
- if (schema.format === "binary") {
297
+ if (schema.format === "binary")
117
298
  return options.binaryAsBlob ? "Blob" : "File";
118
- }
119
299
  return "string";
120
300
  case "array": {
121
301
  const itemType = this.resolve(
@@ -130,14 +310,12 @@ export class TypeResolver {
130
310
  }
131
311
  case "object":
132
312
  default:
133
- if (schema.properties) {
313
+ if (schema.properties)
134
314
  return this.createInlineObjectType(schema, contextName, options);
135
- }
136
-
137
315
  if (schema.additionalProperties) {
138
316
  const valueType =
139
317
  schema.additionalProperties === true
140
- ? "any"
318
+ ? "unknown"
141
319
  : this.resolve(
142
320
  schema.additionalProperties,
143
321
  `${contextName}Value`,
@@ -145,311 +323,413 @@ export class TypeResolver {
145
323
  );
146
324
  return `Record<string, ${valueType}>`;
147
325
  }
148
-
149
- return schemaType === "object" ? "Record<string, any>" : "any";
326
+ return schemaType === "object" ? "Record<string, unknown>" : "unknown";
150
327
  }
151
328
  }
152
329
 
153
- createInlineObjectType(schema, contextName, options = {}) {
154
- const name = pascalCase(contextName);
155
-
156
- if (!this.inlineTypes.has(name)) {
157
- this.inlineTypes.set(name, schema);
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++}`;
158
339
  }
159
-
340
+ if (!this.inlineTypes.has(name)) this.inlineTypes.set(name, schema);
160
341
  return name;
161
342
  }
162
343
 
163
- createInterface(name, schema) {
344
+ componentNamesHasValue(value) {
345
+ return [...this.componentNames.values()].includes(value);
346
+ }
347
+
348
+ objectMembers(name, schema) {
164
349
  const required = new Set(schema.required ?? []);
165
- const lines = [`export interface ${name} {`];
350
+ const lines = [];
166
351
 
167
- for (const [propertyName, propertySchema] of Object.entries(
168
- schema.properties ?? {},
169
- )) {
170
- // Required names must still be checked against the original OpenAPI name.
171
- const propertyRequired = required.has(propertyName);
172
- const optionalToken = propertyRequired ? "" : "?";
352
+ const propertyEntries = Object.entries(schema.properties ?? {});
353
+ assertUniqueTypePropertyNames(
354
+ propertyEntries.map(([propertyName]) => propertyName),
355
+ name,
356
+ );
173
357
 
358
+ for (const [propertyName, propertySchema] of propertyEntries) {
359
+ const localPropertyName = typePropertyName(propertyName);
360
+ const optionalToken = required.has(propertyName) ? "" : "?";
174
361
  const propertyType = this.resolve(
175
362
  propertySchema,
176
363
  `${name}${pascalCase(propertyName)}`,
177
- { inlineEnumAsUnion: true },
364
+ {
365
+ inlineEnumAsUnion: true,
366
+ },
367
+ );
368
+ const resolvedSchema = dereference(
369
+ this.openApi,
370
+ propertySchema,
371
+ "Property schema reference",
178
372
  );
179
-
180
- const resolvedSchema = dereference(this.openApi, propertySchema);
181
- const typescriptPropertyName = camelCase(propertyName);
182
-
183
373
  lines.push(
184
374
  jsDocFromSchema(resolvedSchema, " ") +
185
- ` ${propertyKey(typescriptPropertyName)}${optionalToken}: ${propertyType};`,
375
+ ` ${propertyKey(localPropertyName)}${optionalToken}: ${propertyType};`,
186
376
  );
187
377
  }
188
378
 
189
379
  if (schema.additionalProperties) {
190
380
  const valueType =
191
381
  schema.additionalProperties === true
192
- ? "any"
382
+ ? "unknown"
193
383
  : this.resolve(schema.additionalProperties, `${name}Value`, {
194
384
  inlineEnumAsUnion: true,
195
385
  });
196
-
197
386
  lines.push(` [key: string]: ${valueType};`);
198
387
  }
199
388
 
200
- lines.push("}");
201
- return lines.join("\n");
389
+ return lines;
202
390
  }
203
391
 
204
- createEnum(name, schema) {
205
- const values = schema.enum ?? [];
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);
206
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) {
207
422
  if (this.options.generateUnionEnums) {
208
- return `export type ${name} = ${unionTypes(this.enumLiteralValues(schema).map((value) => JSON.stringify(value)))};`;
423
+ return `export type ${name} = ${unionTypes(
424
+ this.enumLiteralValues(schema).map((value) => JSON.stringify(value)),
425
+ )};`;
209
426
  }
210
427
 
211
428
  const lines = [`export const ${name} = {`];
212
-
213
- for (const [index, value] of values.entries()) {
429
+ for (const [index, value] of (schema.enum ?? []).entries()) {
214
430
  const memberName = this.enumMemberName(schema, value, index);
215
431
  const enumValue = this.enumRuntimeValue(schema, value, index);
216
- lines.push(` ${memberName}: ${JSON.stringify(enumValue)},`);
432
+ lines.push(` ${propertyKey(memberName)}: ${JSON.stringify(enumValue)},`);
217
433
  }
218
-
219
434
  lines.push("} as const;");
220
435
  lines.push(`export type ${name} = (typeof ${name})[keyof typeof ${name}];`);
221
436
  return lines.join("\n");
222
437
  }
223
438
 
224
439
  enumMetadataNames(schema) {
225
- const enumNames =
440
+ const names =
226
441
  schema?.["x-enumNames"] ??
227
442
  schema?.["x-enum-varnames"] ??
228
443
  schema?.["x-enumVarnames"];
229
-
230
- return Array.isArray(enumNames) ? enumNames : [];
444
+ return Array.isArray(names) ? names : [];
231
445
  }
232
446
 
233
447
  enumLiteralValues(schema) {
234
448
  const values = schema.enum ?? [];
235
- const enumNames = this.enumMetadataNames(schema);
236
-
237
- if (this.options.enumNamesAsValues && enumNames.length > 0) {
238
- return values.map((value, index) => enumNames[index] ?? value);
239
- }
240
-
241
- return values;
449
+ const names = this.enumMetadataNames(schema);
450
+ return this.options.enumNamesAsValues && names.length > 0
451
+ ? values.map((value, index) => names[index] ?? value)
452
+ : values;
242
453
  }
243
454
 
244
455
  enumRuntimeValue(schema, value, index) {
245
- const enumNames = this.enumMetadataNames(schema);
246
-
247
- if (this.options.enumNamesAsValues && enumNames.length > 0) {
248
- return enumNames[index] ?? value;
249
- }
250
-
251
- return value;
456
+ const names = this.enumMetadataNames(schema);
457
+ return this.options.enumNamesAsValues && names.length > 0
458
+ ? (names[index] ?? value)
459
+ : value;
252
460
  }
253
461
 
254
462
  enumMemberName(schema, value, index) {
255
- const enumNames = this.enumMetadataNames(schema);
256
- const rawName = enumNames[index] ?? value;
257
- return pascalCase(String(rawName), "Value").replace(/^[0-9]/, "_$&");
463
+ const rawName = this.enumMetadataNames(schema)[index] ?? value;
464
+ return operationTypeName(String(rawName) || "Value");
258
465
  }
259
466
  }
260
467
 
261
- function shouldUseInterface(schema) {
262
- return Boolean(
263
- schema?.properties ||
264
- schema?.additionalProperties ||
265
- schema?.type === "object",
266
- );
468
+ function parameterTypeName(operationId, location) {
469
+ return `${operationId}${pascalCase(location)}Params`;
267
470
  }
268
471
 
269
- export function generateDataContracts(openApi, operations, options = {}) {
270
- const resolver = new TypeResolver(openApi, options);
271
- const declarations = [];
272
- const schemas = openApi.components?.schemas ?? {};
273
-
274
- const schemaEntries = Object.entries(schemas).sort(
275
- ([aName, aSchema], [bName, bSchema]) => {
276
- const aIsEnum = Array.isArray(aSchema.enum);
277
- const bIsEnum = Array.isArray(bSchema.enum);
278
- if (aIsEnum !== bIsEnum) return aIsEnum ? -1 : 1;
279
- return aName.localeCompare(bName);
280
- },
281
- );
282
-
283
- for (const [name, schema] of schemaEntries) {
284
- if (Array.isArray(schema.enum)) {
285
- declarations.push(resolver.createEnum(name, schema));
286
- } else if (shouldUseInterface(schema)) {
287
- declarations.push(resolver.createInterface(name, schema));
288
- } else {
289
- declarations.push(
290
- `export type ${name} = ${resolver.resolve(schema, name, { inlineEnumAsUnion: true })};`,
291
- );
292
- }
293
- }
294
-
295
- for (const operationInfo of operations) {
296
- const operationId = operationInfo.operation.operationId;
297
- if (!operationId) continue;
298
-
299
- const parameterProperties = operationInfo.parameters.map((parameter) => ({
300
- name: parameter.name,
301
- schema: parameter.schema ?? {},
302
- required: Boolean(parameter.required),
303
- }));
304
-
305
- if (parameterProperties.length > 0) {
306
- declarations.push(
307
- createParamsInterface(
308
- openApi,
309
- resolver,
310
- `${operationId}Params`,
311
- parameterProperties,
312
- ),
313
- );
314
- }
315
-
316
- const bodyContent = getRequestBodyContent(
317
- operationInfo.operation.requestBody,
318
- );
319
- const bodySchema = bodyContent?.mediaType?.schema;
320
-
321
- if (
322
- bodySchema &&
323
- !bodySchema.$ref &&
324
- (bodyContent.contentType === "multipart/form-data" ||
325
- shouldUseInterface(bodySchema))
326
- ) {
327
- const payloadName = `${operationId}Payload`;
328
- if (!schemas[payloadName]) {
329
- declarations.push(resolver.createInterface(payloadName, bodySchema));
330
- }
331
- }
332
-
333
- const successResponse = getSuccessResponse(
334
- operationInfo.operation,
335
- options.defaultResponseAsSuccess,
336
- );
337
- const responseContent = getResponseContent(successResponse?.response);
338
- const responseSchema = responseContent?.mediaType?.schema;
339
-
340
- if (responseSchema && !responseSchema.$ref) {
341
- resolver.resolve(responseSchema, `${operationId}Response`, {
342
- binaryAsBlob:
343
- responseContent?.contentType === "application/octet-stream",
344
- inlineEnumAsUnion: true,
345
- });
346
- }
347
- }
348
-
349
- for (const [name, schema] of resolver.inlineTypes.entries()) {
350
- if (!schemas[name]) {
351
- declarations.push(resolver.createInterface(name, schema));
352
- }
353
- }
354
-
355
- return `${generatedFileHeader()}\n\n${declarations.join("\n\n")}\n`;
472
+ function combinedParameterTypeName(operationId) {
473
+ return `${operationId}Params`;
356
474
  }
357
475
 
358
476
  function createParamsInterface(openApi, resolver, name, parameters) {
359
- const required = new Set(
360
- parameters
361
- .filter((parameter) => parameter.required)
362
- .map((parameter) => parameter.name),
363
- );
364
477
  const lines = [`export interface ${name} {`];
365
-
478
+ assertUniqueTypePropertyNames(
479
+ parameters.map((parameter) => parameter.name),
480
+ name,
481
+ );
366
482
  for (const parameter of parameters) {
367
- const parameterSchema = dereference(openApi, parameter.schema);
368
- const optionalToken = required.has(parameter.name) ? "" : "?";
483
+ const parameterSchema = getParameterSchema(openApi, parameter);
484
+ const optionalToken =
485
+ parameter.required || parameter.in === "path" ? "" : "?";
369
486
  const parameterType = resolver.resolve(
370
- parameter.schema,
487
+ parameterSchema,
371
488
  `${name}${pascalCase(parameter.name)}`,
372
- { inlineEnumAsUnion: true },
489
+ {
490
+ inlineEnumAsUnion: true,
491
+ },
373
492
  );
374
493
  lines.push(
375
494
  jsDocFromSchema(parameterSchema, " ") +
376
- ` ${propertyKey(parameter.name)}${optionalToken}: ${parameterType};`,
495
+ ` ${propertyKey(typePropertyName(parameter.name))}${optionalToken}: ${parameterType};`,
377
496
  );
378
497
  }
379
-
380
498
  lines.push("}");
381
499
  return lines.join("\n");
382
500
  }
383
501
 
384
- export function getOperationTypes(openApi, operationInfo, options = {}) {
385
- const resolver = new TypeResolver(openApi, options);
386
- const operationId = operationInfo.operation.operationId;
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;
387
526
  const parameters = operationInfo.parameters ?? [];
388
- const pathParams = parameters.filter((parameter) => parameter.in === "path");
389
- const queryParams = parameters.filter(
390
- (parameter) => parameter.in === "query",
527
+ const parametersByLocation = Object.fromEntries(
528
+ ["path", "query", "header", "cookie"].map((location) => [
529
+ location,
530
+ parameters.filter((parameter) => parameter.in === location),
531
+ ]),
391
532
  );
533
+
392
534
  const requestBodyContent = getRequestBodyContent(
535
+ openApi,
393
536
  operationInfo.operation.requestBody,
394
537
  );
395
538
  const requestBodySchema = requestBodyContent?.mediaType?.schema;
396
- const successResponse = getSuccessResponse(
397
- operationInfo.operation,
398
- options.defaultResponseAsSuccess,
399
- );
400
- const responseContent = getResponseContent(successResponse?.response);
401
- const responseSchema = responseContent?.mediaType?.schema;
402
-
403
539
  let bodyType;
404
540
  if (requestBodySchema) {
405
541
  if (requestBodySchema.$ref) {
406
- bodyType = resolver.resolve(requestBodySchema, `${operationId}Payload`);
542
+ bodyType = resolver.resolve(requestBodySchema, `${operationTypeNameBase}Payload`);
407
543
  } else if (
408
544
  requestBodyContent?.contentType === "multipart/form-data" ||
409
- shouldUseInterface(requestBodySchema)
545
+ hasObjectShape(requestBodySchema)
410
546
  ) {
411
- bodyType = `${operationId}Payload`;
547
+ bodyType = `${operationTypeNameBase}Payload`;
412
548
  } else {
413
- bodyType = resolver.resolve(requestBodySchema, `${operationId}Payload`, {
549
+ bodyType = resolver.resolve(requestBodySchema, `${operationTypeNameBase}Payload`, {
414
550
  inlineEnumAsUnion: true,
415
551
  });
416
552
  }
417
553
  }
418
554
 
419
- let responseType = "void";
420
- if (responseSchema) {
421
- responseType = resolver.resolve(responseSchema, `${operationId}Response`, {
422
- binaryAsBlob: responseContent?.contentType === "application/octet-stream",
423
- inlineEnumAsUnion: true,
424
- });
425
- }
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
+ : {};
426
601
 
427
602
  return {
428
603
  operationId,
429
- methodName: operationId
430
- ? operationId.charAt(0).toLowerCase() + operationId.slice(1)
431
- : undefined,
432
- paramsTypeName: parameters.length > 0 ? `${operationId}Params` : undefined,
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
+ ),
433
618
  bodyType,
619
+ bodyRequired: Boolean(requestBodyContent?.requestBody?.required),
620
+ bodyWireSchemaKey: requestBodySchema
621
+ ? operationWireSchemaKey(operationId, "body")
622
+ : undefined,
434
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,
435
632
  contentType: requestBodyContent?.contentType,
436
633
  responseContentType: responseContent?.contentType,
437
- hasPathParams: pathParams.length > 0,
438
- hasQueryParams: queryParams.length > 0,
439
- pathParams,
440
- queryParams,
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"],
441
639
  };
442
640
  }
443
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
+
444
723
  export function generatedFileHeader() {
445
724
  return `/* eslint-disable */
446
725
  /* tslint:disable */
447
726
  /*
448
- * --------------------------------------------------------------------------------
449
- * ## THIS FILE WAS GENERATED BY typedapi-client-helpers ##
450
- * ## ##
451
- * ## AUTHOR: Stef Buurman ##
727
+ * -------------------------------------------------------------------------------------
728
+ * ## THIS FILE WAS GENERATED BY typedapi-client-helpers ##
729
+ * ## DO NOT EDIT THIS FILE DIRECTLY ##
730
+ * ## ##
731
+ * ## AUTHOR: Stef Buurman ##
452
732
  * ## SOURCE: https://github.com/Stef-Buurman/TypedApi/tree/main/npm/typed-api-client ##
453
- * --------------------------------------------------------------------------------
733
+ * -------------------------------------------------------------------------------------
454
734
  */`;
455
735
  }