@nestia/migrate 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 (65) hide show
  1. package/README.md +1 -1
  2. package/lib/analyzers/NestiaMigrateControllerAnalyzer.d.ts +13 -0
  3. package/lib/analyzers/NestiaMigrateControllerAnalyzer.js +32 -2
  4. package/lib/analyzers/NestiaMigrateControllerAnalyzer.js.map +1 -1
  5. package/lib/analyzers/NestiaMigrateControllerAnalyzer.mjs +14 -2
  6. package/lib/bundles/NEST_TEMPLATE.js +1 -1
  7. package/lib/bundles/NEST_TEMPLATE.js.map +1 -1
  8. package/lib/bundles/NEST_TEMPLATE.mjs +1 -1
  9. package/lib/bundles/SDK_TEMPLATE.js +1 -1
  10. package/lib/bundles/SDK_TEMPLATE.js.map +1 -1
  11. package/lib/bundles/SDK_TEMPLATE.mjs +1 -1
  12. package/lib/executable/NestiaMigrateCommander.js +5 -1
  13. package/lib/executable/NestiaMigrateCommander.js.map +1 -1
  14. package/lib/executable/NestiaMigrateCommander.mjs +1 -1
  15. package/lib/executable/NestiaMigrateInquirer.js +7 -25
  16. package/lib/executable/NestiaMigrateInquirer.js.map +1 -1
  17. package/lib/executable/NestiaMigrateInquirer.mjs +5 -7
  18. package/lib/programmers/NestiaMigrateApiFunctionProgrammer.js +7 -5
  19. package/lib/programmers/NestiaMigrateApiFunctionProgrammer.js.map +1 -1
  20. package/lib/programmers/NestiaMigrateApiFunctionProgrammer.mjs +4 -2
  21. package/lib/programmers/NestiaMigrateApiNamespaceProgrammer.js +31 -13
  22. package/lib/programmers/NestiaMigrateApiNamespaceProgrammer.js.map +1 -1
  23. package/lib/programmers/NestiaMigrateApiNamespaceProgrammer.mjs +17 -8
  24. package/lib/programmers/NestiaMigrateApiSimulationProgrammer.js +24 -22
  25. package/lib/programmers/NestiaMigrateApiSimulationProgrammer.js.map +1 -1
  26. package/lib/programmers/NestiaMigrateApiSimulationProgrammer.mjs +13 -20
  27. package/lib/programmers/NestiaMigrateE2eFileProgrammer.js +20 -5
  28. package/lib/programmers/NestiaMigrateE2eFileProgrammer.js.map +1 -1
  29. package/lib/programmers/NestiaMigrateE2eFileProgrammer.mjs +6 -2
  30. package/lib/programmers/NestiaMigrateNestMethodProgrammer.js +69 -13
  31. package/lib/programmers/NestiaMigrateNestMethodProgrammer.js.map +1 -1
  32. package/lib/programmers/NestiaMigrateNestMethodProgrammer.mjs +46 -9
  33. package/lib/programmers/NestiaMigrateSchemaProgrammer.js +25 -4
  34. package/lib/programmers/NestiaMigrateSchemaProgrammer.js.map +1 -1
  35. package/lib/programmers/NestiaMigrateSchemaProgrammer.mjs +15 -4
  36. package/lib/utils/PathTemplate.d.ts +19 -0
  37. package/lib/utils/PathTemplate.js +42 -0
  38. package/lib/utils/PathTemplate.js.map +1 -0
  39. package/lib/utils/PathTemplate.mjs +34 -0
  40. package/lib/utils/PathTemplate.mjs.map +1 -0
  41. package/lib/utils/RequestBody.d.ts +9 -0
  42. package/lib/utils/RequestBody.js +16 -0
  43. package/lib/utils/RequestBody.js.map +1 -0
  44. package/lib/utils/RequestBody.mjs +9 -0
  45. package/lib/utils/RequestBody.mjs.map +1 -0
  46. package/lib/utils/SuccessStatus.d.ts +14 -0
  47. package/lib/utils/SuccessStatus.js +28 -0
  48. package/lib/utils/SuccessStatus.js.map +1 -0
  49. package/lib/utils/SuccessStatus.mjs +14 -0
  50. package/lib/utils/SuccessStatus.mjs.map +1 -0
  51. package/package.json +4 -4
  52. package/src/analyzers/NestiaMigrateControllerAnalyzer.ts +39 -2
  53. package/src/bundles/NEST_TEMPLATE.ts +1 -1
  54. package/src/bundles/SDK_TEMPLATE.ts +1 -1
  55. package/src/executable/NestiaMigrateCommander.ts +7 -1
  56. package/src/executable/NestiaMigrateInquirer.ts +12 -26
  57. package/src/programmers/NestiaMigrateApiFunctionProgrammer.ts +10 -4
  58. package/src/programmers/NestiaMigrateApiNamespaceProgrammer.ts +41 -20
  59. package/src/programmers/NestiaMigrateApiSimulationProgrammer.ts +23 -73
  60. package/src/programmers/NestiaMigrateE2eFileProgrammer.ts +41 -8
  61. package/src/programmers/NestiaMigrateNestMethodProgrammer.ts +88 -29
  62. package/src/programmers/NestiaMigrateSchemaProgrammer.ts +28 -10
  63. package/src/utils/PathTemplate.ts +39 -0
  64. package/src/utils/RequestBody.ts +14 -0
  65. package/src/utils/SuccessStatus.ts +29 -0
