@nestia/sdk 14.0.0 → 14.0.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 (186) hide show
  1. package/README.md +1 -1
  2. package/assets/bundle/distribute/package.json +2 -2
  3. package/assets/bundle/distribute/tsconfig.json +15 -105
  4. package/lib/INestiaConfig.d.ts +19 -10
  5. package/lib/NestiaSdkApplication.js +58 -27
  6. package/lib/NestiaSdkApplication.js.map +1 -1
  7. package/lib/NestiaSwaggerComposer.js +11 -10
  8. package/lib/NestiaSwaggerComposer.js.map +1 -1
  9. package/lib/analyses/ConfigAnalyzer.js +4 -4
  10. package/lib/analyses/ConfigAnalyzer.js.map +1 -1
  11. package/lib/analyses/ParameterNameAnalyzer.d.ts +20 -0
  12. package/lib/analyses/ParameterNameAnalyzer.js +48 -0
  13. package/lib/analyses/ParameterNameAnalyzer.js.map +1 -0
  14. package/lib/analyses/PathAnalyzer.d.ts +30 -0
  15. package/lib/analyses/PathAnalyzer.js +108 -16
  16. package/lib/analyses/PathAnalyzer.js.map +1 -1
  17. package/lib/analyses/ReflectControllerAnalyzer.js +2 -1
  18. package/lib/analyses/ReflectControllerAnalyzer.js.map +1 -1
  19. package/lib/analyses/ReflectHttpOperationAnalyzer.js +7 -7
  20. package/lib/analyses/ReflectHttpOperationAnalyzer.js.map +1 -1
  21. package/lib/analyses/ReflectHttpOperationParameterAnalyzer.js +122 -23
  22. package/lib/analyses/ReflectHttpOperationParameterAnalyzer.js.map +1 -1
  23. package/lib/analyses/ReflectHttpOperationResponseAnalyzer.js +5 -2
  24. package/lib/analyses/ReflectHttpOperationResponseAnalyzer.js.map +1 -1
  25. package/lib/analyses/ReflectMcpOperationAnalyzer.js +5 -2
  26. package/lib/analyses/ReflectMcpOperationAnalyzer.js.map +1 -1
  27. package/lib/analyses/ReflectWebSocketOperationAnalyzer.js +6 -3
  28. package/lib/analyses/ReflectWebSocketOperationAnalyzer.js.map +1 -1
  29. package/lib/analyses/SecurityAnalyzer.d.ts +10 -0
  30. package/lib/analyses/SecurityAnalyzer.js +24 -19
  31. package/lib/analyses/SecurityAnalyzer.js.map +1 -1
  32. package/lib/analyses/SwaggerExampleAnalyzer.d.ts +15 -0
  33. package/lib/analyses/SwaggerExampleAnalyzer.js +20 -0
  34. package/lib/analyses/SwaggerExampleAnalyzer.js.map +1 -0
  35. package/lib/analyses/TypedHttpRouteAnalyzer.js +30 -21
  36. package/lib/analyses/TypedHttpRouteAnalyzer.js.map +1 -1
  37. package/lib/analyses/TypedWebSocketRouteAnalyzer.js +1 -1
  38. package/lib/analyses/TypedWebSocketRouteAnalyzer.js.map +1 -1
  39. package/lib/executable/internal/NestiaSdkWatcher.js +3 -3
  40. package/lib/executable/internal/NestiaSdkWatcher.js.map +1 -1
  41. package/lib/executable/sdk.js +16 -1
  42. package/lib/executable/sdk.js.map +1 -1
  43. package/lib/factories/ExpressionFactory.d.ts +3 -0
  44. package/lib/factories/ExpressionFactory.js +6 -2
  45. package/lib/factories/ExpressionFactory.js.map +1 -1
  46. package/lib/factories/LiteralFactory.js +3 -1
  47. package/lib/factories/LiteralFactory.js.map +1 -1
  48. package/lib/generates/SdkGenerator.js +3 -3
  49. package/lib/generates/SdkGenerator.js.map +1 -1
  50. package/lib/generates/SwaggerGenerator.js +135 -27
  51. package/lib/generates/SwaggerGenerator.js.map +1 -1
  52. package/lib/generates/internal/FilePrinter.d.ts +14 -0
  53. package/lib/generates/internal/FilePrinter.js +31 -0
  54. package/lib/generates/internal/FilePrinter.js.map +1 -1
  55. package/lib/generates/internal/ImportDictionary.d.ts +2 -0
  56. package/lib/generates/internal/ImportDictionary.js +13 -0
  57. package/lib/generates/internal/ImportDictionary.js.map +1 -1
  58. package/lib/generates/internal/SdkAliasCollection.d.ts +2 -1
  59. package/lib/generates/internal/SdkAliasCollection.js +8 -4
  60. package/lib/generates/internal/SdkAliasCollection.js.map +1 -1
  61. package/lib/generates/internal/SdkDistributionComposer.js +72 -14
  62. package/lib/generates/internal/SdkDistributionComposer.js.map +1 -1
  63. package/lib/generates/internal/SdkFileProgrammer.js +83 -12
  64. package/lib/generates/internal/SdkFileProgrammer.js.map +1 -1
  65. package/lib/generates/internal/SdkHttpFunctionProgrammer.js +21 -23
  66. package/lib/generates/internal/SdkHttpFunctionProgrammer.js.map +1 -1
  67. package/lib/generates/internal/SdkHttpNamespaceProgrammer.js +27 -34
  68. package/lib/generates/internal/SdkHttpNamespaceProgrammer.js.map +1 -1
  69. package/lib/generates/internal/SdkHttpParameterProgrammer.d.ts +40 -1
  70. package/lib/generates/internal/SdkHttpParameterProgrammer.js +105 -6
  71. package/lib/generates/internal/SdkHttpParameterProgrammer.js.map +1 -1
  72. package/lib/generates/internal/SdkHttpRouteProgrammer.js +16 -32
  73. package/lib/generates/internal/SdkHttpRouteProgrammer.js.map +1 -1
  74. package/lib/generates/internal/SdkHttpSimulationProgrammer.js +18 -14
  75. package/lib/generates/internal/SdkHttpSimulationProgrammer.js.map +1 -1
  76. package/lib/generates/internal/SdkMcpRouteProgrammer.js +39 -11
  77. package/lib/generates/internal/SdkMcpRouteProgrammer.js.map +1 -1
  78. package/lib/generates/internal/SdkPathTemplate.d.ts +21 -0
  79. package/lib/generates/internal/SdkPathTemplate.js +50 -0
  80. package/lib/generates/internal/SdkPathTemplate.js.map +1 -0
  81. package/lib/generates/internal/SdkTypeProgrammer.js +15 -15
  82. package/lib/generates/internal/SdkTypeProgrammer.js.map +1 -1
  83. package/lib/generates/internal/SdkTypeTagProgrammer.d.ts +20 -1
  84. package/lib/generates/internal/SdkTypeTagProgrammer.js +386 -67
  85. package/lib/generates/internal/SdkTypeTagProgrammer.js.map +1 -1
  86. package/lib/generates/internal/SdkWebSocketNamespaceProgrammer.js +27 -36
  87. package/lib/generates/internal/SdkWebSocketNamespaceProgrammer.js.map +1 -1
  88. package/lib/generates/internal/SdkWebSocketParameterProgrammer.d.ts +35 -1
  89. package/lib/generates/internal/SdkWebSocketParameterProgrammer.js +72 -5
  90. package/lib/generates/internal/SdkWebSocketParameterProgrammer.js.map +1 -1
  91. package/lib/generates/internal/SdkWebSocketRouteProgrammer.js +57 -33
  92. package/lib/generates/internal/SdkWebSocketRouteProgrammer.js.map +1 -1
  93. package/lib/generates/internal/SwaggerOperationComposer.js +19 -8
  94. package/lib/generates/internal/SwaggerOperationComposer.js.map +1 -1
  95. package/lib/generates/internal/SwaggerOperationParameterComposer.d.ts +1 -1
  96. package/lib/generates/internal/SwaggerOperationParameterComposer.js +201 -19
  97. package/lib/generates/internal/SwaggerOperationParameterComposer.js.map +1 -1
  98. package/lib/generates/internal/SwaggerOperationResponseComposer.d.ts +2 -0
  99. package/lib/generates/internal/SwaggerOperationResponseComposer.js +39 -7
  100. package/lib/generates/internal/SwaggerOperationResponseComposer.js.map +1 -1
  101. package/lib/internal/legacy.d.ts +49 -18
  102. package/lib/internal/legacy.js +84 -25
  103. package/lib/internal/legacy.js.map +1 -1
  104. package/lib/structures/IOperationMetadata.d.ts +22 -0
  105. package/lib/structures/IReflectHttpOperationException.d.ts +3 -2
  106. package/lib/structures/IReflectHttpOperationParameter.d.ts +8 -3
  107. package/lib/structures/IReflectHttpOperationSuccess.d.ts +4 -3
  108. package/lib/structures/ITypedHttpRoute.d.ts +8 -0
  109. package/lib/structures/ITypedHttpRouteException.d.ts +3 -1
  110. package/lib/structures/ITypedHttpRouteParameter.d.ts +3 -1
  111. package/lib/structures/ITypedHttpRouteSuccess.d.ts +3 -1
  112. package/lib/structures/ITypedWebSocketRoute.d.ts +8 -0
  113. package/lib/utils/SourceFinder.d.ts +10 -0
  114. package/lib/utils/SourceFinder.js +27 -4
  115. package/lib/utils/SourceFinder.js.map +1 -1
  116. package/lib/utils/VersioningStrategy.d.ts +13 -1
  117. package/lib/utils/VersioningStrategy.js +24 -12
  118. package/lib/utils/VersioningStrategy.js.map +1 -1
  119. package/native/go.mod +1 -1
  120. package/native/go.sum +2 -2
  121. package/native/sdk/sdk_http_rules.go +210 -0
  122. package/native/sdk/sdk_metadata_json.go +73 -3
  123. package/native/sdk/sdk_transform.go +801 -49
  124. package/package.json +6 -6
  125. package/src/INestiaConfig.ts +19 -10
  126. package/src/NestiaSdkApplication.ts +63 -34
  127. package/src/NestiaSwaggerComposer.ts +11 -8
  128. package/src/analyses/ConfigAnalyzer.ts +3 -3
  129. package/src/analyses/ParameterNameAnalyzer.ts +51 -0
  130. package/src/analyses/PathAnalyzer.ts +106 -14
  131. package/src/analyses/ReflectControllerAnalyzer.ts +2 -1
  132. package/src/analyses/ReflectHttpOperationAnalyzer.ts +4 -4
  133. package/src/analyses/ReflectHttpOperationParameterAnalyzer.ts +122 -39
  134. package/src/analyses/ReflectHttpOperationResponseAnalyzer.ts +6 -7
  135. package/src/analyses/ReflectMcpOperationAnalyzer.ts +6 -2
  136. package/src/analyses/ReflectWebSocketOperationAnalyzer.ts +7 -3
  137. package/src/analyses/SecurityAnalyzer.ts +30 -20
  138. package/src/analyses/SwaggerExampleAnalyzer.ts +23 -0
  139. package/src/analyses/TypedHttpRouteAnalyzer.ts +29 -20
  140. package/src/analyses/TypedWebSocketRouteAnalyzer.ts +1 -0
  141. package/src/executable/internal/NestiaSdkWatcher.ts +2 -2
  142. package/src/executable/sdk.ts +24 -1
  143. package/src/factories/ExpressionFactory.ts +8 -2
  144. package/src/factories/LiteralFactory.ts +7 -2
  145. package/src/generates/SdkGenerator.ts +3 -3
  146. package/src/generates/SwaggerGenerator.ts +173 -32
  147. package/src/generates/internal/FilePrinter.ts +28 -0
  148. package/src/generates/internal/ImportDictionary.ts +11 -0
  149. package/src/generates/internal/SdkAliasCollection.ts +11 -4
  150. package/src/generates/internal/SdkDistributionComposer.ts +89 -15
  151. package/src/generates/internal/SdkFileProgrammer.ts +108 -16
  152. package/src/generates/internal/SdkHttpFunctionProgrammer.ts +28 -30
  153. package/src/generates/internal/SdkHttpNamespaceProgrammer.ts +34 -64
  154. package/src/generates/internal/SdkHttpParameterProgrammer.ts +193 -13
  155. package/src/generates/internal/SdkHttpRouteProgrammer.ts +17 -31
  156. package/src/generates/internal/SdkHttpSimulationProgrammer.ts +51 -14
  157. package/src/generates/internal/SdkMcpRouteProgrammer.ts +112 -15
  158. package/src/generates/internal/SdkPathTemplate.ts +73 -0
  159. package/src/generates/internal/SdkTypeProgrammer.ts +19 -22
  160. package/src/generates/internal/SdkTypeTagProgrammer.ts +506 -78
  161. package/src/generates/internal/SdkWebSocketNamespaceProgrammer.ts +43 -68
  162. package/src/generates/internal/SdkWebSocketParameterProgrammer.ts +126 -5
  163. package/src/generates/internal/SdkWebSocketRouteProgrammer.ts +71 -48
  164. package/src/generates/internal/SwaggerOperationComposer.ts +20 -6
  165. package/src/generates/internal/SwaggerOperationParameterComposer.ts +279 -30
  166. package/src/generates/internal/SwaggerOperationResponseComposer.ts +55 -11
  167. package/src/internal/legacy.ts +114 -26
  168. package/src/structures/IOperationMetadata.ts +28 -0
  169. package/src/structures/IReflectHttpOperationException.ts +7 -2
  170. package/src/structures/IReflectHttpOperationParameter.ts +9 -2
  171. package/src/structures/IReflectHttpOperationSuccess.ts +8 -3
  172. package/src/structures/ITypedHttpRoute.ts +8 -0
  173. package/src/structures/ITypedHttpRouteException.ts +4 -1
  174. package/src/structures/ITypedHttpRouteParameter.ts +3 -1
  175. package/src/structures/ITypedHttpRouteSuccess.ts +4 -1
  176. package/src/structures/ITypedWebSocketRoute.ts +8 -0
  177. package/src/utils/SourceFinder.ts +25 -4
  178. package/src/utils/VersioningStrategy.ts +28 -13
  179. package/lib/validators/HttpHeadersValidator.d.ts +0 -11
  180. package/lib/validators/HttpHeadersValidator.js +0 -15
  181. package/lib/validators/HttpHeadersValidator.js.map +0 -1
  182. package/lib/validators/HttpQueryValidator.d.ts +0 -10
  183. package/lib/validators/HttpQueryValidator.js +0 -14
  184. package/lib/validators/HttpQueryValidator.js.map +0 -1
  185. package/src/validators/HttpHeadersValidator.ts +0 -12
  186. package/src/validators/HttpQueryValidator.ts +0 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestia/sdk",
