typedapi-client-helpers 0.1.16 → 0.2.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 (137) hide show
  1. package/README.md +220 -490
  2. package/dist/index.cjs +274 -47
  3. package/dist/index.d.cts +79 -95
  4. package/dist/index.d.ts +79 -95
  5. package/dist/index.js +267 -47
  6. package/package.json +30 -13
  7. package/scripts/generate-api-minimal.mjs +158 -785
  8. package/scripts/openapi-generator/controller-template.mjs +246 -0
  9. package/scripts/openapi-generator/http-client-template.mjs +34 -0
  10. package/scripts/openapi-generator/index.mjs +113 -0
  11. package/scripts/openapi-generator/method-template.mjs +440 -0
  12. package/scripts/openapi-generator/names.mjs +145 -0
  13. package/scripts/openapi-generator/openapi-utils.mjs +147 -0
  14. package/scripts/openapi-generator/schema.mjs +455 -0
  15. package/bin/typedapi-generate.js +0 -3
  16. package/dist/api/generated/Import.d.ts +0 -22
  17. package/dist/api/generated/Import.d.ts.map +0 -1
  18. package/dist/api/generated/Import.js +0 -48
  19. package/dist/api/generated/Import.js.map +0 -1
  20. package/dist/api/generated/ImportRoute.d.ts +0 -30
  21. package/dist/api/generated/ImportRoute.d.ts.map +0 -1
  22. package/dist/api/generated/ImportRoute.js +0 -13
  23. package/dist/api/generated/ImportRoute.js.map +0 -1
  24. package/dist/api/generated/Order.d.ts +0 -38
  25. package/dist/api/generated/Order.d.ts.map +0 -1
  26. package/dist/api/generated/Order.js +0 -75
  27. package/dist/api/generated/Order.js.map +0 -1
  28. package/dist/api/generated/OrderRoute.d.ts +0 -83
  29. package/dist/api/generated/OrderRoute.d.ts.map +0 -1
  30. package/dist/api/generated/OrderRoute.js +0 -13
  31. package/dist/api/generated/OrderRoute.js.map +0 -1
  32. package/dist/api/generated/Product.d.ts +0 -62
  33. package/dist/api/generated/Product.d.ts.map +0 -1
  34. package/dist/api/generated/Product.js +0 -114
  35. package/dist/api/generated/Product.js.map +0 -1
  36. package/dist/api/generated/ProductRoute.d.ts +0 -140
  37. package/dist/api/generated/ProductRoute.d.ts.map +0 -1
  38. package/dist/api/generated/ProductRoute.js +0 -13
  39. package/dist/api/generated/ProductRoute.js.map +0 -1
  40. package/dist/api/generated/Report.d.ts +0 -14
  41. package/dist/api/generated/Report.d.ts.map +0 -1
  42. package/dist/api/generated/Report.js +0 -32
  43. package/dist/api/generated/Report.js.map +0 -1
  44. package/dist/api/generated/ReportRoute.d.ts +0 -23
  45. package/dist/api/generated/ReportRoute.d.ts.map +0 -1
  46. package/dist/api/generated/ReportRoute.js +0 -13
  47. package/dist/api/generated/ReportRoute.js.map +0 -1
  48. package/dist/api/generated/Supplier.d.ts +0 -46
  49. package/dist/api/generated/Supplier.d.ts.map +0 -1
  50. package/dist/api/generated/Supplier.js +0 -87
  51. package/dist/api/generated/Supplier.js.map +0 -1
  52. package/dist/api/generated/SupplierRoute.d.ts +0 -101
  53. package/dist/api/generated/SupplierRoute.d.ts.map +0 -1
  54. package/dist/api/generated/SupplierRoute.js +0 -13
  55. package/dist/api/generated/SupplierRoute.js.map +0 -1
  56. package/dist/api/generated/data-contracts.d.ts +0 -277
  57. package/dist/api/generated/data-contracts.d.ts.map +0 -1
  58. package/dist/api/generated/data-contracts.js +0 -26
  59. package/dist/api/generated/data-contracts.js.map +0 -1
  60. package/dist/api/generated/http-client.d.ts +0 -61
  61. package/dist/api/generated/http-client.d.ts.map +0 -1
  62. package/dist/api/generated/http-client.js +0 -171
  63. package/dist/api/generated/http-client.js.map +0 -1
  64. package/dist/api/index.d.ts +0 -8
  65. package/dist/api/index.d.ts.map +0 -1
  66. package/dist/api/index.js +0 -8
  67. package/dist/api/index.js.map +0 -1
  68. package/dist/api/methods/Import.api.d.ts +0 -6
  69. package/dist/api/methods/Import.api.d.ts.map +0 -1
  70. package/dist/api/methods/Import.api.js +0 -27
  71. package/dist/api/methods/Import.api.js.map +0 -1
  72. package/dist/api/methods/Order.api.d.ts +0 -29
  73. package/dist/api/methods/Order.api.d.ts.map +0 -1
  74. package/dist/api/methods/Order.api.js +0 -52
  75. package/dist/api/methods/Order.api.js.map +0 -1
  76. package/dist/api/methods/Product.api.d.ts +0 -39
  77. package/dist/api/methods/Product.api.d.ts.map +0 -1
  78. package/dist/api/methods/Product.api.js +0 -69
  79. package/dist/api/methods/Product.api.js.map +0 -1
  80. package/dist/api/methods/Report.api.d.ts +0 -6
  81. package/dist/api/methods/Report.api.d.ts.map +0 -1
  82. package/dist/api/methods/Report.api.js +0 -19
  83. package/dist/api/methods/Report.api.js.map +0 -1
  84. package/dist/api/methods/Supplier.api.d.ts +0 -35
  85. package/dist/api/methods/Supplier.api.d.ts.map +0 -1
  86. package/dist/api/methods/Supplier.api.js +0 -63
  87. package/dist/api/methods/Supplier.api.js.map +0 -1
  88. package/dist/index.d.ts.map +0 -1
  89. package/dist/index.js.map +0 -1
  90. package/dist/interfaces/ApiResult.d.ts +0 -17
  91. package/dist/interfaces/ApiResult.d.ts.map +0 -1
  92. package/dist/interfaces/ApiResult.js +0 -2
  93. package/dist/interfaces/ApiResult.js.map +0 -1
  94. package/dist/interfaces/Filter.d.ts +0 -31
  95. package/dist/interfaces/Filter.d.ts.map +0 -1
  96. package/dist/interfaces/Filter.js +0 -2
  97. package/dist/interfaces/Filter.js.map +0 -1
  98. package/dist/interfaces/SortType.d.ts +0 -7
  99. package/dist/interfaces/SortType.d.ts.map +0 -1
  100. package/dist/interfaces/SortType.js +0 -2
  101. package/dist/interfaces/SortType.js.map +0 -1
  102. package/dist/types/ApiCallOptions.d.ts +0 -9
  103. package/dist/types/ApiCallOptions.d.ts.map +0 -1
  104. package/dist/types/ApiCallOptions.js +0 -2
  105. package/dist/types/ApiCallOptions.js.map +0 -1
  106. package/dist/types/ApiMethodTypes.d.ts +0 -24
  107. package/dist/types/ApiMethodTypes.d.ts.map +0 -1
  108. package/dist/types/ApiMethodTypes.js +0 -2
  109. package/dist/types/ApiMethodTypes.js.map +0 -1
  110. package/dist/types/HttpResponse.d.ts +0 -16
  111. package/dist/types/HttpResponse.d.ts.map +0 -1
  112. package/dist/types/HttpResponse.js +0 -2
  113. package/dist/types/HttpResponse.js.map +0 -1
  114. package/dist/utils/SortTypeFromSortDirection.d.ts +0 -22
  115. package/dist/utils/SortTypeFromSortDirection.d.ts.map +0 -1
  116. package/dist/utils/SortTypeFromSortDirection.js +0 -45
  117. package/dist/utils/SortTypeFromSortDirection.js.map +0 -1
  118. package/dist/utils/buildQuery.d.ts +0 -11
  119. package/dist/utils/buildQuery.d.ts.map +0 -1
  120. package/dist/utils/buildQuery.js +0 -94
  121. package/dist/utils/buildQuery.js.map +0 -1
  122. package/dist/utils/extractArgsCallbacksAndParams.d.ts +0 -22
  123. package/dist/utils/extractArgsCallbacksAndParams.d.ts.map +0 -1
  124. package/dist/utils/extractArgsCallbacksAndParams.js +0 -44
  125. package/dist/utils/extractArgsCallbacksAndParams.js.map +0 -1
  126. package/dist/utils/extractArgsToastsAndParams.d.ts +0 -18
  127. package/dist/utils/extractArgsToastsAndParams.d.ts.map +0 -1
  128. package/dist/utils/extractArgsToastsAndParams.js +0 -40
  129. package/dist/utils/extractArgsToastsAndParams.js.map +0 -1
  130. package/dist/utils/handleApiResponse.d.ts +0 -9
  131. package/dist/utils/handleApiResponse.d.ts.map +0 -1
  132. package/dist/utils/handleApiResponse.js +0 -49
  133. package/dist/utils/handleApiResponse.js.map +0 -1
  134. package/dist/utils/toFormData.d.ts +0 -8
  135. package/dist/utils/toFormData.d.ts.map +0 -1
  136. package/dist/utils/toFormData.js +0 -68
  137. package/dist/utils/toFormData.js.map +0 -1