@@ -5,7 +5,7 @@ export const SDK_TEMPLATE: Record<string, string> = {
5
5
  "LICENSE": "MIT License\n\nCopyright (c) 2024 Jeongho Nam\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
6
6
  "README.md": "# Software Development Kit\nThis is a SDK library generated by [`@nestia/migrate`](https://nestia.io/docs/migrate) or [`@nestia/editor`](https://nestia.io/docs/editor).\n\nWith this SDK library, you can easily and safely interact with backend server.\n\nJust import and call some API functions like gif image below:\n\n![nestia-sdk-demo](https://user-images.githubusercontent.com/13158709/215004990-368c589d-7101-404e-b81b-fbc936382f05.gif)\n\n> Left is server code, and right is client code utilizing the SDK\n\n\n\n\n## How to Test\n```bash\npnpm install\npnpm run start # run only \"test/start.ts\" file\npnpm run test # everything under the \"test/features\" directory\npnpm run test:simulate # \"test/features\" with mockup simulation mode\n```\n\nIf you run `pnpm run start` command, only [test/start.ts](test/start.ts) file would be executed.\n\nOtherwise you run `pnpm run test` command instead, run everything in the [test/features](test/features) directory.\n\nFor reference, the [test/features](test/features) directory and E2E test functions (for each API endpoints) would be automatically composed only when you've configured the \"E2E test function generation mode\" of the `@nestia/migrate` (or `@nestia/editor`).\n\n```bash\npnpm dlx @nestia/migrate\n? Migration mode (Use arrow keys):\n NestJS\n > SDK\n? Swagger file location: assets/input/clickhouse.json\n? Output directory path: assets/output/clickhouse-sdk-manual\n? Mokup Simulator: true\n? E2E Test Functions: true\n```\n\n\n\n\n## Deploy\n```bash\npnpm install\npnpm run deploy\n```\n\nJust run `pnpm run deploy` command, then your SDK library would be published.\n\nBy the way, the initial package name of this template repository is `@ORGANIZATION/PROJECT-api`. I think it would better to change the word to your own organization and project name. If you're utilizing `VsCode`, you can do it through `Edit > Replace in Files` (*Ctrl + Shift + H*) feature.\n\n-----------\n\n> ## What [`Nestia`](https://nestia.io) is:\n> ![Nestia Logo](https://nestia.io/logo.png)\n> \n> [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE)\n> [![npm version](https://img.shields.io/npm/v/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core)\n> [![Downloads](https://img.shields.io/npm/dm/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core)\n> [![Build Status](https://github.com/samchon/nestia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild)\n> [![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/)\n> \n> Nestia is a set of helper libraries for NestJS, supporting below features:\n> \n> - `@nestia/core`: Super-fast decorators\n> - `@nestia/sdk`:\n> - Swagger generator evolved than ever\n> - SDK library generator for clients\n> - Mockup Simulator for client applications\n> - Automatic E2E test functions generator\n> - `@nestia/migrate`: Migration from Swagger to NestJS\n> - `@nestia/editor`: Online TypeScript Swagger Editor\n> - `nestia`: Just CLI (command line interface) tool\n> \n>> **Note**\n>> \n>> - **Only one line** required, with pure TypeScript type\n>> - Enhance performance **30x** up\n>> - Runtime validator is **20,000x faster** than `class-validator`\n>> - JSON serialization is **200x faster** than `class-transformer`\n>> - Software Development Kit\n>> - SDK is a collection of `fetch` functions with type definitions like [tRPC](https://> trpc.io/)\n>> - Mockup simulator means embedded backend simulator in SDK\n>> - similar with [msw](https://mswjs.io/), but fully automated\n",
7
7
  "hello.js": "function print(command, description) {\n return console.log(`\\x1b[1m${command}\\x1b[2m: ${description}\\x1b[0m`);\n}\n\nconsole.log(\"-----------------------------------------\");\nconsole.log(\"\\x1b[7mGenerated by \\x1b[2m@nestia/editor\\x1b[0m\");\nconsole.log(\"\");\nconsole.log(\" - \\x1b[36mhttps://nestia.io/docs/editor\\x1b[0m\");\nconsole.log(\" - \\x1b[36mhttps://github.com/samchon/nestia\\x1b[0m\");\nconsole.log(\"-----------------------------------------\");\n\nprint(\"pnpm run start\", \"Run only test/start.ts\");\nprint(\"pnpm run test\", \"Run every test/features/**/*.ts files\");\nprint(\"pnpm run test:simulate\", \"Test with mockup simulator\");\n",
8
- "package.json": "{\n \"name\": \"@ORGANIZATION/PROJECT-api\",\n \"version\": \"0.1.0\",\n \"description\": \"SDK library generated by Nestia\",\n \"main\": \"src/index.ts\",\n \"exports\": {\n \".\": \"./src/index.ts\",\n \"./lib/*\": \"./src/*.ts\",\n \"./package.json\": \"./package.json\"\n },\n \"publishConfig\": {\n \"main\": \"lib/index.mjs\",\n \"module\": \"lib/index.mjs\",\n \"types\": \"lib/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./lib/index.d.ts\",\n \"import\": \"./lib/index.mjs\",\n \"default\": \"./lib/index.js\"\n },\n \"./lib/*\": {\n \"types\": \"./lib/*.d.ts\",\n \"import\": \"./lib/*.mjs\",\n \"default\": \"./lib/*.js\"\n },\n \"./package.json\": \"./package.json\"\n }\n },\n \"scripts\": {\n \"build\": \"rimraf lib && ttsc && rolldown -c\",\n \"build:test\": \"rimraf bin && ttsc --project test/tsconfig.json\",\n \"deploy\": \"pnpm run build && pnpm publish\",\n \"dev\": \"pnpm run build:test --watch\",\n \"hello\": \"node hello\",\n \"start\": \"ttsx test/start.ts\",\n \"swagger\": \"ttsx test/swagger.ts\",\n \"test\": \"ttsx test/index.ts\",\n \"test:simulate\": \"ttsx test/index.ts --simulate true\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/samchon/nestia\"\n },\n \"author\": \"Jeongho Nam\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/samchon/nestia/issues\"\n },\n \"homepage\": \"https://nestia.io\",\n \"files\": [\n \"lib\",\n \"swagger.json\",\n \"package.json\",\n \"README.md\"\n ],\n \"dependencies\": {\n \"@nestia/fetcher\": \"^13.0.4\",\n \"tgrid\": \"^1.2.1\",\n \"typia\": \"^14.0.2\"\n },\n \"devDependencies\": {\n \"@nestia/e2e\": \"^13.0.4\",\n \"@ttsc/unplugin\": \"^0.28.1\",\n \"@types/express\": \"^4.17.21\",\n \"@types/swagger-ui-express\": \"^4.1.6\",\n \"chalk\": \"4.1.2\",\n \"express\": \"^4.19.2\",\n \"rimraf\": \"^5.0.5\",\n \"rolldown\": \"^1.1.5\",\n \"swagger-ui-express\": \"^5.0.0\",\n \"tinyglobby\": \"^0.2.17\",\n \"ttsc\": \"^0.28.1\",\n \"typescript\": \"^7.0.2\",\n \"@nestia/core\": \"^13.0.4\"\n },\n \"packageManager\": \"pnpm@10.19.0\"\n}",
8
+ "package.json": "{\n \"name\": \"@ORGANIZATION/PROJECT-api\",\n \"version\": \"0.1.0\",\n \"description\": \"SDK library generated by Nestia\",\n \"main\": \"src/index.ts\",\n \"exports\": {\n \".\": \"./src/index.ts\",\n \"./lib/*\": \"./src/*.ts\",\n \"./package.json\": \"./package.json\"\n },\n \"publishConfig\": {\n \"main\": \"lib/index.mjs\",\n \"module\": \"lib/index.mjs\",\n \"types\": \"lib/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./lib/index.d.ts\",\n \"import\": \"./lib/index.mjs\",\n \"default\": \"./lib/index.js\"\n },\n \"./lib/*\": {\n \"types\": \"./lib/*.d.ts\",\n \"import\": \"./lib/*.mjs\",\n \"default\": \"./lib/*.js\"\n },\n \"./package.json\": \"./package.json\"\n }\n },\n \"scripts\": {\n \"build\": \"rimraf lib && ttsc && rolldown -c\",\n \"build:test\": \"rimraf bin && ttsc --project test/tsconfig.json\",\n \"deploy\": \"pnpm run build && pnpm publish\",\n \"dev\": \"pnpm run build:test --watch\",\n \"hello\": \"node hello\",\n \"start\": \"ttsx test/start.ts\",\n \"swagger\": \"ttsx test/swagger.ts\",\n \"test\": \"ttsx test/index.ts\",\n \"test:simulate\": \"ttsx test/index.ts --simulate true\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/samchon/nestia\"\n },\n \"author\": \"Jeongho Nam\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/samchon/nestia/issues\"\n },\n \"homepage\": \"https://nestia.io\",\n \"files\": [\n \"lib\",\n \"swagger.json\",\n \"package.json\",\n \"README.md\"\n ],\n \"dependencies\": {\n \"@nestia/fetcher\": \"^14.0.1\",\n \"tgrid\": \"^1.2.1\",\n \"typia\": \"15.0.0\"\n },\n \"devDependencies\": {\n \"@nestia/e2e\": \"^14.0.1\",\n \"@ttsc/unplugin\": \"^0.28.1\",\n \"@types/express\": \"^4.17.21\",\n \"@types/swagger-ui-express\": \"^4.1.6\",\n \"chalk\": \"4.1.2\",\n \"express\": \"^4.19.2\",\n \"rimraf\": \"^5.0.5\",\n \"rolldown\": \"^1.1.5\",\n \"swagger-ui-express\": \"^5.0.0\",\n \"tinyglobby\": \"^0.2.17\",\n \"ttsc\": \"^0.28.1\",\n \"typescript\": \"^7.0.2\",\n \"@nestia/core\": \"^14.0.1\"\n },\n \"packageManager\": \"pnpm@10.19.0\"\n}",
9
9
  "rolldown.config.mjs": "import ttsc from \"@ttsc/unplugin/rolldown\";\nimport path from \"node:path\";\nimport { globSync } from \"tinyglobby\";\n\nexport default {\n input: globSync(\"./src/**/*.ts\"),\n external: (id) => !id.startsWith(\".\") && !path.isAbsolute(id),\n output: {\n dir: \"./lib\",\n format: \"esm\",\n sourcemap: true,\n entryFileNames: \"[name].mjs\",\n preserveModules: true,\n preserveModulesRoot: \"src\",\n },\n plugins: [ttsc()],\n};\n",
10
10
  "src/HttpError.ts": "export { HttpError } from \"@nestia/fetcher\";\n",
11
11
  "src/IConnection.ts": "export type { IConnection } from \"@nestia/fetcher\";\n",
@@ -76,8 +76,14 @@ export namespace NestiaMigrateCommander {
76
76
  );
77
77
  await NestiaMigrateFileArchiver.archive({
78
78
  mkdir: fs.promises.mkdir,
79
+ // only TypeScript is formatted: prettier's TypeScript parser also
80
+ // accepts a `.gitignore` or `.env` line, and rewrites it into code
79
81
  writeFile: async (file, content) =>
80
- fs.promises.writeFile(file, await beautify(content), "utf-8"),
82
+ fs.promises.writeFile(
83
+ file,
84
+ /\.[cm]?tsx?$/.test(file) ? await beautify(content) : content,
85
+ "utf-8",
86
+ ),
81
87
  root: options.output,
82
88
  files,
83
89
  });
@@ -74,32 +74,18 @@ export namespace NestiaMigrateInquirer {
74
74
  partial.input ??= await input("input")("Swagger file location");
75
75
  partial.output ??= await input("output")("Response directory path");
76
76
  partial.package ??= await input("package")("Package name");
77
- partial.keyword ??=
78
- (await select("keyword")("Keyword parameter mode")([
79
- "true",
80
- "false",
81
- ])) === "true";
82
-
83
- if (partial.keyword)
84
- partial.keyword = (partial.keyword as any) === "true";
85
- else
86
- partial.keyword =
87
- (await select("keyword")("Keyword parameter mode")([
88
- "true",
89
- "false",
90
- ])) === "true";
91
- if (partial.simulate)
92
- partial.simulate = (partial.simulate as any) === "true";
93
- else
94
- partial.simulate =
95
- (await select("simulate")("Mokup Simulator")(["true", "false"])) ===
96
- "true";
97
-
98
- if (partial.e2e) partial.e2e = (partial.e2e as any) === "true";
99
- else
100
- partial.e2e =
101
- (await select("e2e")("Generate E2E tests")(["true", "false"])) ===
102
- "true";
77
+ // a flag given alone is `true`, and one given a value is its text
78
+ const flag = (value: unknown): boolean | undefined =>
79
+ value === undefined ? undefined : value === true || value === "true";
80
+ const ask = async (name: string, message: string): Promise<boolean> =>
81
+ (await select(name)(message)(["true", "false"])) === "true";
82
+ partial.keyword =
83
+ flag(partial.keyword) ??
84
+ (await ask("keyword", "Keyword parameter mode"));
85
+ partial.simulate =
86
+ flag(partial.simulate) ?? (await ask("simulate", "Mokup Simulator"));
87
+ partial.e2e =
88
+ flag(partial.e2e) ?? (await ask("e2e", "Generate E2E tests"));
103
89
  return partial as IOutput;
104
90
  });
