@nestia/migrate 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.
- package/README.md +1 -1
- package/lib/analyzers/NestiaMigrateControllerAnalyzer.d.ts +13 -0
- package/lib/analyzers/NestiaMigrateControllerAnalyzer.js +32 -2
- package/lib/analyzers/NestiaMigrateControllerAnalyzer.js.map +1 -1
- package/lib/analyzers/NestiaMigrateControllerAnalyzer.mjs +14 -2
- package/lib/bundles/NEST_TEMPLATE.js +1 -1
- package/lib/bundles/NEST_TEMPLATE.js.map +1 -1
- package/lib/bundles/NEST_TEMPLATE.mjs +1 -1
- package/lib/bundles/SDK_TEMPLATE.js +1 -1
- package/lib/bundles/SDK_TEMPLATE.js.map +1 -1
- package/lib/bundles/SDK_TEMPLATE.mjs +1 -1
- package/lib/executable/NestiaMigrateCommander.js +5 -1
- package/lib/executable/NestiaMigrateCommander.js.map +1 -1
- package/lib/executable/NestiaMigrateCommander.mjs +1 -1
- package/lib/executable/NestiaMigrateInquirer.js +7 -25
- package/lib/executable/NestiaMigrateInquirer.js.map +1 -1
- package/lib/executable/NestiaMigrateInquirer.mjs +5 -7
- package/lib/programmers/NestiaMigrateApiFunctionProgrammer.js +7 -5
- package/lib/programmers/NestiaMigrateApiFunctionProgrammer.js.map +1 -1
- package/lib/programmers/NestiaMigrateApiFunctionProgrammer.mjs +4 -2
- package/lib/programmers/NestiaMigrateApiNamespaceProgrammer.js +31 -13
- package/lib/programmers/NestiaMigrateApiNamespaceProgrammer.js.map +1 -1
- package/lib/programmers/NestiaMigrateApiNamespaceProgrammer.mjs +17 -8
- package/lib/programmers/NestiaMigrateApiSimulationProgrammer.js +24 -22
- package/lib/programmers/NestiaMigrateApiSimulationProgrammer.js.map +1 -1
- package/lib/programmers/NestiaMigrateApiSimulationProgrammer.mjs +13 -20
- package/lib/programmers/NestiaMigrateE2eFileProgrammer.js +20 -5
- package/lib/programmers/NestiaMigrateE2eFileProgrammer.js.map +1 -1
- package/lib/programmers/NestiaMigrateE2eFileProgrammer.mjs +6 -2
- package/lib/programmers/NestiaMigrateNestMethodProgrammer.js +69 -13
- package/lib/programmers/NestiaMigrateNestMethodProgrammer.js.map +1 -1
- package/lib/programmers/NestiaMigrateNestMethodProgrammer.mjs +46 -9
- package/lib/programmers/NestiaMigrateSchemaProgrammer.js +25 -4
- package/lib/programmers/NestiaMigrateSchemaProgrammer.js.map +1 -1
- package/lib/programmers/NestiaMigrateSchemaProgrammer.mjs +15 -4
- package/lib/utils/PathTemplate.d.ts +19 -0
- package/lib/utils/PathTemplate.js +42 -0
- package/lib/utils/PathTemplate.js.map +1 -0
- package/lib/utils/PathTemplate.mjs +34 -0
- package/lib/utils/PathTemplate.mjs.map +1 -0
- package/lib/utils/RequestBody.d.ts +9 -0
- package/lib/utils/RequestBody.js +16 -0
- package/lib/utils/RequestBody.js.map +1 -0
- package/lib/utils/RequestBody.mjs +9 -0
- package/lib/utils/RequestBody.mjs.map +1 -0
- package/lib/utils/SuccessStatus.d.ts +14 -0
- package/lib/utils/SuccessStatus.js +28 -0
- package/lib/utils/SuccessStatus.js.map +1 -0
- package/lib/utils/SuccessStatus.mjs +14 -0
- package/lib/utils/SuccessStatus.mjs.map +1 -0
- package/package.json +2 -2
- package/src/analyzers/NestiaMigrateControllerAnalyzer.ts +39 -2
- package/src/bundles/NEST_TEMPLATE.ts +1 -1
- package/src/bundles/SDK_TEMPLATE.ts +1 -1
- package/src/executable/NestiaMigrateCommander.ts +7 -1
- package/src/executable/NestiaMigrateInquirer.ts +12 -26
- package/src/programmers/NestiaMigrateApiFunctionProgrammer.ts +10 -4
- package/src/programmers/NestiaMigrateApiNamespaceProgrammer.ts +41 -20
- package/src/programmers/NestiaMigrateApiSimulationProgrammer.ts +23 -73
- package/src/programmers/NestiaMigrateE2eFileProgrammer.ts +41 -8
- package/src/programmers/NestiaMigrateNestMethodProgrammer.ts +88 -29
- package/src/programmers/NestiaMigrateSchemaProgrammer.ts +28 -10
- package/src/utils/PathTemplate.ts +39 -0
- package/src/utils/RequestBody.ts +14 -0
- package/src/utils/SuccessStatus.ts +29 -0
|
@@ -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(
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
])) === "true";
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
partial.
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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.
|
|
296
|
-
|
|
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 !==
|
|
325
|
+
(i !== parameters.length - 1
|
|
305
326
|
? factory.createTemplateMiddle
|
|
306
|
-
: factory.createTemplateTail)(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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", "
|
|
161
|
-
|
|
162
|
-
[
|
|
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 ?? {}).
|
|
384
|
-
|
|
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
|
-
[
|
|
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",
|
|
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
|
-
|
|
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"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { IHttpMigrateRoute } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
export namespace PathTemplate {
|
|
4
|
+
export type ISegment =
|
|
5
|
+
| { type: "literal"; value: string }
|
|
6
|
+
| { type: "param"; parameter: IHttpMigrateRoute.IParameter };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The route's path as literal text and path parameters, in order, read from
|
|
10
|
+
* the document's own template (`/files/{id}.json`), whose braces delimit each
|
|
11
|
+
* parameter name. The emended path (`/files/:id.json`) cannot be split back:
|
|
12
|
+
* its `:id.json` could name `id` or `id.json`, and a name such as `item-id`
|
|
13
|
+
* would read as `item` followed by `-id`. It always starts with `/`, as the
|
|
14
|
+
* emended path does.
|
|
15
|
+
*/
|
|
16
|
+
export const segments = (route: IHttpMigrateRoute): ISegment[] => {
|
|
17
|
+
const path: string = route.path.startsWith("/")
|
|
18
|
+
? route.path
|
|
19
|
+
: `/${route.path}`;
|
|
20
|
+
const output: ISegment[] = [];
|
|
21
|
+
const literal = (value: string): void => {
|
|
22
|
+
if (value.length === 0) return;
|
|
23
|
+
const last: ISegment | undefined = output[output.length - 1];
|
|
24
|
+
if (last?.type === "literal") last.value += value;
|
|
25
|
+
else output.push({ type: "literal", value });
|
|
26
|
+
};
|
|
27
|
+
let cursor: number = 0;
|
|
28
|
+
for (const match of path.matchAll(/\{([^{}]+)\}/g)) {
|
|
29
|
+
literal(path.slice(cursor, match.index));
|
|
30
|
+
cursor = match.index! + match[0].length;
|
|
31
|
+
const parameter: IHttpMigrateRoute.IParameter | undefined =
|
|
32
|
+
route.parameters.find((p) => p.name === match[1]);
|
|
33
|
+
if (parameter === undefined) literal(match[0]);
|
|
34
|
+
else output.push({ type: "param", parameter });
|
|
35
|
+
}
|
|
36
|
+
literal(path.slice(cursor));
|
|
37
|
+
return output;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IHttpMigrateRoute } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
export namespace RequestBody {
|
|
4
|
+
/**
|
|
5
|
+
* Whether the route's body may be omitted: a JSON or text body the document
|
|
6
|
+
* does not require. The SDK's positional and keyword functions and the Nest
|
|
7
|
+
* controller all read this one rule.
|
|
8
|
+
*/
|
|
9
|
+
export const optional = (route: IHttpMigrateRoute): boolean =>
|
|
10
|
+
route.body !== null &&
|
|
11
|
+
(route.body.type === "application/json" ||
|
|
12
|
+
route.body.type === "text/plain") &&
|
|
13
|
+
route.operation().requestBody?.required === false;
|
|
14
|
+
}
|