typedapi-client-helpers 0.2.1 → 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.
Files changed (124) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +231 -220
  3. package/dist/cjs/httpClient.js +258 -0
  4. package/dist/cjs/httpClient.js.map +1 -0
  5. package/dist/cjs/index.js +32 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/cjs/interfaces/ApiResult.js +3 -0
  8. package/dist/cjs/interfaces/ApiResult.js.map +1 -0
  9. package/dist/cjs/interfaces/Filter.js +3 -0
  10. package/dist/cjs/interfaces/Filter.js.map +1 -0
  11. package/dist/cjs/interfaces/SortType.js +3 -0
  12. package/dist/cjs/interfaces/SortType.js.map +1 -0
  13. package/dist/cjs/package.json +3 -0
  14. package/dist/cjs/types/ApiCallOptions.js +3 -0
  15. package/dist/cjs/types/ApiCallOptions.js.map +1 -0
  16. package/dist/cjs/types/ApiMethodArguments.js +3 -0
  17. package/dist/cjs/types/ApiMethodArguments.js.map +1 -0
  18. package/dist/cjs/types/ApiMethodOptions.js +3 -0
  19. package/dist/cjs/types/ApiMethodOptions.js.map +1 -0
  20. package/dist/cjs/types/ApiMethodTypes.js +3 -0
  21. package/dist/cjs/types/ApiMethodTypes.js.map +1 -0
  22. package/dist/cjs/types/HttpResponse.js +3 -0
  23. package/dist/cjs/types/HttpResponse.js.map +1 -0
  24. package/dist/cjs/types/httpClientTypes.js +3 -0
  25. package/dist/cjs/types/httpClientTypes.js.map +1 -0
  26. package/dist/cjs/utils/SortTypeFromSortDirection.js +43 -0
  27. package/dist/cjs/utils/SortTypeFromSortDirection.js.map +1 -0
  28. package/dist/cjs/utils/buildQuery.js +96 -0
  29. package/dist/cjs/utils/buildQuery.js.map +1 -0
  30. package/dist/cjs/utils/extractArgsCallbacksAndParams.js +51 -0
  31. package/dist/cjs/utils/extractArgsCallbacksAndParams.js.map +1 -0
  32. package/dist/cjs/utils/extractArgsToastsAndParams.js +49 -0
  33. package/dist/cjs/utils/extractArgsToastsAndParams.js.map +1 -0
  34. package/dist/cjs/utils/handleApiResponse.js +132 -0
  35. package/dist/cjs/utils/handleApiResponse.js.map +1 -0
  36. package/dist/cjs/utils/toFormData.js +58 -0
  37. package/dist/cjs/utils/toFormData.js.map +1 -0
  38. package/dist/cjs/utils/wireMapping.js +123 -0
  39. package/dist/cjs/utils/wireMapping.js.map +1 -0
  40. package/dist/esm/httpClient.js +251 -0
  41. package/dist/esm/httpClient.js.map +1 -0
  42. package/dist/esm/index.js +16 -0
  43. package/dist/esm/index.js.map +1 -0
  44. package/dist/esm/interfaces/ApiResult.js +2 -0
  45. package/dist/esm/interfaces/ApiResult.js.map +1 -0
  46. package/dist/esm/interfaces/Filter.js +2 -0
  47. package/dist/esm/interfaces/Filter.js.map +1 -0
  48. package/dist/esm/interfaces/SortType.js +2 -0
  49. package/dist/esm/interfaces/SortType.js.map +1 -0
  50. package/dist/esm/types/ApiCallOptions.js +2 -0
  51. package/dist/esm/types/ApiCallOptions.js.map +1 -0
  52. package/dist/esm/types/ApiMethodArguments.js +2 -0
  53. package/dist/esm/types/ApiMethodArguments.js.map +1 -0
  54. package/dist/esm/types/ApiMethodOptions.js +2 -0
  55. package/dist/esm/types/ApiMethodOptions.js.map +1 -0
  56. package/dist/esm/types/ApiMethodTypes.js +2 -0
  57. package/dist/esm/types/ApiMethodTypes.js.map +1 -0
  58. package/dist/esm/types/HttpResponse.js +2 -0
  59. package/dist/esm/types/HttpResponse.js.map +1 -0
  60. package/dist/esm/types/httpClientTypes.js +2 -0
  61. package/dist/esm/types/httpClientTypes.js.map +1 -0
  62. package/dist/esm/utils/SortTypeFromSortDirection.js +38 -0
  63. package/dist/esm/utils/SortTypeFromSortDirection.js.map +1 -0
  64. package/dist/esm/utils/buildQuery.js +93 -0
  65. package/dist/esm/utils/buildQuery.js.map +1 -0
  66. package/dist/esm/utils/extractArgsCallbacksAndParams.js +48 -0
  67. package/dist/esm/utils/extractArgsCallbacksAndParams.js.map +1 -0
  68. package/dist/esm/utils/extractArgsToastsAndParams.js +46 -0
  69. package/dist/esm/utils/extractArgsToastsAndParams.js.map +1 -0
  70. package/dist/esm/utils/handleApiResponse.js +128 -0
  71. package/dist/esm/utils/handleApiResponse.js.map +1 -0
  72. package/dist/esm/utils/toFormData.js +55 -0
  73. package/dist/esm/utils/toFormData.js.map +1 -0
  74. package/dist/esm/utils/wireMapping.js +119 -0
  75. package/dist/esm/utils/wireMapping.js.map +1 -0
  76. package/dist/types/httpClient.d.ts +63 -0
  77. package/dist/types/httpClient.d.ts.map +1 -0
  78. package/dist/types/index.d.ts +16 -0
  79. package/dist/types/index.d.ts.map +1 -0
  80. package/dist/types/interfaces/ApiResult.d.ts +41 -0
  81. package/dist/types/interfaces/ApiResult.d.ts.map +1 -0
  82. package/dist/types/interfaces/Filter.d.ts +31 -0
  83. package/dist/types/interfaces/Filter.d.ts.map +1 -0
  84. package/dist/types/interfaces/SortType.d.ts +7 -0
  85. package/dist/types/interfaces/SortType.d.ts.map +1 -0
  86. package/dist/types/types/ApiCallOptions.d.ts +26 -0
  87. package/dist/types/types/ApiCallOptions.d.ts.map +1 -0
  88. package/dist/types/types/ApiMethodArguments.d.ts +6 -0
  89. package/dist/types/types/ApiMethodArguments.d.ts.map +1 -0
  90. package/dist/types/types/ApiMethodOptions.d.ts +8 -0
  91. package/dist/types/types/ApiMethodOptions.d.ts.map +1 -0
  92. package/dist/types/types/ApiMethodTypes.d.ts +47 -0
  93. package/dist/types/types/ApiMethodTypes.d.ts.map +1 -0
  94. package/dist/types/types/HttpResponse.d.ts +2 -0
  95. package/dist/types/types/HttpResponse.d.ts.map +1 -0
  96. package/dist/types/types/httpClientTypes.d.ts +2 -0
  97. package/dist/types/types/httpClientTypes.d.ts.map +1 -0
  98. package/dist/types/utils/SortTypeFromSortDirection.d.ts +13 -0
  99. package/dist/types/utils/SortTypeFromSortDirection.d.ts.map +1 -0
  100. package/dist/types/utils/buildQuery.d.ts +16 -0
  101. package/dist/types/utils/buildQuery.d.ts.map +1 -0
  102. package/dist/types/utils/extractArgsCallbacksAndParams.d.ts +22 -0
  103. package/dist/types/utils/extractArgsCallbacksAndParams.d.ts.map +1 -0
  104. package/dist/types/utils/extractArgsToastsAndParams.d.ts +20 -0
  105. package/dist/types/utils/extractArgsToastsAndParams.d.ts.map +1 -0
  106. package/dist/types/utils/handleApiResponse.d.ts +12 -0
  107. package/dist/types/utils/handleApiResponse.d.ts.map +1 -0
  108. package/dist/types/utils/toFormData.d.ts +10 -0
  109. package/dist/types/utils/toFormData.d.ts.map +1 -0
  110. package/dist/types/utils/wireMapping.d.ts +30 -0
  111. package/dist/types/utils/wireMapping.d.ts.map +1 -0
  112. package/package.json +90 -84
  113. package/scripts/build.mjs +74 -0
  114. package/scripts/generate-api-minimal.mjs +568 -476
  115. package/scripts/openapi-generator/controller-template.mjs +1 -1
  116. package/scripts/openapi-generator/index.mjs +348 -113
  117. package/scripts/openapi-generator/method-template.mjs +405 -440
  118. package/scripts/openapi-generator/names.mjs +37 -86
  119. package/scripts/openapi-generator/openapi-utils.mjs +411 -147
  120. package/scripts/openapi-generator/schema.mjs +737 -455
  121. package/dist/index.cjs +0 -536
  122. package/dist/index.d.cts +0 -285
  123. package/dist/index.d.ts +0 -285
  124. package/dist/index.js +0 -495