105
91
  };
@@ -7,6 +7,8 @@ import { StatementFactory } from "../factories/StatementFactory";
7
7
  import ts from "../internal/ts";
8
8
  import { INestiaMigrateConfig } from "../structures/INestiaMigrateConfig";
9
9
  import { FilePrinter } from "../utils/FilePrinter";
10
+ import { RequestBody } from "../utils/RequestBody";
11
+ import { SuccessStatus } from "../utils/SuccessStatus";
10
12
  import { NestiaMigrateImportProgrammer } from "./NestiaMigrateImportProgrammer";
11
13
  import { NestiaMigrateSchemaProgrammer } from "./NestiaMigrateSchemaProgrammer";
12
14
 
@@ -110,9 +112,7 @@ export namespace NestiaMigrateApiFunctionProgrammer {
110
112
  factory.createTypeReferenceNode(
111
113
  `${ctx.route.accessor.at(-1)!}.Body`,
112
114
  ),
113
- (ctx.route.body.type === "application/json" ||
114
- ctx.route.body.type === "text/plain") &&
115
- ctx.route.operation().requestBody?.required === false
115
+ RequestBody.optional(ctx.route)
116
116
  ? factory.createToken(SyntaxKind.QuestionToken)
117
117
  : undefined,
118
118
  ),
