@nestia/sdk 13.0.4 → 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 +8 -8
  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
@@ -1,4 +1,4 @@
1
- import { type Expression, factory } from "@ttsc/factory";
1
+ import { type Expression, SyntaxKind, factory } from "@ttsc/factory";
2
2
 
3
3
  import { ExpressionFactory } from "./ExpressionFactory";
4
4
  import { IdentifierFactory } from "./IdentifierFactory";
@@ -30,7 +30,12 @@ export namespace LiteralFactory {
30
30
  if (typeof input === "number") return ExpressionFactory.number(input);
31
31
  if (typeof input === "string") return factory.createStringLiteral(input);
32
32
  if (typeof input === "bigint")
33
- return factory.createStringLiteral(input.toString());
33
+ return input < BigInt(0)
34
+ ? factory.createPrefixUnaryExpression(
35
+ SyntaxKind.MinusToken,
36
+ factory.createBigIntLiteral((-input).toString()),
37
+ )
38
+ : factory.createBigIntLiteral(input.toString());
34
39
  if (typeof input === "function")
35
40
  return factory.createIdentifier("undefined");
36
41
  throw new TypeError("LiteralFactory.write: unsupported input type.");
@@ -118,7 +118,7 @@ export namespace SdkGenerator {
118
118
  props.errors.push({
119
119
  file: props.route.controller.file,
120
120
  class: props.route.controller.class.name,
121
- function: props.route.name,
121
+ function: props.route.key,
122
122
  from: `parameter ${JSON.stringify(p.name)}`,
123
123
  contents: [`implicit (unnamed) parameter type.`],
124
124
  });
@@ -129,7 +129,7 @@ export namespace SdkGenerator {
129
129
  props.errors.push({
130
130
  file: props.route.controller.file,
131
131
  class: props.route.controller.class.name,
132
- function: props.route.name,
132
+ function: props.route.key,
133
133
  from: `exception ${JSON.stringify(key)}`,
134
134
  contents: [`implicit (unnamed) exception type.`],
135
135
  });
@@ -140,7 +140,7 @@ export namespace SdkGenerator {
140
140
  props.errors.push({
141
141
  file: props.route.controller.file,
142
142
  class: props.route.controller.class.name,
143
- function: props.route.name,
143
+ function: props.route.key,
144
144
  from: "success",
145
145
  contents: [`implicit (unnamed) return type.`],
146
146
  });
@@ -13,6 +13,7 @@ import { Singleton } from "tstl";
13
13
  import type { IJsonSchemaCollection } from "typia";
14
14
 
15
15
  import { INestiaConfig } from "../INestiaConfig";
16
+ import { PathAnalyzer } from "../analyses/PathAnalyzer";
16
17
  import {
17
18
  JsonSchemasProgrammer,
18
19
  MetadataSchema,
@@ -211,7 +212,8 @@ export namespace SwaggerGenerator {
211
212
  schemas: {},
212
213
  securitySchemes: config.security,
213
214
  },
214
- tags: config.tags ?? [],
215
+ // a copy, because composing pushes each route's tags into this list
216
+ tags: clone(config.tags ?? []),
215
217
  "x-typia-emended-v12": true,
216
218
  };
217
219
  };
@@ -238,7 +240,12 @@ export namespace SwaggerGenerator {
238
240
  routes: ITypedHttpRoute[];
239
241
  }): void => {
240
242
  // SWAGGER CUSTOMIZER
241
- const customizers: Array<() => void> = [];
243
+ const customizers: Array<{
244
+ route: ITypedHttpRoute;
245
+ method: OpenApi.Method;
246
+ path: string;
247
+ closures: Function[];
248
+ }> = [];
242
249
  const neighbor = {
243
250
  at: new Singleton(() => {
244
251
  const functor: Map<Function, Endpoint> = new Map();
@@ -262,63 +269,197 @@ export namespace SwaggerGenerator {
262
269
  (key: Accessor): OpenApi.IOperation | undefined => {
263
270
  const method: OpenApi.Method =
264
271
  key.method.toLowerCase() as OpenApi.Method;
265
- const path: string =
272
+ const normalized: string =
266
273
  "/" +
267
274
  key.path
268
275
  .split("/")
269
276
  .filter((str) => !!str.length)
270
- .map((str) =>
271
- str.startsWith(":") ? `{${str.substring(1)}}` : str,
272
- )
273
277
  .join("/");
278
+ // a route path, `:id`, as the router reads it; an accessor already
279
+ // in OpenAPI form, `{id}`, is taken as is
280
+ const path: string = normalized.includes(":")
281
+ ? PathAnalyzer.toOpenApi(normalized)
282
+ : normalized;
274
283
  return props.document.paths?.[path]?.[method];
275
284
  },
276
285
  ),
277
286
  };
278
287
 
279
288
  // COMPOSE OPERATIONS
289
+ const violations: string[] = [];
280
290
  for (const r of props.routes) {
281
- const operation: OpenApi.IOperation = SwaggerOperationComposer.compose({
282
- ...props,
283
- route: r,
284
- });
291
+ const method: OpenApi.Method = r.method.toLowerCase() as OpenApi.Method;
285
292
  const path: string = getPath(r);
286
293
  props.document.paths ??= {};
287
294
  props.document.paths[path] ??= {};
288
- props.document.paths[path][r.method.toLowerCase() as "get"] = operation;
295
+ props.document.paths[path][method] = SwaggerOperationComposer.compose({
296
+ ...props,
297
+ route: r,
298
+ });
299
+ violations.push(
300
+ ...validateSecurity({
301
+ config: props.config,
302
+ route: r,
303
+ security: props.document.paths[path][method]!.security,
304
+ }),
305
+ );
289
306
 
290
307
  const closure: Function | Function[] | undefined = Reflect.getMetadata(
291
308
  "nestia/SwaggerCustomizer",
292
309
  r.controller.class.prototype,
293
- r.name,
310
+ r.key,
294
311
  );
295
- if (closure !== undefined) {
296
- const array: Function[] = Array.isArray(closure) ? closure : [closure];
297
- customizers.push(() => {
298
- for (const closure of array)
299
- closure({
300
- swagger: props.document,
301
- method: r.method,
302
- path,
303
- route: operation,
304
- at: (func: Function) => neighbor.at.get().get(func),
305
- get: (accessor: Accessor) => neighbor.get.get()(accessor),
306
- } satisfies SwaggerCustomizer.IProps);
312
+ if (closure !== undefined)
313
+ customizers.push({
314
+ route: r,
315
+ method,
316
+ path,
317
+ closures: Array.isArray(closure) ? closure : [closure],
307
318
  });
308
- }
309
319
  }
310
320
 
311
- // DO CUSTOMIZE
312
- for (const fn of customizers) fn();
321
+ if (violations.length !== 0)
322
+ throw new Error(
323
+ [
324
+ `Error on Swagger composition: invalid security requirements. Declare every scheme a route names, with its scopes, in the "security" of the Swagger configuration: "swagger.security" of "nestia.config.ts", or the configuration given to NestiaSwaggerComposer.document().`,
325
+ "",
326
+ "List of violations:",
327
+ ...violations,
328
+ ].join("\n"),
329
+ );
330
+
331
+ // DETACH, then DO CUSTOMIZE
332
+ detach(props.document);
333
+ // Each customizer receives the operation composed for its own route, found
334
+ // before any customizer runs, since one may move or delete paths.
335
+ const operations: OpenApi.IOperation[] = customizers.map(
336
+ (c) => props.document.paths![c.path]![c.method]!,
337
+ );
338
+ customizers.forEach((c, i) => {
339
+ for (const closure of c.closures)
340
+ closure({
341
+ swagger: props.document,
342
+ method: c.route.method,
343
+ path: c.path,
344
+ route: operations[i]!,
345
+ at: (func: Function) => neighbor.at.get().get(func),
346
+ get: (accessor: Accessor) => neighbor.get.get()(accessor),
347
+ } satisfies SwaggerCustomizer.IProps);
348
+ });
349
+ };
350
+
351
+ /**
352
+ * Replaces every member of the composed document with a copy, in place.
353
+ *
354
+ * Operations still hold values nestia does not own by reference: decorator
355
+ * examples, `@ApiExtension` values, security requirements, and the configured
356
+ * servers, security schemes, and document info. They outlive the document,
357
+ * since route metadata and the configuration serve every composition in the
358
+ * process, so a `SwaggerCustomizer` or a caller editing one document edited
359
+ * all of them, and the next composition started from the edit. It runs before
360
+ * the customizers, which may then edit freely.
361
+ */
362
+ const detach = (document: OpenApi.IDocument): void => {
363
+ const copy: OpenApi.IDocument = clone(document);
364
+ for (const key of Object.keys(document))
365
+ delete (document as unknown as Record<string, unknown>)[key];
366
+ Object.assign(document, copy);
367
+ };
368
+
369
+ /**
370
+ * Copies the arrays and plain objects `value` is built of, keeping every
371
+ * other value as it is.
372
+ *
373
+ * A document is edited as the tree its JSON is, so every reference to an
374
+ * array or plain object gets its own copy, except a reference back to an
375
+ * enclosing one, which stays circular. Values are not converted: a JSON copy
376
+ * would throw on a bigint example or turn a `Date` into a string before the
377
+ * customizer or caller that handles it runs.
378
+ */
379
+ const clone = <T>(
380
+ value: T,
381
+ ancestors: Map<object, object> = new Map(),
382
+ ): T => {
383
+ if (typeof value !== "object" || value === null) return value;
384
+ const prototype: unknown = Object.getPrototypeOf(value);
385
+ const array: boolean = Array.isArray(value);
386
+ if (!array && prototype !== Object.prototype && prototype !== null)
387
+ return value;
388
+ const circular: object | undefined = ancestors.get(value);
389
+ if (circular !== undefined) return circular as T;
390
+ const output: Record<string, unknown> = (
391
+ array ? new Array((value as unknown[]).length) : {}
392
+ ) as Record<string, unknown>;
393
+ ancestors.set(value, output);
394
+ for (const key of Object.keys(value))
395
+ Object.defineProperty(output, key, {
396
+ value: clone((value as Record<string, unknown>)[key], ancestors),
397
+ writable: true,
398
+ enumerable: true,
399
+ configurable: true,
400
+ });
401
+ ancestors.delete(value);
402
+ return output as T;
313
403
  };
314
404
 
315
- const getPath = (route: ITypedHttpRoute): string => {
316
- let str: string = route.path;
317
- for (const param of route.pathParameters)
318
- str = str.replace(`:${param.field}`, `{${param.field}}`);
319
- return str;
405
+ /**
406
+ * The violations of an operation's security requirements against the
407
+ * configured `swagger.security` schemes: a scheme it never declares, an
408
+ * OAuth2 scope no flow of the scheme declares, and, for an OpenAPI 3.0 or
409
+ * Swagger 2.0 document, scopes on a scheme other than OAuth2 or OpenID
410
+ * Connect, which those versions require to be empty. From 3.1 on, such a
411
+ * scheme's array lists the role names the operation requires. An OpenID
412
+ * Connect scheme lists no scopes of its own, so its scopes are not checked.
413
+ */
414
+ const validateSecurity = (props: {
415
+ config: Omit<INestiaConfig.ISwaggerConfig, "output">;
416
+ route: ITypedHttpRoute;
417
+ security: Record<string, string[]>[] | undefined;
418
+ }): string[] => {
419
+ const violations: string[] = [];
420
+ const report = (message: string): void => {
421
+ violations.push(
422
+ ` - ${message} (${props.route.controller.class.name}.${props.route.key}() at "${props.route.method} ${props.route.path}")`,
423
+ );
424
+ };
425
+ for (const requirement of props.security ?? [])
426
+ for (const [name, scopes] of Object.entries(requirement)) {
427
+ const scheme: OpenApi.ISecurityScheme | undefined =
428
+ props.config.security?.[name];
429
+ if (scheme === undefined)
430
+ report(`target security scheme "${name}" does not exist.`);
431
+ else if (scopes.length === 0 || scheme.type === "openIdConnect")
432
+ continue;
433
+ else if (scheme.type !== "oauth2") {
434
+ const version: string = props.config.openapi ?? "3.2";
435
+ if (version === "3.0" || version === "2.0")
436
+ report(
437
+ `target security scheme "${name}" is neither "oauth2" nor "openIdConnect" type, but you've configured the scopes, which OpenAPI ${version} requires to be empty.`,
438
+ );
439
+ } else {
440
+ const declared: Set<string> = new Set(
441
+ Object.values(scheme.flows ?? {}).flatMap((flow) =>
442
+ Object.keys(
443
+ (flow as { scopes?: Record<string, string> } | undefined)
444
+ ?.scopes ?? {},
445
+ ),
446
+ ),
447
+ );
448
+ for (const scope of scopes)
449
+ if (declared.has(scope) === false)
450
+ report(
451
+ `target security scheme "${name}" does not have a specific scope "${scope}".`,
452
+ );
453
+ }
454
+ }
455
+ return violations;
320
456
  };
321
457
 
458
+ // each parameter where the router reads it; a replacement by text would
459
+ // turn `:identity` into `{id}entity` for a path also holding `:id`
460
+ const getPath = (route: ITypedHttpRoute): string =>
461
+ PathAnalyzer.toOpenApi(route.path);
462
+
322
463
  const isSwaggerExcluded = (route: ITypedHttpRoute): boolean => {
323
464
  const controller: unknown = Reflect.getMetadata(
324
465
  "swagger/apiExcludeController",
@@ -31,6 +31,34 @@ export namespace FilePrinter {
31
31
  );
32
32
  };
33
33
 
34
+ /**
35
+ * Writes a JSDoc tag so TypeScript reads its text back as it is.
36
+ *
37
+ * TypeScript takes a continued line's margin off up to the column the text
38
+ * began at: past `@<head> ` when the text starts on the tag's line, or the
39
+ * tag's own column when it starts on the next one. So a text whose first line
40
+ * is indented, such as an `@example`'s code, starts on the next line, and any
41
+ * other text continues at its first line's column. Either way every line
42
+ * keeps the indentation it has beyond the margin.
43
+ *
44
+ * @param head The tag's name, followed by a `@param` tag's parameter name
45
+ * @param text The tag's text
46
+ */
47
+ export const jsDocTag = (head: string, text: string): string => {
48
+ const lines: string[] = text.split("\n").map((line) => line.trimEnd());
49
+ while (lines.length !== 0 && lines[0] === "") lines.shift();
50
+ while (lines.length !== 0 && lines.at(-1) === "") lines.pop();
51
+ if (lines.length === 0) return `@${head}`;
52
+ else if (/^\s/.test(lines[0]!)) return [`@${head}`, ...lines].join("\n");
53
+ const margin: string = " ".repeat(head.length + 2);
54
+ return [
55
+ `@${head} ${lines[0]}`,
56
+ ...lines
57
+ .slice(1)
58
+ .map((line) => (line.length ? `${margin}${line}` : line)),
59
+ ].join("\n");
60
+ };
61
+
34
62
  export const enter = () =>
35
63
  factory.createExpressionStatement(factory.createIdentifier("\n"));
36
64
 
@@ -59,6 +59,17 @@ export class ImportDictionary {
59
59
  }
60
60
  }
61
61
 
62
+ /** Every identifier the imports bind in the file. */
63
+ public locals(): string[] {
64
+ const output: string[] = [];
65
+ for (const { second: c } of this.components_) {
66
+ if (c.asterisk !== null) output.push(c.asterisk);
67
+ if (c.default !== null) output.push(c.default);
68
+ for (const { first: local } of c.elements) output.push(local);
69
+ }
70
+ return output;
71
+ }
72
+
62
73
  public external(props: ImportDictionary.IProps): string {
63
74
  const file: string = `node_modules/${props.file}`;
64
75
  return this.internal({
@@ -16,6 +16,7 @@ import { FilePrinter } from "./FilePrinter";
16
16
  import { ImportDictionary } from "./ImportDictionary";
17
17
  import { SdkHttpParameterProgrammer } from "./SdkHttpParameterProgrammer";
18
18
  import { SdkTypeProgrammer } from "./SdkTypeProgrammer";
19
+ import type { SdkWebSocketParameterProgrammer } from "./SdkWebSocketParameterProgrammer";
19
20
 
20
21
  export namespace SdkAliasCollection {
21
22
  export const name = ({ type }: { type: IReflectType }): TypeNode =>
@@ -82,7 +83,10 @@ export namespace SdkAliasCollection {
82
83
  .flat(),
83
84
  );
84
85
 
85
- export const websocketProps = (route: ITypedWebSocketRoute): TypeNode =>
86
+ export const websocketProps = (
87
+ route: ITypedWebSocketRoute,
88
+ names: SdkWebSocketParameterProgrammer.INames,
89
+ ): TypeNode =>
86
90
  factory.createTypeLiteralNode([
87
91
  ...route.pathParameters.map((p) =>
88
92
  factory.createPropertySignature(
@@ -96,7 +100,7 @@ export namespace SdkAliasCollection {
96
100
  ? [
97
101
  factory.createPropertySignature(
98
102
  undefined,
99
- "query",
103
+ names.query,
100
104
  undefined,
101
105
  factory.createTypeReferenceNode("Query"),
102
106
  ),
@@ -104,7 +108,7 @@ export namespace SdkAliasCollection {
104
108
  : []),
105
109
  factory.createPropertySignature(
106
110
  undefined,
107
- "provider",
111
+ names.provider,
108
112
  undefined,
109
113
  factory.createTypeReferenceNode("Provider"),
110
114
  ),
@@ -233,7 +237,10 @@ export namespace SdkAliasCollection {
233
237
  branches.map((b) =>
234
238
  factory.createPropertySignature(
235
239
  undefined,
236
- factory.createNumericLiteral(b.status),
240
+ // a status range like "4XX" is no numeric literal
241
+ /^\d+$/.test(b.status)
242
+ ? factory.createNumericLiteral(b.status)
243
+ : factory.createStringLiteral(b.status),
237
244
  undefined,
238
245
  b.type,
239
246
  ),
@@ -29,17 +29,34 @@ export namespace SdkDistributionComposer {
29
29
  await replace({ root, output })(file);
30
30
 
31
31
  // INSTALL PACKAGES
32
+ //
33
+ // The package compiles with ttsc, which applies the typia transform the
34
+ // SDK's `assert` and `simulate` code needs through typia's own plugin;
35
+ // typia removed its `setup` command in 14.0.0.
32
36
  const v: IDependencies = await dependencies({
37
+ root,
33
38
  websocket: props.websocket,
34
39
  });
35
- execute("npm install --save-dev rimraf");
36
- execute(`npm install --save @nestia/fetcher@${v.version}`);
37
- execute(`npm install --save typia@${v.typia}`);
38
- if (props.mcp && v.mcp !== undefined)
39
- execute(`npm install --save @modelcontextprotocol/sdk@${v.mcp}`);
40
- if (props.websocket && v.tgrid !== undefined)
41
- execute(`npm install --save tgrid@${v.tgrid}`);
42
- execute("npx typia setup --manager npm");
40
+ // one install per dependency kind: each npm run resolves and reifies the
41
+ // whole tree again, and the runtime set lands together or not at all, so
42
+ // an interrupted setup never leaves @nestia/fetcher, which marks the
43
+ // package configured, without its peers
44
+ execute(
45
+ `npm install --save-dev rimraf ttsc@${v.ttsc} typescript@${v.typescript}`,
46
+ );
47
+ execute(
48
+ [
49
+ "npm install --save",
50
+ `@nestia/fetcher@${v.version}`,
51
+ `typia@${v.typia}`,
52
+ ...(props.mcp && v.mcp !== undefined
53
+ ? [`@modelcontextprotocol/sdk@${v.mcp}`]
54
+ : []),
55
+ ...(props.websocket && v.tgrid !== undefined
56
+ ? [`tgrid@${v.tgrid}`]
57
+ : []),
58
+ ].join(" "),
59
+ );
43
60
  } finally {
44
61
  process.chdir(root);
45
62
  }
@@ -82,7 +99,60 @@ export namespace SdkDistributionComposer {
82
99
  );
83
100
  };
84
101
 
102
+ /**
103
+ * The version of a package the project installed, exactly: the staged package
104
+ * must run the typia, and compile with the ttsc and TypeScript, the project
105
+ * itself uses, with the tgrid and MCP SDK its WebSocket and MCP functions
106
+ * import. `@nestia/sdk`'s own specifier is only the fallback, and a
107
+ * workspace's `catalog:` one names no version npm could install.
108
+ */
109
+ const installed = (
110
+ root: string,
111
+ name: string,
112
+ fallback: string | undefined,
113
+ ): string | undefined =>
114
+ manifestVersion(root, name) ??
115
+ (fallback !== undefined && fallback.startsWith("catalog:") === false
116
+ ? fallback
117
+ : undefined);
118
+
119
+ /**
120
+ * The version in the manifest of the package as installed. Not every package
121
+ * exports its `package.json`, as tgrid does not, and a subpath export may map
122
+ * it to a nested manifest of no name, as `@modelcontextprotocol/sdk` maps it
123
+ * to `dist/cjs/package.json`; so the manifest is the nearest `package.json`
124
+ * naming the package, at or above what the request resolves to.
125
+ */
126
+ const manifestVersion = (root: string, name: string): string | undefined => {
127
+ for (const request of [`${name}/package.json`, name]) {
128
+ let file: string;
129
+ try {
130
+ file = require.resolve(request, { paths: [root, __dirname] });
131
+ } catch {
132
+ continue;
133
+ }
134
+ for (let directory: string = path.dirname(file); ; ) {
135
+ try {
136
+ const json: { name?: unknown; version?: unknown } = JSON.parse(
137
+ fs.readFileSync(path.join(directory, "package.json"), "utf8"),
138
+ );
139
+ if (
140
+ json.name === name &&
141
+ typeof json.version === "string" &&
142
+ json.version.length !== 0
143
+ )
144
+ return json.version;
145
+ } catch {}
146
+ const parent: string = path.dirname(directory);
147
+ if (parent === directory) break;
148
+ directory = parent;
149
+ }
150
+ }
151
+ return undefined;
152
+ };
153
+
85
154
  const dependencies = async (opts: {
155
+ root: string;
86
156
  websocket: boolean;
87
157
  }): Promise<IDependencies> => {
88
158
  const content: string = await fs.promises.readFile(
@@ -98,20 +168,22 @@ export namespace SdkDistributionComposer {
98
168
  ...json.devDependencies,
99
169
  ...json.dependencies,
100
170
  };
101
- const required = (key: "version" | "typia"): string => {
102
- const value: string | undefined =
103
- key === "version" ? json.version : dependencies[key];
171
+ const required = (key: string, value: string | undefined): string => {
104
172
  if (typeof value !== "string" || value.length === 0)
105
173
  throw new Error(
106
174
  `Unable to resolve ${key} version for SDK distribution.`,
107
175
  );
108
176
  return value;
109
177
  };
178
+ const version = (name: string): string | undefined =>
179
+ installed(opts.root, name, dependencies[name]);
110
180
  return {
111
- version: required("version"),
112
- typia: required("typia"),
113
- tgrid: opts.websocket ? dependencies.tgrid : undefined,
114
- mcp: dependencies["@modelcontextprotocol/sdk"],
181
+ version: required("@nestia/fetcher", json.version),
182
+ typia: required("typia", version("typia")),
183
+ ttsc: required("ttsc", version("ttsc")),
184
+ typescript: required("typescript", version("typescript")),
185
+ tgrid: opts.websocket ? version("tgrid") : undefined,
186
+ mcp: version("@modelcontextprotocol/sdk"),
115
187
  };
116
188
  };
117
189
  }
@@ -119,6 +191,8 @@ export namespace SdkDistributionComposer {
119
191
  interface IDependencies {
120
192
  version: string;
121
193
  typia: string;
194
+ ttsc: string;
195
+ typescript: string;
122
196
  tgrid: string | undefined;
123
197
  mcp: string | undefined;
124
198
  }