@@ -1,7 +1,8 @@
1
+ #!/usr/bin/env node
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
- import { generateApi } from "swagger-typescript-api";
5
+ import { generateApi } from "./openapi-generator/index.mjs";
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
@@ -17,79 +18,62 @@ const consumerPackageJson = fs.existsSync(consumerPackageJsonPath)
17
18
  ? JSON.parse(fs.readFileSync(consumerPackageJsonPath, "utf8"))
18
19
  : {};
19
20
 
20
- const isRunningInsidePackage =
21
- path.resolve(process.cwd()) === path.resolve(__dirname, "..");
22
-
23
- const runtimePackageName = isRunningInsidePackage
24
- ? "../.."
25
- : runtimePackageJson.name;
26
-
27
21
  const cwd = process.cwd();
28
-
29
22
  const config = {
30
23
  ...(runtimePackageJson.config ?? {}),
31
24
  ...(consumerPackageJson.config ?? {}),
32
25
  };
33
26
 
34
- const swaggerFile =
35
- process.env.SWAGGER_FILE ??
36
- process.env.npm_config_swagger_file ??
37
- config.swaggerFile;
27
+ const runtimePackageName =
28
+ path.resolve(cwd) === packageRoot ? "../.." : runtimePackageJson.name;
38
29
 
39
- const swaggerUrl =
40
- process.env.SWAGGER_URL ??
41
- process.env.npm_config_swagger_url ??
42
- config.swaggerUrl;
30
+ const swaggerFile = getStringSetting(
31
+ process.env.SWAGGER_FILE,
32
+ process.env.npm_config_swagger_file,
33
+ config.swaggerFile,
34
+ config.input,
35
+ );
43
36
 
44
- const defaultSwaggerUrl = "https://localhost:7000/swagger/v1/swagger.json";
37
+ const swaggerUrl = getStringSetting(
38
+ process.env.SWAGGER_URL,
39
+ process.env.npm_config_swagger_url,
40
+ config.swaggerUrl,
41
+ config.url,
42
+ );
45
43
 