@@ -208,9 +208,15 @@ export namespace NestiaMigrateApiFunctionProgrammer {
208
208
  getArguments(ctx, false),
209
209
  ),
210
210
  ),
211
+ // the fetcher reads only 200, 201, and this status as success
211
212
  factory.createPropertyAssignment(
212
213
  "status",
213
- factory.createNull(),
214
+ ((status) =>
215
+ status === null
216
+ ? factory.createNull()
217
+ : factory.createNumericLiteral(status))(
218
+ SuccessStatus.of(ctx.route),
219
+ ),
214
220
  ),
215
221
  ],
216
222
  true,
@@ -9,6 +9,8 @@ import { TypeFactory } from "../factories/TypeFactory";
9
9
  import ts from "../internal/ts";
10
10
  import { INestiaMigrateConfig } from "../structures/INestiaMigrateConfig";
11
11
  import { FilePrinter } from "../utils/FilePrinter";
12
+ import { PathTemplate } from "../utils/PathTemplate";
13
+ import { RequestBody } from "../utils/RequestBody";
12
14
  import { NestiaMigrateApiSimulationProgrammer } from "./NestiaMigrateApiSimulationProgrammer";
13
15
  import { NestiaMigrateImportProgrammer } from "./NestiaMigrateImportProgrammer";