@@ -1,440 +1,405 @@
1
- import { getOperationTypes, generatedFileHeader } from "./schema.mjs";
2
- import { pascalCase, sanitizeIdentifier } from "./names.mjs";
3
-
4
- function unique(values) {
5
- return [...new Set(values.filter(Boolean))];
6
- }
7
-
8
- function createImport(names, from, typeOnly = false) {
9
- const cleanNames = unique(names).sort((a, b) => a.localeCompare(b));
10
- if (cleanNames.length === 0) return "";
11
- const keyword = typeOnly ? "import type" : "import";
12
- if (cleanNames.length <= 3) {
13
- return `${keyword} { ${cleanNames.join(", ")} } from ${JSON.stringify(from)};`;
14
- }
15
- return `${keyword} {\n${cleanNames.map((name) => ` ${name},`).join("\n")}\n} from ${JSON.stringify(from)};`;
16
- }
17
-
18
- function isPrimitiveType(type) {
19
- return /^(void|any|unknown|string|number|boolean|Blob|File|Record<)/.test(
20
- type,
21
- );
22
- }
23
-
24
- function extractTopLevelTypeNames(type) {
25
- const withoutLiterals = String(type ?? "").replace(
26
- /"(?:\\.|[^"])*"|'(?:\\.|[^'])*'/g,
27
- "",
28
- );
29
- const matches = withoutLiterals.match(/\b[A-Z][A-Za-z0-9_]*\b/g) ?? [];
30
- return matches.filter(
31
- (name) =>
32
- ![
33
- "Array",
34
- "Record",
35
- "Promise",
36
- "HttpResponse",
37
- "Blob",
38
- "File",
39
- "ApiResult",
40
- "RequestParams",
41
- "FormData",
42
- ].includes(name),
43
- );
44
- }
45
-
46
- function pathExpression(
47
- routePath,
48
- pathParams,
49
- paramsObjectName = "pathParams",
50
- ) {
51
- const parts = [];
52
- let cursor = 0;
53
- const regex = /\{([^}]+)\}/g;
54
- let match;
55
-
56
- while ((match = regex.exec(routePath))) {
57
- const [placeholder, paramName] = match;
58
- const before = routePath.slice(cursor, match.index);
59
- if (before) parts.push(JSON.stringify(before));
60
- parts.push(
61
- `encodeURIComponent(String(${paramsObjectName}.${sanitizeIdentifier(paramName)}))`,
62
- );
63
- cursor = match.index + placeholder.length;
64
- }
65
-
66
- const after = routePath.slice(cursor);
67
- if (after) parts.push(JSON.stringify(after));
68
-
69
- if (parts.length === 0) return JSON.stringify(routePath);
70
-
71
- return parts
72
- .map((part) =>
73
- /^"/.test(part) ? part.slice(1, -1).replace(/`/g, "\\`") : `\${${part}}`,
74
- )
75
- .join("")
76
- .replace(/^/, "`")
77
- .replace(/$/, "`");
78
- }
79
-
80
- function requestFormat(responseContentType, responseType) {
81
- if (responseType === "void") return undefined;
82
- if (!responseContentType) return undefined;
83
- if (responseContentType.includes("json")) return "json";
84
- if (responseContentType.startsWith("text/")) return "text";
85
- if (
86
- responseContentType === "application/octet-stream" ||
87
- responseType === "Blob"
88
- )
89
- return "blob";
90
- return undefined;
91
- }
92
-
93
- function contentTypeEnum(contentType) {
94
- switch (contentType) {
95
- case "application/json":
96
- return "ContentType.Json";
97
- case "application/vnd.api+json":
98
- return "ContentType.JsonApi";
99
- case "multipart/form-data":
100
- return "ContentType.FormData";
101
- case "application/x-www-form-urlencoded":
102
- return "ContentType.UrlEncoded";
103
- case "text/plain":
104
- return "ContentType.Text";
105
- default:
106
- return contentType?.includes("json") ? "ContentType.Json" : undefined;
107
- }
108
- }
109
-
110
- function queryExpression(operationTypes, queryValueName = "query") {
111
- if (!operationTypes.hasQueryParams) return undefined;
112
-
113
- if (!operationTypes.hasPathParams) {
114
- return `${queryValueName} ?? {}`;
115
- }
116
-
117
- const entries = operationTypes.queryParams.map((parameter) => {
118
- const identifier = sanitizeIdentifier(parameter.name);
119
- return `${JSON.stringify(parameter.name)}: pathParams.${identifier}`;
120
- });
121
-
122
- return `{ ${entries.join(", ")} }`;
123
- }
124
-
125
- function requestItems(operationInfo, operationTypes, options = {}) {
126
- const items = [
127
- `path: ${pathExpression(operationInfo.routePath, operationTypes.pathParams)}`,
128
- `method: ${JSON.stringify(operationInfo.method)}`,
129
- ];
130
-
131
- const query = queryExpression(
132
- operationTypes,
133
- options.queryValueName ?? "query",
134
- );
135
- if (query) items.push(`query: ${query}`);
136
-
137
- if (operationTypes.bodyType) {
138
- items.push(`body: ${options.bodyExpression ?? "data"}`);
139
- }
140
-
141
- const type = contentTypeEnum(operationTypes.contentType);
142
- if (type) items.push(`type: ${type}`);
143
-
144
- const format = requestFormat(
145
- operationTypes.responseContentType,
146
- operationTypes.responseType,
147
- );
148
- if (format) items.push(`format: ${JSON.stringify(format)}`);
149
-
150
- items.push("...params");
151
- return items;
152
- }
153
-
154
- function callbackOptions(defaultHandlers) {
155
- if (!defaultHandlers) return "{ onSuccess, onError }";
156
-
157
- return "{ onSuccess: onSuccess ?? typedApiDefaultSuccessHandler, onError: onError ?? typedApiDefaultErrorHandler }";
158
- }
159
-
160
- function optionsType(operationTypes) {
161
- return `ApiMethodOptions<${operationTypes.responseType}, any, RequestParams>`;
162
- }
163
-
164
- function promiseType(operationTypes) {
165
- return `Promise<ApiResult<${operationTypes.responseType}>>`;
166
- }
167
-
168
- function isPaginated(operationTypes) {
169
- return /(?:Api)?Pagination(?:Sort)?Response\b|Paged(?:Response|Result)?\b|Paginated(?:Response|Result)?\b/.test(
170
- operationTypes.responseType,
171
- );
172
- }
173
-
174
- function methodDescription(operationInfo, operationTypes) {
175
- const description =
176
- operationInfo.operation.description ||
177
- operationInfo.operation.summary ||
178
- "No description";
179
- const tags = (operationInfo.operation.tags ?? []).join(", ");
180
-
181
- return `/**\n * ${description.replace(/\n/g, "\n * ")}\n *\n * @tags ${tags}\n * @name ${operationTypes.operationId}\n * @request ${operationInfo.method}:${operationInfo.routePath}\n */`;
182
- }
183
-
184
- function createRequestCall(operationInfo, operationTypes, options = {}) {
185
- const items = requestItems(operationInfo, operationTypes, options)
186
- .map((item) => ` ${item},`)
187
- .join("\n");
188
-
189
- return `request<${operationTypes.responseType}, any>({\n${items}\n })`;
190
- }
191
-
192
- function createPaginatedMethod(operationInfo, operationTypes, defaultHandlers) {
193
- const queryTypeName = `${pascalCase(operationTypes.methodName)}Query`;
194
- const requestCall = createRequestCall(operationInfo, operationTypes, {
195
- queryValueName: "builtQuery",
196
- });
197
-
198
- return `${methodDescription(operationInfo, operationTypes)}
199
- export async function ${operationTypes.methodName}(
200
- filters: FilterFormValues<${queryTypeName}>[] = [],
201
- page = 1,
202
- pageSize = 100,
203
- sortBy: SortableKeys<${operationTypes.responseType}> | null = null,
204
- sortDirection?: SortDirection,
205
- options: ${optionsType(operationTypes)} = {},
206
- ): ${promiseType(operationTypes)} {
207
- const { onSuccess, onError, params } = options;
208
- const builtQuery = buildQuery<
209
- ${queryTypeName},
210
- UnwrapArray<ExtractDataIfPaginated<${operationTypes.responseType}>>
211
- >(filters, page, pageSize, sortBy, sortDirection);
212
-
213
- return handleApiResponse<${operationTypes.responseType}, any>(
214
- () => ${requestCall},
215
- ${callbackOptions(defaultHandlers)},
216
- );
217
- }`;
218
- }
219
-
220
- function createSimpleQueryMethod(
221
- operationInfo,
222
- operationTypes,
223
- defaultHandlers,
224
- ) {
225
- const queryTypeName = `${pascalCase(operationTypes.methodName)}Query`;
226
- const requestCall = createRequestCall(operationInfo, operationTypes, {
227
- queryValueName: "query",
228
- });
229
-
230
- return `${methodDescription(operationInfo, operationTypes)}
231
- export async function ${operationTypes.methodName}(
232
- query?: ${queryTypeName},
233
- options: ${optionsType(operationTypes)} = {},
234
- ): ${promiseType(operationTypes)} {
235
- const { onSuccess, onError, params } = options;
236
-
237
- return handleApiResponse<${operationTypes.responseType}, any>(
238
- () => ${requestCall},
239
- ${callbackOptions(defaultHandlers)},
240
- );
241
- }`;
242
- }
243
-
244
- function wrapperArguments(operationTypes) {
245
- const args = [];
246
-
247
- if (operationTypes.paramsTypeName && operationTypes.hasPathParams) {
248
- args.push(`pathParams: ${operationTypes.paramsTypeName}`);
249
- }
250
-
251
- if (operationTypes.bodyType) {
252
- args.push(`data: ${operationTypes.bodyType}`);
253
- }
254
-
255
- args.push(`options: ${optionsType(operationTypes)} = {}`);
256
- return args.join(",\n ");
257
- }
258
-
259
- function createRegularMethod(operationInfo, operationTypes, defaultHandlers) {
260
- const isFormData = operationTypes.contentType === "multipart/form-data";
261
- const requestCall = createRequestCall(operationInfo, operationTypes, {
262
- bodyExpression: isFormData ? "formData" : "data",
263
- });
264
-
265
- const formDataLine = isFormData
266
- ? "\n const formData = toFormData(data);\n"
267
- : "";
268
-
269
- return `${methodDescription(operationInfo, operationTypes)}
270
- export async function ${operationTypes.methodName}(
271
- ${wrapperArguments(operationTypes)}
272
- ): ${promiseType(operationTypes)} {
273
- const { onSuccess, onError, params } = options;${formDataLine}
274
- return handleApiResponse<${operationTypes.responseType}, any>(
275
- () => ${requestCall},
276
- ${callbackOptions(defaultHandlers)},
277
- );
278
- }`;
279
- }
280
-
281
- function createMethod(operationInfo, operationTypes, defaultHandlers, options) {
282
- if (
283
- operationTypes.hasQueryParams &&
284
- !operationTypes.hasPathParams &&
285
- options.useFilterFormValues !== false &&
286
- isPaginated(operationTypes)
287
- ) {
288
- return createPaginatedMethod(
289
- operationInfo,
290
- operationTypes,
291
- defaultHandlers,
292
- );
293
- }
294
-
295
- if (
296
- operationTypes.hasQueryParams &&
297
- !operationTypes.hasPathParams &&
298
- !operationTypes.bodyType
299
- ) {
300
- return createSimpleQueryMethod(
301
- operationInfo,
302
- operationTypes,
303
- defaultHandlers,
304
- );
305
- }
306
-
307
- return createRegularMethod(operationInfo, operationTypes, defaultHandlers);
308
- }
309
-
310
- export function methodFileNameForController(controllerName) {
311
- return `${controllerName}.api.ts`;
312
- }
313
-
314
- export function controllerNameForOperation(operationInfo, options = {}) {
315
- if (options.moduleNameFirstTag !== false) {
316
- const [tag] = operationInfo.operation.tags ?? [];
317
- if (tag) return pascalCase(tag, "Default");
318
- }
319
-
320
- const firstPathPart =
321
- operationInfo.routePath.split("/").filter(Boolean)[1] ??
322
- operationInfo.routePath.split("/").filter(Boolean)[0] ??
323
- "Default";
324
-
325
- return pascalCase(firstPathPart, "Default");
326
- }
327
-
328
- export function generateMethodFile(
329
- openApi,
330
- controllerName,
331
- operations,
332
- options = {},
333
- ) {
334
- const dataContractImports = [];
335
- const helperTypeImports = ["ApiResult", "ApiMethodOptions"];
336
- const helperValueImports = ["handleApiResponse"];
337
- const methods = [];
338
- const queryAliases = [];
339
- let needsContentType = false;
340
- let needsToFormData = false;
341
- let needsPaginationTypes = false;
342
-
343
- for (const operationInfo of operations) {
344
- const operationTypes = getOperationTypes(openApi, operationInfo, options);
345
- if (!operationTypes.operationId || !operationTypes.methodName) continue;
346
-
347
- if (operationTypes.paramsTypeName)
348
- dataContractImports.push(operationTypes.paramsTypeName);
349
- if (
350
- operationTypes.bodyType &&
351
- !isPrimitiveType(operationTypes.bodyType)
352
- ) {
353
- dataContractImports.push(
354
- ...extractTopLevelTypeNames(operationTypes.bodyType),
355
- );
356
- }
357
- if (!isPrimitiveType(operationTypes.responseType)) {
358
- dataContractImports.push(
359
- ...extractTopLevelTypeNames(operationTypes.responseType),
360
- );
361
- }
362
-
363
- if (operationTypes.hasQueryParams && !operationTypes.hasPathParams) {
364
- const queryTypeName = `${pascalCase(operationTypes.methodName)}Query`;
365
- queryAliases.push(
366
- `export type ${queryTypeName} = NonNullable<${operationTypes.paramsTypeName}>;`,
367
- );
368
- }
369
-
370
- if (contentTypeEnum(operationTypes.contentType)) needsContentType = true;
371
- if (operationTypes.contentType === "multipart/form-data")
372
- needsToFormData = true;
373
- if (
374
- operationTypes.hasQueryParams &&
375
- !operationTypes.hasPathParams &&
376
- options.useFilterFormValues !== false &&
377
- isPaginated(operationTypes)
378
- ) {
379
- needsPaginationTypes = true;
380
- }
381
-
382
- methods.push(
383
- createMethod(
384
- operationInfo,
385
- operationTypes,
386
- options.defaultHandlers,
387
- options,
388
- ),
389
- );
390
- }
391
-
392
- if (needsToFormData) helperValueImports.push("toFormData");
393
- if (needsPaginationTypes) {
394
- helperValueImports.push("buildQuery");
395
- helperTypeImports.push(
396
- "ExtractDataIfPaginated",
397
- "FilterFormValues",
398
- "SortableKeys",
399
- "SortDirection",
400
- "UnwrapArray",
401
- );
402
- }
403
-
404
- const defaultHandlers = options.defaultHandlers;
405
- const useTypeOnlyImports = options.useTypeOnlyImports !== false;
406
- const imports = [
407
- generatedFileHeader(),
408
- createImport(
409
- ["request", ...(needsContentType ? ["ContentType"] : [])],
410
- "../generated/http-client",
411
- ),
412
- createImport(
413
- ["RequestParams"],
414
- "../generated/http-client",
415
- useTypeOnlyImports,
416
- ),
417
- createImport(
418
- dataContractImports,
419
- "../generated/data-contracts",
420
- useTypeOnlyImports,
421
- ),
422
- createImport(
423
- helperValueImports,
424
- options.runtimePackageName ?? "typedapi-client-helpers",
425
- ),
426
- createImport(
427
- helperTypeImports,
428
- options.runtimePackageName ?? "typedapi-client-helpers",
429
- useTypeOnlyImports,
430
- ),
431
- defaultHandlers
432
- ? `import { ${defaultHandlers.success} as typedApiDefaultSuccessHandler, ${defaultHandlers.error} as typedApiDefaultErrorHandler } from ${JSON.stringify(defaultHandlers.path)};`
433
- : "",
434
- ].filter(Boolean);
435
-
436
- const aliases = unique(queryAliases).join("\n");
437
- const aliasBlock = aliases ? `${aliases}\n\n` : "";
438
-
439
- return `${imports.join("\n")}\n\n${aliasBlock}${methods.join("\n\n")}\n`;
440
- }
1
+ import { getOperationTypes, generatedFileHeader } from "./schema.mjs";
2
+ import { pascalCase, typePropertyName } from "./names.mjs";
3
+
4
+ function unique(values) {
5
+ return [...new Set(values.filter(Boolean))];
6
+ }
7
+
8
+ function createImport(names, from, typeOnly = false) {
9
+ const cleanNames = unique(names).sort((a, b) => a.localeCompare(b));
10
+ if (cleanNames.length === 0) return "";
11
+ const keyword = typeOnly ? "import type" : "import";
12
+ if (cleanNames.length <= 3) return `${keyword} { ${cleanNames.join(", ")} } from ${JSON.stringify(from)};`;
13
+ return `${keyword} {\n${cleanNames.map((name) => ` ${name},`).join("\n")}\n} from ${JSON.stringify(from)};`;
14
+ }
15
+
16
+ function isPrimitiveType(type) {
17
+ return /^(void|any|unknown|string|number|boolean|Blob|File|Record<)/.test(type);
18
+ }
19
+
20
+ function extractTopLevelTypeNames(type) {
21
+ const withoutLiterals = String(type ?? "").replace(/"(?:\\.|[^"])*"|'(?:\\.|[^'])*'/g, "");
22
+ const matches = withoutLiterals.match(/\b[A-Z][A-Za-z0-9_]*\b/g) ?? [];
23
+ return matches.filter(
24
+ (name) => ![
25
+ "Array", "Record", "Promise", "HttpResponse", "Blob", "File", "ApiResult",
26
+ "RequestParams", "FormData", "ApiClientError", "ApiHttpError",
27
+ ].includes(name),
28
+ );
29
+ }
30
+
31
+ function localPropertyAccess(baseExpression, wireName) {
32
+ return `${baseExpression}[${JSON.stringify(typePropertyName(wireName))}]`;
33
+ }
34
+
35
+ function parameterValueExpression(sourceExpression, parameter) {
36
+ return localPropertyAccess(sourceExpression, parameter.name);
37
+ }
38
+
39
+ function parameterObjectExpression(sourceExpression, parameters) {
40
+ const properties = parameters.map(
41
+ (parameter) => `${JSON.stringify(parameter.name)}: ${parameterValueExpression(
42
+ sourceExpression,
43
+ parameter,
44
+ )}`,
45
+ );
46
+ return properties.length <= 2
47
+ ? `{ ${properties.join(", ")} }`
48
+ : `{
49
+ ${properties.map((property) => ` ${property},`).join("\n")}
50
+ }`;
51
+ }
52
+
53
+ function pathExpression(routePath, pathParams, inputName = "input") {
54
+ const parts = [];
55
+ let cursor = 0;
56
+ const regex = /\{([^}]+)\}/g;
57
+ let match;
58
+
59
+ while ((match = regex.exec(routePath))) {
60
+ const [placeholder, paramName] = match;
61
+ const before = routePath.slice(cursor, match.index);
62
+ if (before) parts.push(JSON.stringify(before));
63
+ const parameter = pathParams.find((item) => item.name === paramName);
64
+ const valueExpression = parameter
65
+ ? parameterValueExpression(inputName, parameter)
66
+ : localPropertyAccess(inputName, paramName);
67
+ parts.push(`encodeURIComponent(String(${valueExpression}))`);
68
+ cursor = match.index + placeholder.length;
69
+ }
70
+
71
+ const after = routePath.slice(cursor);
72
+ if (after) parts.push(JSON.stringify(after));
73
+ if (parts.length === 0) return JSON.stringify(routePath);
74
+
75
+ return parts
76
+ .map((part) => /^"/.test(part) ? part.slice(1, -1).replace(/`/g, "\\`") : `\${${part}}`)
77
+ .join("")
78
+ .replace(/^/, "`")
79
+ .replace(/$/, "`");
80
+ }
81
+
82
+ function requestFormat(responseContentType, responseType) {
83
+ if (responseType === "void" || !responseContentType) return undefined;
84
+ if (responseContentType.includes("json")) return "json";
85
+ if (responseContentType.startsWith("text/")) return "text";
86
+ if (responseContentType === "application/octet-stream" || responseType === "Blob") return "blob";
87
+ return undefined;
88
+ }
89
+
90
+ function contentTypeEnum(contentType) {
91
+ switch (contentType) {
92
+ case "application/json": return "ContentType.Json";
93
+ case "application/vnd.api+json": return "ContentType.JsonApi";
94
+ case "multipart/form-data": return "ContentType.FormData";
95
+ case "application/x-www-form-urlencoded": return "ContentType.UrlEncoded";
96
+ case "text/plain": return "ContentType.Text";
97
+ default: return contentType?.includes("json") ? "ContentType.Json" : undefined;
98
+ }
99
+ }
100
+
101
+ function toWireExpression(valueExpression, schemaKey) {
102
+ if (!schemaKey) return valueExpression;
103
+ return `toWireValue(${valueExpression}, typedApiWireSchemas[${JSON.stringify(schemaKey)}], typedApiWireSchemas)`;
104
+ }
105
+
106
+ function requestItems(operationInfo, operationTypes, options = {}) {
107
+ const inputName = options.inputName ?? "input";
108
+ const items = [
109
+ "...params",
110
+ `path: ${pathExpression(
111
+ operationInfo.routePath,
112
+ operationTypes.parametersByLocation.path,
113
+ inputName,
114
+ )}`,
115
+ `method: ${JSON.stringify(operationInfo.method)}`,
116
+ ];
117
+
118
+ if (options.queryValueName) {
119
+ items.push(`query: ${options.queryValueName}`);
120
+ } else if (operationTypes.hasQueryParams) {
121
+ items.push(
122
+ `query: ${parameterObjectExpression(
123
+ inputName,
124
+ operationTypes.parametersByLocation.query,
125
+ )}`,
126
+ );
127
+ }
128
+
129
+ if (operationTypes.bodyType) {
130
+ const bodyExpression = options.bodyValueName ?? "data";
131
+ items.push(`body: ${toWireExpression(bodyExpression, operationTypes.bodyWireSchemaKey)}`);
132
+ }
133
+
134
+ if (operationTypes.hasHeaderParams || operationTypes.hasCookieParams) {
135
+ const headerSources = ["params.headers"];
136
+ if (operationTypes.hasHeaderParams) {
137
+ const headersExpression = parameterObjectExpression(
138
+ inputName,
139
+ operationTypes.parametersByLocation.header,
140
+ );
141
+ headerSources.push(`toRequestHeaders(${headersExpression})`);
142
+ }
143
+ if (operationTypes.hasCookieParams) {
144
+ const cookiesExpression = parameterObjectExpression(
145
+ inputName,
146
+ operationTypes.parametersByLocation.cookie,
147
+ );
148
+ const cookieCondition = operationTypes.parametersByLocation.cookie
149
+ .map((parameter) => `${parameterValueExpression(inputName, parameter)} !== undefined`)
150
+ .join(" || ") || "false";
151
+ headerSources.push(
152
+ `${cookieCondition} ? { Cookie: toCookieHeader(${cookiesExpression}) } : undefined`,
153
+ );
154
+ }
155
+ items.push(`headers: mergeHeaders(${headerSources.join(", ")})`);
156
+ }
157
+
158
+ const type = contentTypeEnum(operationTypes.contentType);
159
+ if (type) items.push(`type: ${type}`);
160
+ const format = requestFormat(operationTypes.responseContentType, operationTypes.responseType);
161
+ if (format) items.push(`format: ${JSON.stringify(format)}`);
162
+ return items;
163
+ }
164
+
165
+ function callbackOptions(operationTypes, defaultHandlers) {
166
+ const items = [];
167
+ if (defaultHandlers) {
168
+ items.push("onSuccess: onSuccess ?? typedApiDefaultSuccessHandler");
169
+ items.push("onError: onError ?? typedApiDefaultErrorHandler");
170
+ } else {
171
+ items.push("onSuccess");
172
+ items.push("onError");
173
+ }
174
+
175
+ if (operationTypes.responseWireSchemaKey) {
176
+ items.push(
177
+ `transformResponse: (value) => fromWireValue(value, typedApiWireSchemas[${JSON.stringify(
178
+ operationTypes.responseWireSchemaKey,
179
+ )}], typedApiWireSchemas) as ${operationTypes.responseType}`,
180
+ );
181
+ }
182
+ if (operationTypes.errorWireSchemaKey) {
183
+ items.push(
184
+ `transformError: (value) => fromWireValue(value, typedApiWireSchemas[${JSON.stringify(
185
+ operationTypes.errorWireSchemaKey,
186
+ )}], typedApiWireSchemas) as ${operationTypes.errorType}`,
187
+ );
188
+ } else if (operationTypes.usesHttpErrorFallback) {
189
+ items.push(
190
+ "transformError: (value, response) => createApiHttpError(response.status, value)",
191
+ );
192
+ }
193
+
194
+ return items.length <= 2
195
+ ? `{ ${items.join(", ")} }`
196
+ : `{\n ${items.join(",\n ")},\n }`;
197
+ }
198
+
199
+ function optionsType(operationTypes) {
200
+ return `ApiMethodOptions<${operationTypes.responseType}, ${operationTypes.errorType}, RequestParams>`;
201
+ }
202
+
203
+ function promiseType(operationTypes) {
204
+ return `Promise<ApiResult<${operationTypes.responseType}, ${operationTypes.errorType}>>`;
205
+ }
206
+
207
+ function isPaginated(operationTypes) {
208
+ return Boolean(operationTypes.paginationMetadata) ||
209
+ /(?:Api)?Pagination(?:Sort)?Response\b|Paged(?:Response|Result)?\b|Paginated(?:Response|Result)?\b/.test(
210
+ operationTypes.responseType,
211
+ );
212
+ }
213
+
214
+ function methodDescription(operationInfo, operationTypes) {
215
+ const description = operationInfo.operation.description || operationInfo.operation.summary || "No description";
216
+ const tags = (operationInfo.operation.tags ?? []).join(", ");
217
+ return `/**\n * ${description.replace(/\n/g, "\n * ")}\n *\n * @tags ${tags}\n * @name ${operationTypes.methodNameSource}\n * @request ${operationInfo.method}:${operationInfo.routePath}\n */`;
218
+ }
219
+
220
+ function createRequestCall(operationInfo, operationTypes, options = {}) {
221
+ const items = requestItems(operationInfo, operationTypes, options)
222
+ .map((item) => ` ${item},`)
223
+ .join("\n");
224
+ return `request<${operationTypes.responseType}, ${operationTypes.errorType}>({\n${items}\n })`;
225
+ }
226
+
227
+ function createPaginatedMethod(operationInfo, operationTypes, defaultHandlers) {
228
+ const queryTypeName = operationTypes.parameterTypeNames.query;
229
+ const requestCall = createRequestCall(operationInfo, operationTypes, { queryValueName: "builtQuery" });
230
+ return `${methodDescription(operationInfo, operationTypes)}
231
+ export async function ${operationTypes.methodName}(
232
+ filters: FilterFormValues<${queryTypeName}>[] = [],
233
+ page = 1,
234
+ pageSize = 100,
235
+ sortBy: SortableKeys<${operationTypes.responseType}> | null = null,
236
+ sortDirection?: SortDirection,
237
+ options: ${optionsType(operationTypes)} = {},
238
+ ): ${promiseType(operationTypes)} {
239
+ const { onSuccess, onError, params = {} } = options;
240
+ const builtQuery = buildQuery<
241
+ ${queryTypeName},
242
+ UnwrapArray<ExtractDataIfPaginated<${operationTypes.responseType}>>
243
+ >(filters, page, pageSize, sortBy, sortDirection);
244
+
245
+ return handleApiResponse<${operationTypes.responseType}, ${operationTypes.errorType}>(
246
+ () => ${requestCall},
247
+ ${callbackOptions(operationTypes, defaultHandlers)},
248
+ );
249
+ }`;
250
+ }
251
+
252
+ function regularParameterArgumentName(operationTypes) {
253
+ if (operationTypes.hasPathParams) return "pathParams";
254
+ if (
255
+ operationTypes.hasQueryParams &&
256
+ !operationTypes.hasHeaderParams &&
257
+ !operationTypes.hasCookieParams
258
+ ) return "query";
259
+ return "requestParams";
260
+ }
261
+
262
+ function createRegularMethod(operationInfo, operationTypes, defaultHandlers) {
263
+ const args = [];
264
+ const parameterArgumentName = operationTypes.paramsTypeName
265
+ ? regularParameterArgumentName(operationTypes)
266
+ : undefined;
267
+
268
+ if (operationTypes.paramsTypeName) {
269
+ const canOmitParams = operationTypes.allParametersOptional && !operationTypes.bodyType;
270
+ args.push(
271
+ `${parameterArgumentName}: ${operationTypes.paramsTypeName}${canOmitParams ? " = {}" : ""}`,
272
+ );
273
+ }
274
+
275
+ if (operationTypes.bodyType) {
276
+ args.push(`data: ${operationTypes.bodyType}`);
277
+ }
278
+
279
+ args.push(`options: ${optionsType(operationTypes)} = {}`);
280
+ const requestCall = createRequestCall(operationInfo, operationTypes, {
281
+ inputName: parameterArgumentName ?? "undefined",
282
+ bodyValueName: operationTypes.bodyType ? "data" : undefined,
283
+ });
284
+
285
+ return `${methodDescription(operationInfo, operationTypes)}
286
+ export async function ${operationTypes.methodName}(
287
+ ${args.join(",\n ")}
288
+ ): ${promiseType(operationTypes)} {
289
+ const { onSuccess, onError, params = {} } = options;
290
+
291
+ return handleApiResponse<${operationTypes.responseType}, ${operationTypes.errorType}>(
292
+ () => ${requestCall},
293
+ ${callbackOptions(operationTypes, defaultHandlers)},
294
+ );
295
+ }`;
296
+ }
297
+
298
+ function createMethod(operationInfo, operationTypes, defaultHandlers, options) {
299
+ if (
300
+ operationTypes.hasQueryParams &&
301
+ !operationTypes.hasPathParams &&
302
+ !operationTypes.bodyType &&
303
+ !operationTypes.hasHeaderParams &&
304
+ !operationTypes.hasCookieParams &&
305
+ options.useFilterFormValues !== false &&
306
+ isPaginated(operationTypes)
307
+ ) {
308
+ return createPaginatedMethod(operationInfo, operationTypes, defaultHandlers);
309
+ }
310
+ return createRegularMethod(operationInfo, operationTypes, defaultHandlers);
311
+ }
312
+
313
+ export function methodFileNameForController(controllerName) {
314
+ return `${controllerName}.api.ts`;
315
+ }
316
+
317
+ export function controllerNameForOperation(operationInfo, options = {}) {
318
+ if (options.moduleNameFirstTag !== false) {
319
+ const [tag] = operationInfo.operation.tags ?? [];
320
+ if (tag) return pascalCase(tag, "Default");
321
+ }
322
+ const parts = operationInfo.routePath.split("/").filter(Boolean);
323
+ return pascalCase(parts[1] ?? parts[0] ?? "Default", "Default");
324
+ }
325
+
326
+ export function generateMethodFile(openApi, controllerName, operations, options = {}) {
327
+ const dataContractImports = [];
328
+ const dataContractValueImports = [];
329
+ const helperTypeImports = ["ApiResult", "ApiMethodOptions"];
330
+ const helperValueImports = ["handleApiResponse"];
331
+ const methods = [];
332
+ let needsContentType = false;
333
+ let needsPaginationTypes = false;
334
+ let needsHeaderHelpers = false;
335
+ let needsWireMappings = false;
336
+
337
+ for (const operationInfo of operations) {
338
+ const operationTypes = getOperationTypes(openApi, operationInfo, options);
339
+
340
+ for (const typeName of Object.values(operationTypes.parameterTypeNames)) dataContractImports.push(typeName);
341
+ if (operationTypes.paramsTypeName) dataContractImports.push(operationTypes.paramsTypeName);
342
+ if (operationTypes.bodyType && !isPrimitiveType(operationTypes.bodyType)) {
343
+ dataContractImports.push(...extractTopLevelTypeNames(operationTypes.bodyType));
344
+ }
345
+ if (!isPrimitiveType(operationTypes.responseType)) {
346
+ dataContractImports.push(...extractTopLevelTypeNames(operationTypes.responseType));
347
+ }
348
+ if (!isPrimitiveType(operationTypes.errorType)) {
349
+ dataContractImports.push(...extractTopLevelTypeNames(operationTypes.errorType));
350
+ }
351
+ if (operationTypes.usesHttpErrorFallback) {
352
+ helperTypeImports.push("ApiHttpError");
353
+ helperValueImports.push("createApiHttpError");
354
+ }
355
+
356
+ if (contentTypeEnum(operationTypes.contentType)) needsContentType = true;
357
+ if (operationTypes.hasHeaderParams || operationTypes.hasCookieParams) needsHeaderHelpers = true;
358
+ if (
359
+ operationTypes.bodyWireSchemaKey ||
360
+ operationTypes.responseWireSchemaKey ||
361
+ operationTypes.errorWireSchemaKey
362
+ ) {
363
+ needsWireMappings = true;
364
+ }
365
+ if (
366
+ operationTypes.hasQueryParams &&
367
+ !operationTypes.hasPathParams && !operationTypes.bodyType &&
368
+ !operationTypes.hasHeaderParams && !operationTypes.hasCookieParams &&
369
+ options.useFilterFormValues !== false && isPaginated(operationTypes)
370
+ ) {
371
+ needsPaginationTypes = true;
372
+ }
373
+
374
+ methods.push(createMethod(operationInfo, operationTypes, options.defaultHandlers, options));
375
+ }
376
+
377
+ if (needsHeaderHelpers) helperValueImports.push("mergeHeaders", "toCookieHeader", "toRequestHeaders");
378
+ if (needsWireMappings) {
379
+ dataContractValueImports.push("typedApiWireSchemas");
380
+ helperValueImports.push("fromWireValue", "toWireValue");
381
+ }
382
+ if (needsPaginationTypes) {
383
+ helperValueImports.push("buildQuery");
384
+ helperTypeImports.push(
385
+ "ExtractDataIfPaginated", "FilterFormValues", "SortableKeys", "SortDirection", "UnwrapArray",
386
+ );
387
+ }
388
+
389
+ const useTypeOnlyImports = options.useTypeOnlyImports !== false;
390
+ const defaultHandlers = options.defaultHandlers;
391
+ const imports = [
392
+ generatedFileHeader(),
393
+ createImport(["request", ...(needsContentType ? ["ContentType"] : [])], "../generated/http-client"),
394
+ createImport(["RequestParams"], "../generated/http-client", useTypeOnlyImports),
395
+ createImport(dataContractValueImports, "../generated/data-contracts"),
396
+ createImport(dataContractImports, "../generated/data-contracts", useTypeOnlyImports),
397
+ createImport(helperValueImports, options.runtimePackageName ?? "typedapi-client-helpers"),
398
+ createImport(helperTypeImports, options.runtimePackageName ?? "typedapi-client-helpers", useTypeOnlyImports),
399
+ defaultHandlers
400
+ ? `import { ${defaultHandlers.success} as typedApiDefaultSuccessHandler, ${defaultHandlers.error} as typedApiDefaultErrorHandler } from ${JSON.stringify(defaultHandlers.path)};`
401
+ : "",
402
+ ].filter(Boolean);
403
+
404
+ return `${imports.join("\n")}\n\n${methods.join("\n\n")}\n`;
405
+ }