46
- const swaggerBackupFileSetting = getStringSetting(
47
- process.env.TYPED_API_SWAGGER_BACKUP_FILE,
48
- process.env.npm_config_typed_api_swagger_backup_file,
49
- config.typedApiSwaggerBackupFile,
50
- "swagger/swagger.backup.json",
44
+ const defaultSwaggerUrl = getStringSetting(
45
+ config.defaultSwaggerUrl,
46
+ "https://localhost:7000/swagger/v1/swagger.json",
51
47
  );
52
48
 
53
- const swaggerBackupPath = path.resolve(packageRoot, swaggerBackupFileSetting);
49
+ const swaggerBackupPath = path.resolve(
50
+ cwd,
51
+ getStringSetting(
52
+ process.env.TYPED_API_SWAGGER_BACKUP_FILE,
53
+ process.env.npm_config_typed_api_swagger_backup_file,
54
+ config.typedApiSwaggerBackupFile,
55
+ "swagger/swagger.backup.json",
56
+ ),
57
+ );
54
58
 
55
59
  const apiRoot = path.resolve(
56
60
  cwd,
57
- process.env.API_OUTPUT ??
58
- process.env.npm_config_api_output ??
59
- config.apiOutput ??
61
+ getStringSetting(
62
+ process.env.API_OUTPUT,
63
+ process.env.npm_config_api_output,
64
+ config.apiOutput,
65
+ config.output,
60
66
  "src/api",
67
+ ),
61
68
  );
62
69
 
63
- const generatedDir = path.join(apiRoot, "generated");
64
70
  const methodsDir = path.join(apiRoot, "methods");
65
71
 
66
- const useTypeOnlyImports =
67
- process.env.TYPED_API_USE_TYPE_ONLY_IMPORTS === "true" ||
68
- process.env.npm_config_typed_api_use_type_only_imports === "true" ||
69
- config.typedApiUseTypeOnlyImports === true;
70
-
71
- const useFilterFormValues =
72
- process.env.TYPED_API_USE_FILTER_FORM_VALUES === "true" ||
73
- process.env.npm_config_typed_api_use_filter_form_values === "true" ||
74
- config.typedApiUseFilterFormValues === true;
75
-
76
- function getStringSetting(...values) {
77
- for (const value of values) {
78
- if (typeof value === "string" && value.trim() !== "") {
79
- return value.trim();
80
- }
81
- }
82
-
83
- return undefined;
84
- }
85
-
86
- const fallbackDefaultFunctionsPath = "defaultApiFunctions";
87
-
88
72
  const defaultFunctionsPathSetting = getStringSetting(
89
73
  process.env.TYPED_API_DEFAULT_FUNCTIONS_PATH,
90
74
  process.env.npm_config_typed_api_default_functions_path,
91
75
  config.typedApiDefaultFunctionsPath,
92
- fallbackDefaultFunctionsPath,
76
+ "src/defaultApiFunctions",
93
77
  );
94
78
 
95
79
  const defaultFunctionsFilePath = path.resolve(cwd, defaultFunctionsPathSetting);
@@ -108,27 +92,94 @@ const defaultErrorHandlerName = getStringSetting(
108
92
  "handleErrors",
109
93
  );
110
94
 
111
- function pascalCase(value) {
112
- return value
113
- .split(/[-_]/)
114
- .filter(Boolean)
115
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
116
- .join("");
117
- }
95
+ const generatorOptions = {
96
+ cleanOutput: getBooleanSetting(
97
+ process.env.TYPED_API_CLEAN_OUTPUT,
98
+ process.env.npm_config_typed_api_clean_output,
99
+ process.env.npm_config_clean_output,
100
+ config.typedApiCleanOutput,
101
+ config.cleanOutput,
102
+ true,
103
+ ),
104
+ moduleNameFirstTag: getBooleanSetting(
105
+ process.env.TYPED_API_MODULE_NAME_FIRST_TAG,
106
+ process.env.npm_config_typed_api_module_name_first_tag,
107
+ process.env.npm_config_module_name_first_tag,
108
+ config.typedApiModuleNameFirstTag,
109
+ config.moduleNameFirstTag,
110
+ true,
111
+ ),
112
+ defaultResponseAsSuccess: getBooleanSetting(
113
+ process.env.TYPED_API_DEFAULT_RESPONSE_AS_SUCCESS,
114
+ process.env.npm_config_typed_api_default_response_as_success,
115
+ process.env.npm_config_default_response_as_success,
116
+ config.typedApiDefaultResponseAsSuccess,
117
+ config.defaultResponseAsSuccess,
118
+ false,
119
+ ),
120
+ generateUnionEnums: getBooleanSetting(
121
+ process.env.TYPED_API_GENERATE_UNION_ENUMS,
122
+ process.env.npm_config_typed_api_generate_union_enums,
123
+ process.env.npm_config_generate_union_enums,
124
+ config.typedApiGenerateUnionEnums,
125
+ config.generateUnionEnums,
126
+ false,
127
+ ),
128
+ enumNamesAsValues: getBooleanSetting(
129
+ process.env.TYPED_API_ENUM_NAMES_AS_VALUES,
130
+ process.env.npm_config_typed_api_enum_names_as_values,
131
+ process.env.npm_config_enum_names_as_values,
132
+ config.typedApiEnumNamesAsValues,
133
+ config.enumNamesAsValues,
134
+ false,
135
+ ),
136
+ useTypeOnlyImports: getBooleanSetting(
137
+ process.env.TYPED_API_USE_TYPE_ONLY_IMPORTS,
138
+ process.env.npm_config_typed_api_use_type_only_imports,
139
+ process.env.npm_config_use_type_only_imports,
140
+ config.typedApiUseTypeOnlyImports,
141
+ config.useTypeOnlyImports,
142
+ true,
143
+ ),
144
+ useFilterFormValues: getBooleanSetting(
145
+ process.env.TYPED_API_USE_FILTER_FORM_VALUES,
146
+ process.env.npm_config_typed_api_use_filter_form_values,
147
+ config.typedApiUseFilterFormValues,
148
+ true,
149
+ ),
150
+ baseUrl: getStringSetting(
151
+ process.env.TYPED_API_BASE_URL,
152
+ process.env.npm_config_typed_api_base_url,
153
+ config.typedApiBaseUrl,
154
+ ),
155
+ runtimePackageName,
156
+ };
118
157
 
119
- function toQueryName(methodName) {
120
- return `${pascalCase(methodName)}Query`;
158
+ function getStringSetting(...values) {
159
+ for (const value of values) {
160
+ if (typeof value === "string" && value.trim() !== "") {
161
+ return value.trim();
162
+ }
163
+ }
164
+
165
+ return undefined;
121
166
  }
122
167
 
123
- function cleanDirectory(directory) {
124
- if (fs.existsSync(directory)) {
125
- fs.rmSync(directory, { recursive: true, force: true });
168
+ function getBooleanSetting(...values) {
169
+ const fallback = values[values.length - 1];
170
+
171
+ for (const value of values.slice(0, -1)) {
172
+ if (typeof value === "boolean") return value;
173
+ if (typeof value === "string" && value.trim() !== "") {
174
+ const normalized = value.trim().toLowerCase();
175
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
176
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
177
+ }
126
178
  }
127
179
 
128
- fs.mkdirSync(directory, { recursive: true });
180
+ return Boolean(fallback);
129
181
  }
130
182
 
131
-
132
183
  function fileExists(filePath) {
133
184
  try {
134
185
  return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
@@ -137,22 +188,22 @@ function fileExists(filePath) {
137
188
  }
138
189
  }
139
190
 
191
+ function displayPath(filePath) {
192
+ return path.relative(cwd, filePath) || ".";
193
+ }
194
+
140
195
  function writeSwaggerBackup(content, sourceLabel) {
141
196
  fs.mkdirSync(path.dirname(swaggerBackupPath), { recursive: true });
142
197
  fs.writeFileSync(swaggerBackupPath, content);
143
198
 
144
199
  console.log(
145
- `Updated Swagger backup from ${sourceLabel}: ${path.relative(packageRoot, swaggerBackupPath)}`,
200
+ `Updated Swagger backup from ${sourceLabel}: ${displayPath(swaggerBackupPath)}`,
146
201
  );
147
202
  }
148
203
 
149
204
  function copySwaggerBackupFromFile(filePath) {
150
205
  const content = fs.readFileSync(filePath, "utf8");
151
-
152
- // Validate that the backup is at least JSON. This prevents overwriting a good
153
- // backup with an invalid or empty file by accident.
154
206
  JSON.parse(content);
155
-
156
207
  writeSwaggerBackup(content, path.relative(cwd, filePath));
157
208
  }
158
209
 
@@ -166,10 +217,7 @@ async function downloadSwaggerBackupFromUrl(url) {
166
217
  }
167
218
 
168
219
  const content = await response.text();
169
-
170
- // Validate before saving, so a server error page is never stored as backup.
171
220
  JSON.parse(content);
172
-
173
221
  writeSwaggerBackup(content, url);
174
222
  }
175
223
 
@@ -179,7 +227,7 @@ function useBackupSwaggerInput(reason) {
179
227
  }
180
228
 
181
229
  console.warn(
182
- `${reason} Using Swagger backup: ${path.relative(packageRoot, swaggerBackupPath)}`,
230
+ `${reason} Using Swagger backup: ${displayPath(swaggerBackupPath)}`,
183
231
  );
184
232
 
185
233
  return {
@@ -253,103 +301,26 @@ async function resolveSwaggerInput() {
253
301
  input: swaggerBackupPath,
254
302
  label: defaultSwaggerUrl,
255
303
  };
256
- } catch (error) {
304
+ } catch {
257
305
  const backupInput = useBackupSwaggerInput(
258
- `Default Swagger file was not found and default Swagger URL is not available.`,
306
+ "Default Swagger file was not found and default Swagger URL is not available.",
259
307
  );
260
308
 
261
309
  if (backupInput) {
262
310
  return backupInput;
263
311
  }
264
312
 
265
- throw error;
266
- }
267
- }
268
-
269
- async function writeFormattedFile(filePath, content) {
270
- fs.writeFileSync(filePath, content);
271
- }
272
-
273
- function removeGeneratedRouteFiles() {
274
- const files = fs.readdirSync(generatedDir);
275
-
276
- for (const file of files) {
277
- if (/route\.ts$/i.test(file)) {
278
- fs.rmSync(path.join(generatedDir, file), { force: true });
279
- }
313
+ throw new Error(
314
+ `No Swagger input found. Add swagger/swagger.json, configure swaggerFile, or configure swaggerUrl.`,
315
+ );
280
316
  }
281
317
  }
282
318
 
283
- async function generateOpenApiClient(swaggerInput) {
284
- cleanDirectory(generatedDir);
285
- cleanDirectory(methodsDir);
286
- fs.mkdirSync(methodsDir, { recursive: true });
287
-
288
- const generateOptions = {
289
- name: "Api.ts",
290
- output: generatedDir,
291
- httpClientType: "fetch",
292
- modular: true,
293
- cleanOutput: true,
294
- generateClient: true,
295
- generateRouteTypes: true,
296
- extractRequestParams: true,
297
- extractRequestBody: true,
298
- moduleNameFirstTag: true,
299
- };
300
-
301
- generateOptions.input = swaggerInput;
302
-
303
- await generateApi(generateOptions);
304
-
305
- removeGeneratedRouteFiles();
306
- }
307
-
308
319
  function createImportStatement({ names, from, typeOnly = false }) {
309
- if (!names || names.length === 0) {
310
- return "";
311
- }
312
-
320
+ const cleanNames = [...new Set(names.filter(Boolean))];
321
+ if (cleanNames.length === 0) return "";
313
322
  const importKeyword = typeOnly ? "import type" : "import";
314
-
315
- return `${importKeyword} {
316
- ${names.map((name) => ` ${name},`).join("\n")}
317
- } from "${from}";`;
318
- }
319
-
320
- function createAliasedImportStatement({ imports, from }) {
321
- if (!imports || imports.length === 0) {
322
- return "";
323
- }
324
-
325
- return `import {
326
- ${imports
327
- .map(({ name, alias }) =>
328
- alias && alias !== name ? ` ${name} as ${alias},` : ` ${name},`,
329
- )
330
- .join("\n")}
331
- } from "${from}";`;
332
- }
333
-
334
- function resolveImportFilePath(fromDirectory, importPath) {
335
- if (!importPath.startsWith(".")) {
336
- return undefined;
337
- }
338
-
339
- const basePath = path.resolve(fromDirectory, importPath);
340
- const candidates = [
341
- basePath,
342
- `${basePath}.ts`,
343
- `${basePath}.tsx`,
344
- `${basePath}.js`,
345
- `${basePath}.jsx`,
346
- path.join(basePath, "index.ts"),
347
- path.join(basePath, "index.tsx"),
348
- path.join(basePath, "index.js"),
349
- path.join(basePath, "index.jsx"),
350
- ];
351
-
352
- return candidates.find((candidate) => fs.existsSync(candidate));
323
+ return `${importKeyword} { ${cleanNames.join(", ")} } from ${JSON.stringify(from)};`;
353
324
  }
354
325
 
355
326
  function createDefaultFunctionsFileIfMissing(filePath) {
@@ -364,12 +335,11 @@ function createDefaultFunctionsFileIfMissing(filePath) {
364
335
 
365
336
  fs.writeFileSync(
366
337
  resolvedFilePath,
367
- `
368
- ${createImportStatement({
369
- names: ["ApiErrorResult", "ApiSuccessResult"],
370
- from: "typedapi-client-helpers",
371
- typeOnly: true,
372
- })}
338
+ `${createImportStatement({
339
+ names: ["ApiErrorResult", "ApiSuccessResult"],
340
+ from: runtimePackageName,
341
+ typeOnly: generatorOptions.useTypeOnlyImports !== false,
342
+ })}
373
343
 
374
344
  export function ${defaultSuccessHandlerName}<T>(
375
345
  _response: ApiSuccessResult<T>,
@@ -399,13 +369,13 @@ function toRelativeImportPath(fromDirectory, filePath) {
399
369
  relativePath = `./${relativePath}`;
400
370
  }
401
371
 
402
- relativePath = relativePath.replaceAll(path.sep, "/");
403
-
404
- return relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
372
+ return relativePath
373
+ .replaceAll(path.sep, "/")
374
+ .replace(/\.(ts|tsx|js|jsx)$/, "");
405
375
  }
406
376
 
407
377
  function escapeRegExp(value) {
408
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
378
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
409
379
  }
410
380
 
411
381
  function fileExportsName(source, exportName) {
@@ -446,11 +416,10 @@ function fileExportsName(source, exportName) {
446
416
  return false;
447
417
  }
448
418
 
449
- function resolveDefaultHandlers(importFromDirectory) {
450
- const defaultFunctionsFile =
451
- resolveImportFilePath(importFromDirectory, defaultFunctionsFilePath) ??
452
- createDefaultFunctionsFileIfMissing(defaultFunctionsFilePath);
453
-
419
+ function resolveDefaultHandlers() {
420
+ const defaultFunctionsFile = createDefaultFunctionsFileIfMissing(
421
+ defaultFunctionsFilePath,
422
+ );
454
423
  const source = fs.readFileSync(defaultFunctionsFile, "utf8");
455
424
  const hasSuccessHandler = fileExportsName(source, defaultSuccessHandlerName);
456
425
  const hasErrorHandler = fileExportsName(source, defaultErrorHandlerName);
@@ -472,627 +441,31 @@ function resolveDefaultHandlers(importFromDirectory) {
472
441
  }
473
442
 
474
443
  return {
475
- defaultFunctionsPath: toRelativeImportPath(
476
- importFromDirectory,
477
- defaultFunctionsFile,
478
- ),
479
- defaultSuccessHandlerName,
480
- defaultErrorHandlerName,
444
+ path: toRelativeImportPath(methodsDir, defaultFunctionsFile),
445
+ success: defaultSuccessHandlerName,
446
+ error: defaultErrorHandlerName,
481
447
  };
482
448
  }
483
449
 
484
- function callbackOptionsExpression(defaultHandlers) {
485
- if (defaultHandlers) {
486
- return `{
487
- onSuccess: onSuccess ?? typedApiDefaultSuccessHandler,
488
- onError: onError ?? typedApiDefaultErrorHandler
489
- }`;
490
- }
491
-
492
- return `{
493
- onSuccess,
494
- onError
495
- }`;
496
- }
497
-
498
- function getAllTsFiles(dir) {
499
- let results = [];
500
-
501
- if (!fs.existsSync(dir)) {
502
- return results;
503
- }
504
-
505
- const list = fs.readdirSync(dir);
506
-
507
- for (const file of list) {
508
- const filePath = path.join(dir, file);
509
- const stat = fs.statSync(filePath);
510
-
511
- if (stat.isDirectory()) {
512
- results = results.concat(getAllTsFiles(filePath));
513
- } else if (filePath.endsWith(".ts")) {
514
- results.push(filePath);
515
- }
516
- }
517
-
518
- return results;
519
- }
520
-
521
- function getGeneratedMethods(source) {
522
- const methodRegex = /^\s*(\w+)\s*=\s*\(/gm;
523
- const matches = [...source.matchAll(methodRegex)];
524
-
525
- return matches.map((match, index) => {
526
- const methodName = match[1];
527
- const startIndex = match.index ?? 0;
528
- const endIndex =
529
- index + 1 < matches.length ? matches[index + 1].index : source.length;
530
-
531
- const methodSource = source.slice(startIndex, endIndex);
532
-
533
- return {
534
- methodName,
535
- methodSource,
536
- isFormData: methodSource.includes("type: ContentType.FormData"),
537
- };
538
- });
539
- }
540
-
541
- function splitTopLevelArguments(value) {
542
- const args = [];
543
- let current = "";
544
- let depth = 0;
545
- let quote = "";
546
-
547
- for (let index = 0; index < value.length; index++) {
548
- const char = value[index];
549
- const previousChar = value[index - 1];
550
-
551
- if (quote) {
552
- current += char;
553
-
554
- if (char === quote && previousChar !== "\\") {
555
- quote = "";
556
- }
557
-
558
- continue;
559
- }
560
-
561
- if (char === '"' || char === "'" || char === "`") {
562
- quote = char;
563
- current += char;
564
- continue;
565
- }
566
-
567
- if (char === "(" || char === "{" || char === "[" || char === "<") {
568
- depth++;
569
- current += char;
570
- continue;
571
- }
572
-
573
- if (char === ")" || char === "}" || char === "]" || char === ">") {
574
- depth--;
575
- current += char;
576
- continue;
577
- }
578
-
579
- if (char === "," && depth === 0) {
580
- if (current.trim()) {
581
- args.push(current.trim());
582
- }
583
-
584
- current = "";
585
- continue;
586
- }
587
-
588
- current += char;
589
- }
590
-
591
- if (current.trim()) {
592
- args.push(current.trim());
593
- }
594
-
595
- return args;
596
- }
597
-
598
- function getGeneratedMethodArgumentCount(signatureParams) {
599
- const args = splitTopLevelArguments(signatureParams);
600
-
601
- if (args.length === 0) {
602
- return 0;
603
- }
604
-
605
- const lastArg = args[args.length - 1];
606
-
607
- if (/\bparams\s*:\s*RequestParams\b/.test(lastArg)) {
608
- return args.length - 1;
609
- }
610
-
611
- return args.length;
612
- }
613
-
614
- async function createMethodFiles() {
615
- const defaultHandlers = resolveDefaultHandlers(methodsDir);
616
-
617
- const generatedFiles = fs
618
- .readdirSync(generatedDir)
619
- .filter(
620
- (file) =>
621
- file.endsWith(".ts") &&
622
- file !== "http-client.ts" &&
623
- file !== "data-contracts.ts" &&
624
- !/route\.ts$/i.test(file) &&
625
- !file.includes(".api"),
626
- );
627
-
628
- const methodExports = [];
629
-
630
- for (const file of generatedFiles) {
631
- const filePath = path.join(generatedDir, file);
632
- const source = fs.readFileSync(filePath, "utf8");
633
-
634
- const baseName = file.replace(".ts", "");
635
- const className = baseName.split("-").map(pascalCase).join("");
636
-
637
- const instanceName = `${className.charAt(0).toLowerCase()}${className.slice(1)}Api`;
638
-
639
- const paginatedMethods = [];
640
- const simpleQueryMethods = [];
641
- const nonQueryMethods = [];
642
-
643
- const generatedMethods = getGeneratedMethods(source);
644
-
645
- for (const generatedMethod of generatedMethods) {
646
- const { methodName, methodSource, isFormData } = generatedMethod;
647
-
648
- const signatureRegex = new RegExp(
649
- `${methodName}\\s*=\\s*\\(([^)]*)\\)\\s*=>`,
650
- "m",
651
- );
652
-
653
- const signatureMatch = methodSource.match(signatureRegex);
654
-
655
- if (!signatureMatch) {
656
- continue;
657
- }
658
-
659
- const params = signatureMatch[1].trim();
660
- const methodArgumentCount = getGeneratedMethodArgumentCount(params);
661
-
662
- const returnTypeMatch = methodSource.match(/this\.request<([^,>]+)/m);
663
- const returnType = returnTypeMatch?.[1] ?? "";
664
-
665
- const isPaginated =
666
- returnType.includes("ApiPaginationResponse") ||
667
- returnType.trim().endsWith("[]");
668
-
669
- if (params.startsWith("query")) {
670
- if (isPaginated) {
671
- paginatedMethods.push(methodName);
672
- } else {
673
- simpleQueryMethods.push(methodName);
674
- }
675
-
676
- continue;
677
- }
678
-
679
- nonQueryMethods.push({
680
- methodName,
681
- isFormData,
682
- methodArgumentCount,
683
- });
684
- }
685
-
686
- const queryTypes = [...paginatedMethods, ...simpleQueryMethods]
687
- .map((methodName) =>
688
- `
689
- export type ${toQueryName(methodName)} =
690
- NonNullable<
691
- Parameters<${className}["${methodName}"]>[0]
692
- >;
693
- `.trim(),
694
- )
695
- .join("\n\n");
696
-
697
- const hasPaginatedMethods = paginatedMethods.length > 0;
698
-
699
- const hasFormDataMethods = nonQueryMethods.some(
700
- ({ isFormData }) => isFormData,
701
- );
702
-
703
- const hasRegularNonQueryMethods = nonQueryMethods.some(
704
- ({ isFormData }) => !isFormData,
705
- );
706
-
707
- const paginatedMethodWrappers = paginatedMethods
708
- .map((methodName) => {
709
- if (useFilterFormValues) {
710
- return `
711
- export async function ${methodName}(
712
- filters: FilterFormValues<${toQueryName(methodName)}>[] = [],
713
- page = 1,
714
- pageSize = 100,
715
- sortBy: SortableKeys<
716
- ExtractResponse<ReturnType<${className}["${methodName}"]>>
717
- > | null = null,
718
- sortDirection?: SortDirection,
719
- options: ApiMethodOptions<
720
- ExtractResponse<ReturnType<${className}["${methodName}"]>>,
721
- ExtractError<ReturnType<${className}["${methodName}"]>>,
722
- RequestParams
723
- > = {}
724
- ): Promise<ApiResult<ExtractResponse<ReturnType<${className}["${methodName}"]>>>> {
725
- const { onSuccess, onError, params } = options;
726
-
727
- return handleApiResponse(
728
- () =>
729
- ${instanceName}.${methodName}(
730
- buildQuery<
731
- ${toQueryName(methodName)},
732
- UnwrapArray<
733
- ExtractDataIfPaginated<
734
- ExtractResponse<ReturnType<${className}["${methodName}"]>>
735
- >
736
- >
737
- >(filters, page, pageSize, sortBy, sortDirection),
738
- params ?? {}
739
- ),
740
- ${callbackOptionsExpression(defaultHandlers)}
741
- );
742
- }
743
- `.trim();
744
- }
745
-
746
- return `
747
- export async function ${methodName}(
748
- query?: ${toQueryName(methodName)},
749
- options: ApiMethodOptions<
750
- ExtractResponse<ReturnType<${className}["${methodName}"]>>,
751
- ExtractError<ReturnType<${className}["${methodName}"]>>,
752
- RequestParams
753
- > = {}
754
- ): Promise<ApiResult<ExtractResponse<ReturnType<${className}["${methodName}"]>>>> {
755
- const { onSuccess, onError, params } = options;
756
-
757
- return handleApiResponse(
758
- () =>
759
- ${instanceName}.${methodName}(
760
- (query ?? {}) as ${toQueryName(methodName)},
761
- params ?? {}
762
- ),
763
- ${callbackOptionsExpression(defaultHandlers)}
764
- );
765
- }
766
- `.trim();
767
- })
768
- .join("\n\n");
769
-
770
- const simpleQueryMethodWrappers = simpleQueryMethods
771
- .map((methodName) =>
772
- `
773
- export async function ${methodName}(
774
- query?: ${toQueryName(methodName)},
775
- options: ApiMethodOptions<
776
- ExtractResponse<ReturnType<${className}["${methodName}"]>>,
777
- ExtractError<ReturnType<${className}["${methodName}"]>>,
778
- RequestParams
779
- > = {}
780
- ): Promise<
781
- ApiResult<
782
- ExtractResponse<
783
- ReturnType<
784
- ${className}["${methodName}"]
785
- >
786
- >
787
- >
788
- > {
789
- const { onSuccess, onError, params } = options;
790
-
791
- return handleApiResponse(
792
- () =>
793
- ${instanceName}.${methodName}(
794
- (query ?? {}) as ${toQueryName(methodName)},
795
- params ?? {}
796
- ),
797
- ${callbackOptionsExpression(defaultHandlers)}
798
- );
799
- }
800
- `.trim(),
801
- )
802
- .join("\n\n");
803
-
804
- const nonQueryMethodWrappers = nonQueryMethods
805
- .map(({ methodName, isFormData, methodArgumentCount }) => {
806
- if (isFormData) {
807
- return `
808
- export async function ${methodName}(
809
- data: Parameters<
810
- ${className}["${methodName}"]
811
- >[0],
812
- options: ApiMethodOptions<
813
- ExtractResponse<ReturnType<${className}["${methodName}"]>>,
814
- ExtractError<ReturnType<${className}["${methodName}"]>>,
815
- RequestParams
816
- > = {}
817
- ): Promise<
818
- ApiResult<
819
- ExtractResponse<
820
- ReturnType<
821
- ${className}["${methodName}"]
822
- >
823
- >
824
- >
825
- > {
826
- const { onSuccess, onError, params } = options;
827
- const formData = toFormData(data);
828
-
829
- return handleApiResponse(
830
- () =>
831
- ${instanceName}.${methodName}(
832
- formData as unknown as Parameters<
833
- ${className}["${methodName}"]
834
- >[0],
835
- params ?? {}
836
- ),
837
- ${callbackOptionsExpression(defaultHandlers)}
838
- );
839
- }
840
- `.trim();
841
- }
842
-
843
- return `
844
- export async function ${methodName}(
845
- ...argsWithOptions: [
846
- ...ApiMethodArguments<
847
- ${className}["${methodName}"],
848
- RequestParams
849
- >,
850
- ApiMethodOptions<
851
- ExtractResponse<ReturnType<${className}["${methodName}"]>>,
852
- ExtractError<ReturnType<${className}["${methodName}"]>>,
853
- RequestParams
854
- >?
855
- ]
856
- ): Promise<
857
- ApiResult<
858
- ExtractResponse<
859
- ReturnType<
860
- ${className}["${methodName}"]
861
- >
862
- >
863
- >
864
- > {
865
- const args = [...argsWithOptions] as unknown[];
866
-
867
- const options =
868
- args.length > ${methodArgumentCount}
869
- ? (args.pop() as ApiMethodOptions<
870
- ExtractResponse<ReturnType<${className}["${methodName}"]>>,
871
- ExtractError<ReturnType<${className}["${methodName}"]>>,
872
- RequestParams
873
- >)
874
- : {};
875
-
876
- const { onSuccess, onError, params } = options ?? {};
877
-
878
- const requestArgs = [
879
- ...args,
880
- params ?? {}
881
- ] as unknown as Parameters<
882
- ${className}["${methodName}"]
883
- >;
884
-
885
- return handleApiResponse(
886
- () =>
887
- ${instanceName}.${methodName}(
888
- ...requestArgs
889
- ),
890
- ${callbackOptionsExpression(defaultHandlers)}
891
- );
892
- }
893
- `.trim();
894
- })
895
- .join("\n\n");
896
-
897
- const runtimeValueImportNames = [
898
- ...(hasPaginatedMethods && useFilterFormValues ? ["buildQuery"] : []),
899
- ...(hasFormDataMethods ? ["toFormData"] : []),
900
- "handleApiResponse",
901
- ];
902
-
903
- const runtimeTypeImportNames = [
904
- "ApiResult",
905
- "ApiMethodOptions",
906
- ...(hasRegularNonQueryMethods ? ["ApiMethodArguments"] : []),
907
- "ExtractResponse",
908
- "ExtractError",
909
-
910
- ...(hasPaginatedMethods && useFilterFormValues
911
- ? [
912
- "ExtractDataIfPaginated",
913
- "FilterFormValues",
914
- "SortableKeys",
915
- "SortDirection",
916
- "UnwrapArray",
917
- ]
918
- : []),
919
- ];
920
-
921
- const runtimeValueImports = createImportStatement({
922
- names: runtimeValueImportNames,
923
- from: runtimePackageName,
924
- });
925
-
926
- const defaultFunctionImports = defaultHandlers
927
- ? createAliasedImportStatement({
928
- imports: [
929
- {
930
- name: defaultHandlers.defaultSuccessHandlerName,
931
- alias: "typedApiDefaultSuccessHandler",
932
- },
933
- {
934
- name: defaultHandlers.defaultErrorHandlerName,
935
- alias: "typedApiDefaultErrorHandler",
936
- },
937
- ],
938
- from: defaultHandlers.defaultFunctionsPath,
939
- })
940
- : "";
941
-
942
- const runtimeTypeImports = createImportStatement({
943
- names: runtimeTypeImportNames,
944
- from: runtimePackageName,
945
- typeOnly: useTypeOnlyImports,
946
- });
947
-
948
- const generatedValueImports = createImportStatement({
949
- names: [className],
950
- from: `../generated/${baseName}`,
951
- });
952
-
953
- const generatedTypeImports =
954
- generatedMethods.length > 0
955
- ? createImportStatement({
956
- names: ["RequestParams"],
957
- from: "../generated/http-client",
958
- typeOnly: useTypeOnlyImports,
959
- })
960
- : "";
961
-
962
- const content = `
963
- ${runtimeValueImports}
964
-
965
- ${defaultFunctionImports}
966
-
967
- ${runtimeTypeImports}
968
-
969
- ${generatedValueImports}
970
-
971
- ${generatedTypeImports}
972
-
973
- /* =======================
974
- Query Types
975
- ======================= */
976
- ${queryTypes}
977
-
978
- /* =======================
979
- API Instance
980
- ======================= */
981
- const ${instanceName} = new ${className}();
982
-
983
- /* =======================
984
- Paginated Query Methods
985
- ======================= */
986
- ${paginatedMethodWrappers}
987
-
988
- /* =======================
989
- Simple Query Methods
990
- ======================= */
991
- ${simpleQueryMethodWrappers}
992
-
993
- /* =======================
994
- Non-Query Methods
995
- ======================= */
996
- ${nonQueryMethodWrappers}
997
- `.trim();
998
-
999
- const outputFile = path.join(methodsDir, `${className}.api.ts`);
1000
-
1001
- await writeFormattedFile(outputFile, content);
1002
-
1003
- methodExports.push(`export * from "./methods/${className}.api";`);
1004
- }
1005
-
1006
- const apiIndexContent = `
1007
- export * from "./generated/data-contracts";
1008
- export * from "./generated/http-client";
1009
- ${methodExports.join("\n")}
1010
- `.trim();
1011
-
1012
- await writeFormattedFile(path.join(apiRoot, "index.ts"), apiIndexContent);
1013
- }
1014
-
1015
- async function fixGeneratedMixedImports(apiDir) {
1016
- const tsFiles = getAllTsFiles(apiDir);
1017
-
1018
- for (const filePath of tsFiles) {
1019
- if (
1020
- filePath.endsWith("http-client.ts") ||
1021
- filePath.endsWith("data-contracts.ts")
1022
- ) {
1023
- continue;
1024
- }
1025
-
1026
- const originalContent = fs.readFileSync(filePath, "utf8");
1027
- let content = originalContent;
1028
-
1029
- content = content.replace(
1030
- /import\s*\{([\s\S]*?)\}\s*from\s*["']\.\/http-client["'];/g,
1031
- (_match, importsText) => {
1032
- const importedNames = importsText
1033
- .split(",")
1034
- .map((name) => name.trim())
1035
- .filter(Boolean);
1036
-
1037
- const typeNames = importedNames.filter(
1038
- (name) => name === "RequestParams",
1039
- );
1040
-
1041
- const valueNames = importedNames.filter(
1042
- (name) => name !== "RequestParams",
1043
- );
1044
-
1045
- const statements = [];
1046
-
1047
- if (valueNames.length > 0) {
1048
- statements.push(
1049
- `import { ${valueNames.join(", ")} } from "./http-client";`,
1050
- );
1051
- }
1052
-
1053
- if (typeNames.length > 0 && useTypeOnlyImports) {
1054
- statements.push(
1055
- `import type { ${typeNames.join(", ")} } from "./http-client";`,
1056
- );
1057
- } else if (typeNames.length > 0) {
1058
- statements.push(
1059
- `import { ${typeNames.join(", ")} } from "./http-client";`,
1060
- );
1061
- }
1062
-
1063
- return statements.join("\n");
1064
- },
1065
- );
1066
-
1067
- if (useTypeOnlyImports) {
1068
- content = content.replace(
1069
- /import\s*\{([\s\S]*?)\}\s*from\s*["']\.\/data-contracts["'];/g,
1070
- (_match, importsText) =>
1071
- `import type {${importsText}} from "./data-contracts";`,
1072
- );
1073
- }
1074
-
1075
- if (content !== originalContent) {
1076
- await writeFormattedFile(filePath, content);
1077
-
1078
- console.log(
1079
- `Fixed mixed imports in ${path.relative(process.cwd(), filePath)}`,
1080
- );
1081
- }
1082
- }
1083
- }
1084
-
1085
450
  async function main() {
1086
451
  const swaggerInput = await resolveSwaggerInput();
1087
452
 
1088
453
  console.log(`Generating API from: ${swaggerInput.label}`);
1089
454
  console.log(`Output folder: ${apiRoot}`);
455
+ console.log(
456
+ "Generator: local typedapi OpenAPI generator, function-only methods",
457
+ );
1090
458
 
1091
- await generateOpenApiClient(swaggerInput.input);
1092
-
1093
- await fixGeneratedMixedImports(generatedDir);
459
+ const result = await generateApi({
460
+ input: swaggerInput.input,
461
+ output: apiRoot,
462
+ ...generatorOptions,
463
+ defaultHandlers: resolveDefaultHandlers(),
464
+ });
1094
465
 
1095
- await createMethodFiles();
466
+ for (const file of result.files) {
467
+ console.log(`Generated ${path.relative(cwd, file)}`);
468
+ }
1096
469
 
1097
470
  console.log("API generation completed.");
1098
471
  }