14
16
  import { NestiaMigrateSchemaProgrammer } from "./NestiaMigrateSchemaProgrammer";
@@ -116,7 +118,9 @@ export namespace NestiaMigrateApiNamespaceProgrammer {
116
118
  required: [
117
119
  ...ctx.route.parameters.map((p) => p.key),
118
120
  ...(ctx.route.query ? [ctx.route.query.key] : []),
119
- ...(ctx.route.body ? [ctx.route.body.key] : []),
121
+ ...(ctx.route.body && RequestBody.optional(ctx.route) === false
122
+ ? [ctx.route.body.key]
123
+ : []),
120
124
  ],
121
125
  },
122
126
  }),
@@ -280,32 +284,49 @@ export namespace NestiaMigrateApiNamespaceProgrammer {
280
284
  ),
281
285
  );
282
286
  const template = () => {
283
- const path: string = getPath(ctx.route);
284
- const split: string[] = path.split(":");
285
- if (split.length === 1) return factory.createStringLiteral(path);
287
+ // the document's template delimits each parameter; the emended path's
288
+ // `:id.json` would not tell the name from the literal after it
289
+ const segments: PathTemplate.ISegment[] = PathTemplate.segments(
290
+ ctx.route,
291
+ );
292
+ if (segments.every((segment) => segment.type === "literal"))
293
+ return factory.createStringLiteral(getPath(ctx.route));
294
+ const literals: string[] = [""];
295
+ const parameters: IHttpMigrateRoute.IParameter[] = [];
296
+ for (const segment of segments)
297
+ if (segment.type === "literal")
298
+ literals[literals.length - 1] += segment.value;
299
+ else {
300
+ parameters.push(segment.parameter);
301
+ literals.push("");
302
+ }
286
303
  return factory.createTemplateExpression(
287
- factory.createTemplateHead(split[0]!),
288
- split.slice(1).map((s, i, arr) => {
289
- const name: string = s.split("/")[0]!;
290
- return factory.createTemplateSpan(
304
+ factory.createTemplateHead(literals[0]!),
305
+ parameters.map((parameter, i) =>
306
+ factory.createTemplateSpan(
307
+ // URI-encoded, "null" when nullish, and a dot segment refused
291
308
  factory.createCallExpression(
292
- factory.createIdentifier("encodeURIComponent"),
309
+ IdentifierFactory.access(
310
+ factory.createIdentifier(
311
+ ctx.importer.external({
312
+ type: "instance",
313
+ library: "@nestia/fetcher",
314
+ name: "PathParameter",
315
+ }),
316
+ ),
317
+ "encode",
318
+ ),
293
319
  undefined,
294
320
  [
295
- factory.createBinaryExpression(
296
- property(
297
- ctx.route.parameters.find((p) => p.name === name)!.key,
298
- ),
299
- factory.createToken(SyntaxKind.QuestionQuestionToken),
300
- factory.createStringLiteral("null"),
301
- ),
321
+ factory.createStringLiteral(parameter.key),
322
+ property(parameter.key),
302
323
  ],
303
324
  ),
304
- (i !== arr.length - 1
325
+ (i !== parameters.length - 1
305
326
  ? factory.createTemplateMiddle
306
- : factory.createTemplateTail)(s.substring(name.length)),
307
- );
308
- }),
327
+ : factory.createTemplateTail)(literals[i + 1]!),
328
+ ),
329
+ ),
309
330
  );
310
331
  };
311
332
  if (!ctx.route.query) return out(template());
@@ -70,7 +70,8 @@ export namespace NestiaMigrateApiSimulationProgrammer {
70
70
  ctx,
71
71
  ctx.route.parameters.length === 0 &&
72
72
  ctx.route.query === null &&
73
- ctx.route.body === null
73
+ ctx.route.body === null &&
74
+ ctx.route.headers === null
74
75
  ? "_connection"
75
76
  : undefined,
76
77
  ),
@@ -127,6 +128,20 @@ export namespace NestiaMigrateApiSimulationProgrammer {
127
128
  },
128
129
  ]
129
130
  : []),
131
+ // the headers travel in the connection, validated as the server does
132
+ ...(ctx.route.headers
133
+ ? [
134
+ {
135
+ category: "headers",
136
+ name: ctx.route.headers.key,
137
+ schema: NestiaMigrateSchemaProgrammer.write({
138
+ components: ctx.components,
139
+ importer: ctx.importer,
140
+ schema: ctx.route.headers.schema,
141
+ }),
142
+ },
143
+ ]
144
+ : []),
130
145
  ];
131
146
  if (parameters.length === 0) return [];
132
147
 
@@ -206,7 +221,9 @@ export namespace NestiaMigrateApiSimulationProgrammer {
206
221
  ),
207
222
  "assert",
208
223
  ),
209
- undefined,
224
+ // `connection.headers` is typed optional with optional
225
+ // members, so the headers name their own type to be checked
226
+ p.category === "headers" ? [p.schema] : undefined,
210
227
  [
211
228
  p.category === "headers"
212
229
  ? factory.createIdentifier("connection.headers")
@@ -218,78 +235,11 @@ export namespace NestiaMigrateApiSimulationProgrammer {
218
235
  ),
219
236
  )
220
237
  .map(factory.createExpressionStatement) as ts.Statement[];