3
- "version": "14.0.0",
3
+ "version": "14.0.1",
4
4
  "description": "Nestia SDK and Swagger generator",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -55,12 +55,12 @@
55
55
  "tgrid": "^1.2.1",
56
56
  "ttsc": "^0.28.1",
57
57
  "tstl": "^3.0.0",
58
- "typia": "^15.0.0",
59
- "@nestia/fetcher": "^14.0.0",
60
- "@nestia/core": "^14.0.0"
58
+ "typia": "15.0.0",
59
+ "@nestia/core": "^14.0.1",
60
+ "@nestia/fetcher": "^14.0.1"
61
61
  },
62
62
  "peerDependencies": {
63
- "@nestia/core": "^14.0.0"
63
+ "@nestia/core": "^14.0.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@modelcontextprotocol/sdk": "^1.18.0",
@@ -88,7 +88,7 @@
88
88
  "scripts": {
89
89
  "build": "rimraf lib && ttsc",
90
90
  "dev": "ttsc --watch",
91
- "test:go": "cd test && go test ./..."
91
+ "test:go": "cd test && go test -count=1 ./..."
92
92
  },
93
93
  "types": "lib/index.d.ts"
94
94
  }
@@ -174,12 +174,11 @@ export namespace INestiaConfig {
174
174
  /**
175
175
  * OpenAPI version.
176
176
  *
177
- * If you configure this property to be `2.0` or `3.0`, the newly generated
178
- * `swagger.json` file would follow the specified OpenAPI version. The newly
179
- * generated `swagger.json` file would be downgraded from the OpenAPI v3.1
180
- * specification by {@link OpenApi.downgrade} method.
177
+ * If you configure this property to be `2.0`, `3.0`, or `3.1`, the newly
178
+ * generated `swagger.json` file would follow the specified OpenAPI version,
179
+ * downgraded from the OpenAPI v3.2 specification.
181
180
  *
182
- * @default 3.1
181
+ * @default 3.2
183
182
  */
184
183
  openapi?: "2.0" | "3.0" | "3.1" | "3.2";
185
184
 
@@ -199,10 +198,12 @@ export namespace INestiaConfig {
199
198
  * Whether to include additional information or not.
200
199
  *
201
200
  * If configured to be `true`, those properties would be added into each API
202
- * endpoinnt.
201
+ * endpoint.
203
202
  *
204
- * - `x-nestia-method`
205
- * - `x-nestia-namespace` ` `x-nestia-jsDocTags`
203
+ * - `x-nestia-method`: the HTTP method, such as `"GET"`
204
+ * - `x-nestia-namespace`: the SDK function's accessor, such as
205
+ * `"bbs.articles.index"`
206
+ * - `x-nestia-jsDocTags`: the JSDoc tags of the controller method
206
207
  *
207
208
  * @default false
208
209
  */
@@ -223,7 +224,10 @@ export namespace INestiaConfig {
223
224
  *
224
225
  * When generating `swagger.json` file through `nestia`, if your controllers
225
226
  * or theirs methods have a security key which is not enrolled in here
226
- * property, it would be an error.
227
+ * property, it would be an error. So would an OAuth2 scope none of the
228
+ * scheme's flows declares, and, for an OpenAPI `3.0` or `2.0` document,
229
+ * scopes on a scheme other than OAuth2 or OpenID Connect, which those
230
+ * versions require to be empty; from `3.1` on they list role names.
227
231
  */
228
232
  security?: Record<string, OpenApi.ISecurityScheme>;
229
233
 
@@ -245,7 +249,12 @@ export namespace INestiaConfig {
245
249
  * If you configure this property to be `true`, the query DTO would be
246
250
  * decomposed into individual query parameters per each property. Otherwise
247
251
  * you set it to be `false`, the query DTO would be one object type which
248
- * contains all of query parameters.
252
+ * contains all of query parameters, spread into its keys by `style: form`
253
+ * and `explode: true`.
254
+ *
255
+ * Swagger 2.0 has no object query parameter, and no OpenAPI version can
256
+ * spread an object into headers, so a query DTO of a Swagger 2.0 document
257
+ * and a headers DTO are decomposed regardless of this property.
249
258
  *
250
259
  * @default true
251
260
  */
@@ -62,18 +62,18 @@ export class NestiaSdkApplication {
62
62
  "Error on NestiaApplication.e2e(): configure INestiaConfig.e2e property.",
63
63
  );
64
64
 
65
- const validate =
66
- (title: string) =>
67
- async (location: string): Promise<void> => {
68
- const parent: string = path.resolve(location + "/..");
69
- const stats: fs.Stats = await fs.promises.lstat(parent);
70
- if (stats.isDirectory() === false)
71
- throw new Error(
72
- `Error on NestiaApplication.e2e(): output directory of ${title} does not exists.`,
73
- );
74
- };
75
- await validate("sdk")(this.config.output);
76
- await validate("e2e")(this.config.e2e);
65
+ await assertDirectory({
66
+ method: "e2e",
67
+ property: "output",
68
+ location: this.config.output,
69
+ directory: path.resolve(this.config.output + "/.."),
70
+ });
71
+ await assertDirectory({
72
+ method: "e2e",
73
+ property: "e2e",
74
+ location: this.config.e2e,
75
+ directory: path.resolve(this.config.e2e + "/.."),
76
+ });
77
77
 
78
78
  print_title("Nestia E2E Generator");
79
79
  await this.generate({
@@ -90,12 +90,12 @@ export class NestiaSdkApplication {
90
90
  "Error on NestiaApplication.sdk(): configure INestiaConfig.output property.",
91
91
  );
92
92
 
93
- const parent: string = path.resolve(this.config.output + "/..");
94
- const stats: fs.Stats = await fs.promises.lstat(parent);
95
- if (stats.isDirectory() === false)
96
- throw new Error(
97
- "Error on NestiaApplication.sdk(): output directory does not exists.",
98
- );
93
+ await assertDirectory({
94
+ method: "sdk",
95
+ property: "output",
96
+ location: this.config.output,
97
+ directory: path.resolve(this.config.output + "/.."),
98
+ });
99
99
 
100
100
  print_title("Nestia SDK Generator");
101
101
  await this.generate({
@@ -111,14 +111,14 @@ export class NestiaSdkApplication {
111
111
  );
112
112
 
113
113
  const parsed: path.ParsedPath = path.parse(this.config.swagger.output);
114
- const directory: string = !!parsed.ext
115
- ? path.resolve(parsed.dir)
116
- : this.config.swagger.output;
117
- const stats: fs.Stats = await fs.promises.lstat(directory);
118
- if (stats.isDirectory() === false)
119
- throw new Error(
120
- "Error on NestiaApplication.swagger(): output directory does not exists.",
121
- );
114
+ await assertDirectory({
115
+ method: "swagger",
116
+ property: "swagger.output",
117
+ location: this.config.swagger.output,
118
+ directory: !!parsed.ext
119
+ ? path.resolve(parsed.dir)
120
+ : path.resolve(this.config.swagger.output),
121
+ });
122
122
 
123
123
  print_title("Nestia Swagger Generator");
124
124
  await this.generate({
@@ -218,10 +218,10 @@ export class NestiaSdkApplication {
218
218
  continue;
219
219
  }
220
220
  const pathList: Set<string> = new Set();
221
- const versions: string[] = VersioningStrategy.merge(project)([
222
- ...(c.versions ?? []),
223
- ...(o.versions ?? []),
224
- ]);
221
+ const versions: string[] = VersioningStrategy.merge(project)({
222
+ controller: c.versions,
223
+ method: o.versions,
224
+ });
225
225
  for (const v of versions)
226
226
  for (const prefix of wrapPaths(c.prefixes))
227
227
  for (const cPath of wrapPaths(c.paths))
@@ -288,6 +288,30 @@ export class NestiaSdkApplication {
288
288
  }
289
289
  }
290
290
 
291
+ /**
292
+ * Asserts the directory an output is written into exists, naming the configured
293
+ * location. `fs.promises.stat()` itself rejects a missing path, so it is caught
294
+ * rather than left to surface as a bare `ENOENT`; a symbolic link to a
295
+ * directory is followed.
296
+ */
297
+ const assertDirectory = async (props: {
298
+ method: string;
299
+ property: string;
300
+ location: string;
301
+ directory: string;
302
+ }): Promise<void> => {
303
+ const stats: fs.Stats | null = await fs.promises
304
+ .stat(props.directory)
305
+ .catch((error: NodeJS.ErrnoException): null => {
306
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
307
+ throw error;
308
+ });
309
+ if (stats === null || stats.isDirectory() === false)
310
+ throw new Error(
311
+ `Error on NestiaApplication.${props.method}(): directory ${JSON.stringify(props.directory)} of INestiaConfig.${props.property} ${JSON.stringify(props.location)} ${stats === null ? "does not exist" : "is not a directory"}.`,
312
+ );
313
+ };
314
+
291
315
  const print_title = (str: string): void => {
292
316
  console.log("-----------------------------------------------------------");
293
317
  console.log(` ${str}`);
@@ -308,6 +332,8 @@ const report = (props: {
308
332
  console.log("");
309
333
  print_title(`Nestia ${StringUtil.capitalize(props.type)} Report`);
310
334
 
335
+ // every contradiction at once, not only the first function's
336
+ const messages: string[] = [];
311
337
  for (const {
312
338
  first: { error },
313
339
  second: contents,
@@ -318,24 +344,27 @@ const report = (props: {
318
344
  `${location} - `,
319
345
  error.class,
320
346
  ...(error.function !== null ? [`.${error.function}()`] : [""]),
321
- ...(error.from !== null ? [` from ${error.from}`] : [""]),
347
+ // an empty origin names no part of the function
348
+ ...(error.from ? [` from ${error.from}`] : [""]),
322
349
  ":\n",
323
350
  contents
324
351
  .map((c) => {
325
352
  if (typeof c === "string") return ` - ${c}`;
326
353
  else
327
354
  return [
355
+ // the property the type was found at, when there is one
328
356
  c.accessor
329
- ? ` - ${c.name}: `
330
- : ` - ${c.name} (${c.accessor}): `,
357
+ ? ` - ${c.name} (${c.accessor}): `
358
+ : ` - ${c.name}: `,
331
359
  ...c.messages.map((msg) => ` - ${msg}`),
332
360
  ].join("\n");
333
361
  })
334
362
  .join("\n"),
335
363
  ].join("");
336
- if (props.type === "error") throw new Error(message);
364
+ if (props.type === "error") messages.push(message);
337
365
  else console.log(message);
338
366
  }
367
+ if (messages.length !== 0) throw new Error(messages.join("\n\n"));
339
368
  };
340
369
 
341
370
  const wrapPaths = (paths: string[]): string[] =>
@@ -68,10 +68,10 @@ export namespace NestiaSwaggerComposer {
68
68
  for (const o of c.operations) {
69
69
  if (o.protocol !== "http") continue;
70
70
  const pathList: Set<string> = new Set();
71
- const versions: string[] = VersioningStrategy.merge(project)([
72
- ...(c.versions ?? []),
73
- ...(o.versions ?? []),
74
- ]);
71
+ const versions: string[] = VersioningStrategy.merge(project)({
72
+ controller: c.versions,
73
+ method: o.versions,
74
+ });
75
75
  for (const v of versions)
76
76
  for (const prefix of wrapPaths(c.prefixes))
77
77
  for (const cPath of wrapPaths(c.paths))
@@ -131,7 +131,8 @@ const report = (props: {
131
131
  `${location} - `,
132
132
  error.class,
133
133
  ...(error.function !== null ? [`.${error.function}()`] : [""]),
134
- ...(error.from !== null ? [` from ${error.from}`] : [""]),
134
+ // an empty origin names no part of the function
135
+ ...(error.from ? [` from ${error.from}`] : [""]),
135
136
  ":\n",
136
137
  contents
137
138
  .map((c) => {
@@ -139,8 +140,8 @@ const report = (props: {
139
140
  else
140
141
  return [
141
142
  c.accessor
142
- ? ` - ${c.name}: `
143
- : ` - ${c.name} (${c.accessor}): `,
143
+ ? ` - ${c.name} (${c.accessor}): `
144
+ : ` - ${c.name}: `,
144
145
  ...c.messages.map((msg) => ` - ${msg}`),
145
146
  ].join("\n");
146
147
  })
@@ -148,7 +149,9 @@ const report = (props: {
148
149
  ].join(""),
149
150
  );
150
151
  }
151
- throw new Error(`Error on NestiaSwaggerComposer.compose():\n${messages}`);
152
+ throw new Error(
153
+ `Error on NestiaSwaggerComposer.compose():\n${messages.join("\n\n")}`,
154
+ );
152
155
  };
153
156
 
154
157
  const wrapPaths = (paths: string[]): string[] =>
@@ -111,10 +111,10 @@ export namespace ConfigAnalyzer {
111
111
  versioning === undefined || versioning.type !== VersioningType.URI
112
112
  ? undefined
113
113
  : {
114
+ // as NestJS's RoutePathFactory.getVersionPrefix(): `false` is no
115
+ // prefix at all, and only an absent one is the default "v"
114
116
  prefix:
115
- versioning.prefix === undefined || versioning.prefix === false
116
- ? "v"
117
- : versioning.prefix,
117
+ versioning.prefix === false ? "" : (versioning.prefix ?? "v"),
118
118
  defaultVersion: versioning.defaultVersion,
119
119
  },
120
120
  };
@@ -0,0 +1,51 @@
1
+ import { NamingConvention } from "@typia/utils";
2
+
3
+ import { StringUtil } from "../utils/StringUtil";
4
+
5
+ export namespace ParameterNameAnalyzer {
6
+ export interface IParameter {
7
+ name: string;
8
+ category: string;
9
+ field?: string | null;
10
+ }
11
+
12
+ /**
13
+ * Names every parameter that declares no name of its own.
14
+ *
15
+ * A destructured parameter, `@TypedBody() { title }: IBody`, binds names but
16
+ * is not one, so the SDK's metadata carries it with an empty name. Its source
17
+ * text is no identifier the SDK could declare, and passing it as one would
18
+ * send `{ title }` alone. It is named after what it carries instead: its
19
+ * field when that is an identifier, or reads as one with its separators
20
+ * dropped in camel case, otherwise its category, such as `body` or `query`.
21
+ * The name yields to every other parameter's, which are the user's and are
22
+ * kept.
23
+ */
24
+ export const name = <T extends IParameter>(parameters: T[]): T[] => {
25
+ const taken: string[] = parameters
26
+ .map((p) => p.name)
27
+ .filter((name) => name.length !== 0);
28
+ return parameters.map((p) => {
29
+ if (p.name.length !== 0) return p;
30
+ const name: string = StringUtil.escapeDuplicate(taken)(candidate(p));
31
+ taken.push(name);
32
+ return { ...p, name };
33
+ });
34
+ };
35
+
36
+ const candidate = (p: IParameter): string => {
37
+ if (p.field !== undefined && p.field !== null && p.field.length !== 0) {
38
+ if (NamingConvention.variable(p.field)) return p.field;
39
+ // `x-tenant` as `xTenant`
40
+ const camel: string = p.field
41
+ .split(/[^A-Za-z0-9_$]+/)
42
+ .filter((word) => word.length !== 0)
43
+ .map((word, i) =>
44
+ i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),
45
+ )
46
+ .join("");
47
+ if (NamingConvention.variable(camel)) return camel;
48
+ }
49
+ return p.category;
50
+ };
51
+ }
@@ -1,16 +1,19 @@
1
1
  import { RequestMethod } from "@nestjs/common";
2
- import path from "path";
3
2
  import { Token, parse } from "path-to-regexp";
4
3
 
5
4
  export namespace PathAnalyzer {
5
+ /**
6
+ * The route paths joined as the router joins them: one `/` between them, and
7
+ * none doubled. A route path is router syntax, never a file path, so a
8
+ * backslash stays the escape of the character after it (`items\\:batchGet`).
9
+ */
6
10
  export const join = (...args: string[]) =>
7
11
  "/" +
8
- _Trim(
9
- path
10
- .join(...args.filter((s) => !!s.length))
11
- .split("\\")
12
- .join("/"),
13
- );
12
+ args
13
+ .join("/")
14
+ .split("/")
15
+ .filter((str) => str.length !== 0)
16
+ .join("/");
14
17
 
15
18
  export const joinWithGlobalPrefix = (props: {
16
19
  globalPrefix: string;
@@ -41,20 +44,78 @@ export namespace PathAnalyzer {
41
44
  );
42
45
  };
43
46
 
47
+ /**
48
+ * Whether a route path holds a wildcard: a `*` not escaped as the literal
49
+ * character (`\\*`), which a route path may hold since #1713.
50
+ */
51
+ export const wildcard = (str: string): boolean => /(^|[^\\])\*/.test(str);
52
+
53
+ /**
54
+ * The literal text and parameters of a path, in order, as path-to-regexp
55
+ * reads it: `/files/:id.json` is `/files/`, the parameter `id`, then `.json`,
56
+ * and `/range/:from-:to` holds two parameters parted by `-`. Every generator
57
+ * that writes a path with its parameters filled in reads it from here, never
58
+ * by splitting the text at `:` or `/`. `null` for a path it cannot parse.
59
+ */
60
+ export const segments = (str: string): ISegment[] | null => {
61
+ const tokens: Token[] | null = _Tokenize(str);
62
+ if (tokens === null) return null;
63
+ const output: ISegment[] = [];
64
+ const literal = (value: string): void => {
65
+ if (value.length === 0) return;
66
+ const last: ISegment | undefined = output[output.length - 1];
67
+ if (last?.type === "literal") last.value += value;
68
+ else output.push({ type: "literal", value });
69
+ };
70
+ for (const token of tokens)
71
+ if (typeof token === "string") literal(token);
72
+ else if (typeof token.name === "number") return null;
73
+ else {
74
+ literal(token.prefix);
75
+ output.push({ type: "param", name: token.name });
76
+ literal(token.suffix);
77
+ }
78
+ return output;
79
+ };
80
+
81
+ /**
82
+ * The path in OpenAPI's template syntax, each parameter written `{name}`:
83
+ * `/files/:id.json` is `/files/{id}.json`.
84
+ */
85
+ export const toOpenApi = (str: string): string => {
86
+ const list: ISegment[] | null = segments(str);
87
+ return list === null
88
+ ? str
89
+ : list
90
+ .map((s) => (s.type === "literal" ? s.value : `{${s.name}}`))
91
+ .join("");
92
+ };
93
+
94
+ export type ISegment =
95
+ | { type: "literal"; value: string }
96
+ | { type: "param"; name: string };
97
+
44
98
  export const parameters = (str: string): string[] | null => {
45
99
  const args = _Parse(str);
46
100
  if (args === null) return null;
47
101
  return args.filter((arg) => arg.type === "param").map((arg) => arg.value);
48
102
  };
49
103
 
104
+ /**
105
+ * The route's tokens, its literal text unescaped. Both routers' spellings of
106
+ * a literal colon are read: path-to-regexp's (Express) `\\:`, and
107
+ * find-my-way's (Fastify) `::`, which path-to-regexp would reject.
108
+ */
109
+ function _Tokenize(str: string): Token[] | null {
110
+ try {
111
+ return parse(fromFastifyColons(join(str)));
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
50
117
  function _Parse(str: string): IArgument[] | null {
51
- const tokens: Token[] | null = (() => {
52
- try {
53
- return parse(path.join(str).split("\\").join("/"));
54
- } catch {
55
- return null;
56
- }
57
- })();
118
+ const tokens: Token[] | null = _Tokenize(str);
58
119
  if (tokens === null) return null;
59
120
 
60
121
  const output: IArgument[] = [];
@@ -128,3 +189,34 @@ const REQUEST_METHODS: Record<string, RequestMethod> = {
128
189
  POST: RequestMethod.POST,
129
190
  PUT: RequestMethod.PUT,
130
191
  };
192
+
193
+ /**
194
+ * A route with find-my-way's (Fastify) literal colon `::` spelled as
195
+ * path-to-regexp's `\:`. find-my-way reads `::` as a colon in static text
196
+ * alone: after a parameter it is part of the parameter's name, so it is left as
197
+ * is there, and path-to-regexp then rejects the route, as no parameter of that
198
+ * name is what the handler reads.
199
+ *
200
+ * @internal
201
+ */
202
+ const fromFastifyColons = (route: string): string => {
203
+ let output: string = "";
204
+ for (let i: number = 0; i < route.length; ) {
205
+ if (route[i] === "\\") {
206
+ output += route.slice(i, i + 2);
207
+ i += 2;
208
+ } else if (route.startsWith("::", i)) {
209
+ output += "\\:";
210
+ i += 2;
211
+ } else if (route[i] === ":") {
212
+ const name: string = /^:[A-Za-z0-9_$]*/.exec(route.slice(i))![0];
213
+ output += name;
214
+ i += name.length;
215
+ if (route.startsWith("::", i)) {
216
+ output += "::";
217
+ i += 2;
218
+ }
219
+ } else output += route[i++];
220
+ }
221
+ return output;
222
+ };
@@ -12,6 +12,7 @@ import { IReflectHttpOperation } from "../structures/IReflectHttpOperation";
12
12
  import { IReflectMcpOperation } from "../structures/IReflectMcpOperation";
13
13
  import { IReflectWebSocketOperation } from "../structures/IReflectWebSocketOperation";
14
14
  import { ArrayUtil } from "../utils/ArrayUtil";
15
+ import { PathAnalyzer } from "./PathAnalyzer";
15
16
  import { ReflectHttpOperationAnalyzer } from "./ReflectHttpOperationAnalyzer";
16
17
  import { ReflectMcpOperationAnalyzer } from "./ReflectMcpOperationAnalyzer";
17
18
  import { ReflectMetadataAnalyzer } from "./ReflectMetadataAnalyzer";
@@ -46,7 +47,7 @@ export namespace ReflectControllerAnalyzer {
46
47
  prefixes: props.controller.prefixes,
47
48
  paths: ReflectMetadataAnalyzer.paths(props.controller.class).filter(
48
49
  (str) => {
49
- if (str.includes("*") === true) {
50
+ if (PathAnalyzer.wildcard(str)) {
50
51
  props.project.warnings.push({
51
52
  file: props.controller.location,
52
53
  class: props.controller.class.name,
@@ -77,7 +77,7 @@ export namespace ReflectHttpOperationAnalyzer {
77
77
  name: props.name,
78
78
  method: method === "ALL" ? "POST" : method,
79
79
  paths: ReflectMetadataAnalyzer.paths(props.function).filter((str) => {
80
- if (str.includes("*") === true) {
80
+ if (PathAnalyzer.wildcard(str)) {
81
81
  props.project.warnings.push({
82
82
  file: props.controller.file,
83
83
  class: props.controller.class.name,
@@ -117,8 +117,8 @@ export namespace ReflectHttpOperationAnalyzer {
117
117
  jsDocTags: props.metadata.jsDocTags,
118
118
  operationId: props.metadata.jsDocTags
119
119
  .find(({ name }) => name === "operationId")
120
- ?.text?.[0]?.text.split(" ")[0]
121
- ?.trim(),
120
+ ?.text?.[0]?.text.trim()
121
+ .split(/\s+/)[0],
122
122
  extensions: ReflectMetadataAnalyzer.extensions(props.function),
123
123
  };
124
124
 
@@ -130,7 +130,7 @@ export namespace ReflectHttpOperationAnalyzer {
130
130
  controllerLocation,
131
131
  metaLocation,
132
132
  );
133
- if (location.includes("*")) continue;
133
+ if (PathAnalyzer.wildcard(location)) continue;
134
134
 
135
135
  // LIST UP PARAMETERS
136
136
  const binded: string[] | null = PathAnalyzer.parameters(location);