221
- return [validator, tryAndCatch(ctx.importer, individual)];
238
+ // the SDK fetches without propagation, so an invalid simulated request
239
+ // throws its HttpError as the real one does, instead of answering a
240
+ // propagation object in place of the response
241
+ return [validator, ...individual];
222
242
  };
223
-
224
- const tryAndCatch = (
225
- importer: NestiaMigrateImportProgrammer,
226
- individual: ts.Statement[],
227
- ) =>
228
- factory.createTryStatement(
229
- factory.createBlock(individual, true),
230
- factory.createCatchClause(
231
- "exp",
232
- factory.createBlock(
233
- [
234
- factory.createIfStatement(
235
- factory.createLogicalNot(
236
- factory.createCallExpression(
237
- IdentifierFactory.access(
238
- factory.createIdentifier(
239
- importer.external({
240
- type: "default",
241
- library: "typia",
242
- name: "typia",
243
- }),
244
- ),
245
- "is",
246
- ),
247
- [
248
- factory.createTypeReferenceNode(
249
- importer.external({
250
- type: "instance",
251
- library: "@nestia/fetcher",
252
- name: "HttpError",
253
- }),
254
- ),
255
- ],
256
- [factory.createIdentifier("exp")],
257
- ),
258
- ),
259
- factory.createThrowStatement(factory.createIdentifier("exp")),
260
- ),
261
- factory.createReturnStatement(
262
- factory.createAsExpression(
263
- factory.createObjectLiteralExpression(
264
- [
265
- factory.createPropertyAssignment(
266
- "success",
267
- factory.createFalse(),
268
- ),
269
- factory.createPropertyAssignment(
270
- "status",
271
- factory.createIdentifier("exp.status"),
272
- ),
273
- factory.createPropertyAssignment(
274
- "headers",
275
- factory.createIdentifier("exp.headers"),
276
- ),
277
- factory.createPropertyAssignment(
278
- "data",
279
- factory.createIdentifier("exp.toJSON().message"),
280
- ),
281
- ],
282
- true,
283
- ),
284
- TypeFactory.keyword("any"),
285
- ),
286
- ),
287
- ],
288
- true,
289
- ),
290
- ),
291
- undefined,
292
- );
293
243
  }
294
244
 
295
245
  const constant = (name: string, expression: ts.Expression) =>
@@ -91,14 +91,6 @@ export namespace NestiaMigrateE2eFunctionProgrammer {
91
91
  factory.createIdentifier("api.functional"),
92
92
  factory.createIdentifier(ctx.route.accessor.join(".")),
93
93
  );
94
- const connection = factory.createIdentifier("connection");
95
- if (
96
- ctx.route.parameters.length === 0 &&
97
- ctx.route.query === null &&
98
- ctx.route.body === null
99
- )
100
- return factory.createCallExpression(fetch, undefined, [connection]);
101
-
102
94
  const random = factory.createPropertyAccessExpression(
103
95
  factory.createIdentifier(
104
96
  ctx.importer.external({
@@ -109,6 +101,47 @@ export namespace NestiaMigrateE2eFunctionProgrammer {
109
101
  ),
110
102
  "random",
111
103
  );
104
+ // the route's headers travel in the connection, as the server requires
105
+ const connection: ts.Expression = ctx.route.headers
106
+ ? factory.createObjectLiteralExpression(
107
+ [
108
+ factory.createSpreadAssignment(
109
+ factory.createIdentifier("connection"),
110
+ ),
111
+ factory.createPropertyAssignment(
112
+ "headers",
113
+ factory.createObjectLiteralExpression(
114
+ [
115
+ factory.createSpreadAssignment(
116
+ factory.createIdentifier("connection.headers"),
117
+ ),
118
+ factory.createSpreadAssignment(
119
+ factory.createCallExpression(
120
+ random,
121
+ [
122
+ NestiaMigrateSchemaProgrammer.write({
123
+ components: ctx.components,
124
+ importer: ctx.importer,
125
+ schema: ctx.route.headers.schema,
126
+ }),
127
+ ],
128
+ undefined,
129
+ ),
130
+ ),
131
+ ],
132
+ true,
133
+ ),
134
+ ),
135
+ ],
136
+ true,
137
+ )
138
+ : factory.createIdentifier("connection");
139
+ if (
140
+ ctx.route.parameters.length === 0 &&
141
+ ctx.route.query === null &&
142
+ ctx.route.body === null
143
+ )
144
+ return factory.createCallExpression(fetch, undefined, [connection]);
112
145
  if (ctx.config.keyword === true)
113
146
  return factory.createCallExpression(fetch, undefined, [
114
147
  connection,
@@ -2,6 +2,7 @@ import { SyntaxKind, factory } from "@ttsc/factory";
2
2
  import { IHttpMigrateRoute } from "@typia/interface";
3
3
  import { OpenApi } from "typia";
4
4
 
5
+ import { NestiaMigrateControllerAnalyzer } from "../analyzers/NestiaMigrateControllerAnalyzer";
5
6
  import { ExpressionFactory } from "../factories/ExpressionFactory";
6
7
  import { IdentifierFactory } from "../factories/IdentifierFactory";
7
8
  import { LiteralFactory } from "../factories/LiteralFactory";
@@ -10,7 +11,9 @@ import ts from "../internal/ts";
10
11
  import { INestiaMigrateConfig } from "../structures/INestiaMigrateConfig";
11
12
  import { INestiaMigrateController } from "../structures/INestiaMigrateController";
12
13
  import { FilePrinter } from "../utils/FilePrinter";
14
+ import { RequestBody } from "../utils/RequestBody";
13
15
  import { StringUtil } from "../utils/StringUtil";
16
+ import { SuccessStatus } from "../utils/SuccessStatus";
14
17
  import { NestiaMigrateImportProgrammer } from "./NestiaMigrateImportProgrammer";
15
18
  import { NestiaMigrateSchemaProgrammer } from "./NestiaMigrateSchemaProgrammer";
16
19
 
@@ -121,7 +124,9 @@ export namespace NestiaMigrateNestMethodProgrammer {
121
124
  );
122
125
 
123
126
  // ROUTER
124
- const localPath: string = ctx.route.emendedPath
127
+ const localPath: string = NestiaMigrateControllerAnalyzer.routePath(
128
+ ctx.route,
129
+ )
125
130
  .slice(ctx.controller.path.length)
126
131
  .split("/")
127
132
  .filter((str) => !!str.length)
@@ -139,35 +144,43 @@ export namespace NestiaMigrateNestMethodProgrammer {
139
144
  : undefined,
140
145
  ),
141
146
  );
147
+ // a vanilla NestJS method decorator, relative to the controller like the
148
+ // nestia ones: the document's full path would be joined onto the
149
+ // controller's again
150
+ const nest = (method: string) =>
151
+ factory.createDecorator(
152
+ factory.createCallExpression(
153
+ external("@nestjs/common", method),
154
+ [],
155
+ localPath.length
156
+ ? [factory.createStringLiteral(localPath)]
157
+ : undefined,
158
+ ),
159
+ );
142
160
  if (ctx.route.success?.["x-nestia-encrypted"])
143
161
  decorators.push(router("EncryptedRoute"));
144
162
  else if (ctx.route.success?.type === "text/plain")
145
- decorators.push(
146
- factory.createDecorator(
147
- factory.createCallExpression(
148
- external("@nestjs/common", StringUtil.capitalize(ctx.route.method)),
149
- [],
150
- [factory.createStringLiteral(ctx.route.path)],
151
- ),
152
- ),
153
- );
163
+ decorators.push(nest(StringUtil.capitalize(ctx.route.method)));
154
164
  else if (ctx.route.success?.type === "application/x-www-form-urlencoded")
155
165
  decorators.push(router("TypedQuery"));
156
- else if (ctx.route.method === "head")
166
+ else if (ctx.route.method === "head") decorators.push(nest("Head"));
167
+ else if (
168
+ ctx.route.success === null ||
169
+ ctx.route.success?.type === "application/json"
170
+ )
171
+ decorators.push(router("TypedRoute"));
172
+ // NestJS answers 201 for POST and 200 otherwise
173
+ const status: number | null = SuccessStatus.of(ctx.route);
174
+ if (status !== null && status !== SuccessStatus.nest(ctx.route))
157
175
  decorators.push(
158
176
  factory.createDecorator(
159
177
  factory.createCallExpression(
160
- external("@nestjs/common", "Head"),
161
- [],
162
- [factory.createStringLiteral(ctx.route.path)],
178
+ external("@nestjs/common", "HttpCode"),
179
+ undefined,
180
+ [ExpressionFactory.number(status)],
163
181
  ),
164
182
  ),
165
183
  );
166
- else if (
167
- ctx.route.success === null ||
168
- ctx.route.success?.type === "application/json"
169
- )
170
- decorators.push(router("TypedRoute"));
171
184
  for (const [key, value] of Object.entries(ctx.route.exceptions ?? {}))
172
185
  decorators.push(
173
186
  factory.createDecorator(
@@ -291,11 +304,7 @@ export namespace NestiaMigrateNestMethodProgrammer {
291
304
  : [],
292
305
  })(ctx.components)(ctx.importer)({
293
306
  schema: ctx.route.body.schema,
294
- required: !(
295
- (ctx.route.body.type === "application/json" ||
296
- ctx.route.body.type === "text/plain") &&
297
- ctx.route.operation().requestBody?.required === false
298
- ),
307
+ required: RequestBody.optional(ctx.route) === false,
299
308
  example: ctx.route.body.media().example,
300
309
  examples: ctx.route.body.media().examples,
301
310
  }),
@@ -380,8 +389,10 @@ export namespace NestiaMigrateNestMethodProgrammer {
380
389
  ),
381
390
  ]
382
391
  : []),
383
- ...Object.entries(media.examples ?? {}).map(([key, value]) =>
384
- factory.createDecorator(
392
+ ...Object.entries(media.examples ?? {}).flatMap(([key, example]) => {
393
+ const value: { value: unknown } | null = exampleValue(example);
394
+ if (value === null) return [];
395
+ return factory.createDecorator(
385
396
  factory.createCallExpression(
386
397
  IdentifierFactory.access(
387
398
  factory.createIdentifier(
@@ -394,9 +405,57 @@ export namespace NestiaMigrateNestMethodProgrammer {
394
405
  kind,
395
406
  ),
396
407
  [],
397
- [factory.createStringLiteral(key), LiteralFactory.write(value)],
408
+ [
409
+ factory.createStringLiteral(key),
410
+ LiteralFactory.write(value.value),
411
+ ],
398
412
  ),
399
- ),
400
- ),
413
+ );
414
+ }),
401
415
  ];
402
416
  }
417
+
418
+ /**
419
+ * The value a named example stands for, or `null` when it names none.
420
+ *
421
+ * OpenAPI names examples with Example Objects, whose `value` (or, since 3.2,
422
+ * `dataValue`) holds the value, and a `$ref` to one arrives resolved. Documents
423
+ * from earlier nestia versions held the values themselves, so an object is read
424
+ * as an Example Object only when it is one: it has one of the value fields, and
425
+ * every other key is an Example Object field or an extension. A raw value of
426
+ * exactly that shape is ambiguous and is read as an Example Object, as a
427
+ * conforming document means it. An Example Object that only points at its value
428
+ * (`externalValue`) or only serializes it (`serializedValue`) gives
429
+ * `SwaggerExample` nothing to hold.
430
+ */
431
+ const exampleValue = (example: unknown): { value: unknown } | null => {
432
+ if (isExampleObject(example) === false) return { value: example };
433
+ const record = example as Record<string, unknown>;
434
+ if (Object.prototype.hasOwnProperty.call(record, "value"))
435
+ return { value: record.value };
436
+ if (Object.prototype.hasOwnProperty.call(record, "dataValue"))
437
+ return { value: record.dataValue };
438
+ return null;
439
+ };
440
+
441
+ const isExampleObject = (example: unknown): boolean =>
442
+ typeof example === "object" &&
443
+ example !== null &&
444
+ Array.isArray(example) === false &&
445
+ VALUE_KEYS.some((key) =>
446
+ Object.prototype.hasOwnProperty.call(example, key),
447
+ ) &&
448
+ Object.keys(example).every(
449
+ (key) =>
450
+ VALUE_KEYS.includes(key) ||
451
+ key === "summary" ||
452
+ key === "description" ||
453
+ key.startsWith("x-"),
454
+ );
455
+
456
+ const VALUE_KEYS: string[] = [
457
+ "value",
458
+ "dataValue",
459
+ "serializedValue",
460
+ "externalValue",
461
+ ];
@@ -123,12 +123,29 @@ export namespace NestiaMigrateSchemaProgrammer {
123
123
  writeNumeric({
124
124
  factory: () => [
125
125
  TypeFactory.keyword("number"),
126
- props.importer.tag("Type", "int32"),
126
+ props.importer.tag("Type", integerType(props.schema)),
127
127
  ],
128
128
  importer: props.importer,
129
129
  schema: props.schema,
130
130
  });
131
131
 
132
+ /**
133
+ * The typia integer type admitting what the schema admits: its own `format`
134
+ * when it names one typia has, else `int64`, as an integer schema without a
135
+ * format admits any integer.
136
+ */
137
+ const integerType = (
138
+ schema: OpenApi.IJsonSchema.IInteger,
139
+ ): "int32" | "uint32" | "int64" | "uint64" => {
140
+ const format: unknown = (schema as { format?: unknown }).format;
141
+ return format === "int32" ||
142
+ format === "uint32" ||
143
+ format === "int64" ||
144
+ format === "uint64"
145
+ ? format
146
+ : "int64";
147
+ };
148
+
132
149
  const writeNumber = (props: {
133
150
  importer: NestiaMigrateImportProgrammer;
134
151
  schema: OpenApi.IJsonSchema.INumber;
@@ -147,19 +164,19 @@ export namespace NestiaMigrateSchemaProgrammer {
147
164
  const intersection: ts.TypeNode[] = props.factory();
148
165
  if (props.schema.default !== undefined)
149
166
  intersection.push(props.importer.tag("Default", props.schema.default));
167
+ // the emended schema holds each bound as a number, as OpenAPI 3.1 does;
168
+ // an OpenAPI 3.0 boolean `exclusiveMinimum` is converted into that form
150
169
  if (props.schema.minimum !== undefined)
170
+ intersection.push(props.importer.tag("Minimum", props.schema.minimum));
171
+ if (typeof props.schema.exclusiveMinimum === "number")
151
172
  intersection.push(
152
- props.importer.tag(
153
- props.schema.exclusiveMinimum ? "ExclusiveMinimum" : "Minimum",
154
- props.schema.minimum,
155
- ),
173
+ props.importer.tag("ExclusiveMinimum", props.schema.exclusiveMinimum),
156
174
  );
157
175
  if (props.schema.maximum !== undefined)
176
+ intersection.push(props.importer.tag("Maximum", props.schema.maximum));
177
+ if (typeof props.schema.exclusiveMaximum === "number")
158
178
  intersection.push(
159
- props.importer.tag(
160
- props.schema.exclusiveMaximum ? "ExclusiveMaximum" : "Maximum",
161
- props.schema.maximum,
162
- ),
179
+ props.importer.tag("ExclusiveMaximum", props.schema.exclusiveMaximum),
163
180
  );
164
181
  if (props.schema.multipleOf !== undefined)
165
182
  intersection.push(
@@ -299,7 +316,8 @@ export namespace NestiaMigrateSchemaProgrammer {
299
316
  schema: props.schema.additionalProperties as OpenApi.IJsonSchema,
300
317
  }),
301
318
  ]);
302
- return !!props.schema.properties?.length &&
319
+ // `properties` is a record, which has no length
320
+ return Object.keys(props.schema.properties ?? {}).length !== 0 &&
303
321
  typeof props.schema.additionalProperties === "object"
304
322
  ? factory.createIntersectionTypeNode([regular(), dynamic()])
305
323
  : typeof props.schema.additionalProperties === "object"