@nestia/sdk 2.6.2 → 2.6.3
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/lib/analyses/ControllerAnalyzer.js +3 -3
- package/lib/analyses/ControllerAnalyzer.js.map +1 -1
- package/lib/analyses/ImportAnalyzer.d.ts +1 -2
- package/lib/analyses/ImportAnalyzer.js +2 -2
- package/lib/analyses/ImportAnalyzer.js.map +1 -1
- package/lib/analyses/ReflectAnalyzer.js +2 -2
- package/lib/analyses/ReflectAnalyzer.js.map +1 -1
- package/lib/generates/SwaggerGenerator.js +4 -4
- package/lib/generates/SwaggerGenerator.js.map +1 -1
- package/lib/generates/internal/ImportDictionary.js +6 -8
- package/lib/generates/internal/ImportDictionary.js.map +1 -1
- package/lib/structures/TypeEntry.js +2 -2
- package/lib/structures/TypeEntry.js.map +1 -1
- package/package.json +4 -4
- package/src/INestiaConfig.ts +248 -248
- package/src/NestiaSdkApplication.ts +255 -255
- package/src/analyses/ControllerAnalyzer.ts +402 -402
- package/src/analyses/ExceptionAnalyzer.ts +148 -148
- package/src/analyses/ImportAnalyzer.ts +1 -2
- package/src/analyses/ReflectAnalyzer.ts +463 -463
- package/src/analyses/SecurityAnalyzer.ts +24 -24
- package/src/generates/CloneGenerator.ts +62 -62
- package/src/generates/E2eGenerator.ts +66 -66
- package/src/generates/SdkGenerator.ts +84 -84
- package/src/generates/SwaggerGenerator.ts +446 -446
- package/src/generates/internal/E2eFileProgrammer.ts +182 -182
- package/src/generates/internal/FilePrinter.ts +53 -53
- package/src/generates/internal/ImportDictionary.ts +147 -149
- package/src/generates/internal/SdkAliasCollection.ts +152 -152
- package/src/generates/internal/SdkCloneProgrammer.ts +155 -155
- package/src/generates/internal/SdkFileProgrammer.ts +115 -115
- package/src/generates/internal/SdkFunctionProgrammer.ts +298 -298
- package/src/generates/internal/SdkImportWizard.ts +55 -55
- package/src/generates/internal/SdkNamespaceProgrammer.ts +510 -510
- package/src/generates/internal/SdkRouteProgrammer.ts +83 -83
- package/src/generates/internal/SdkSimulationProgrammer.ts +365 -365
- package/src/generates/internal/SdkTypeProgrammer.ts +385 -385
- package/src/generates/internal/SwaggerSchemaGenerator.ts +438 -438
- package/src/structures/IController.ts +94 -94
- package/src/structures/IRoute.ts +53 -53
- package/src/structures/ISwagger.ts +91 -91
- package/src/structures/ISwaggerRoute.ts +54 -54
- package/src/structures/ISwaggerSecurityScheme.ts +65 -65
- package/src/structures/ParamCategory.ts +1 -1
- package/src/structures/TypeEntry.ts +1 -1
- package/src/utils/StringUtil.ts +6 -6
|
@@ -1,402 +1,402 @@
|
|
|
1
|
-
import { VERSION_NEUTRAL, VersionValue } from "@nestjs/common/interfaces";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { HashMap } from "tstl
|
|
4
|
-
import ts from "typescript";
|
|
5
|
-
import { CommentFactory } from "typia/lib/factories/CommentFactory";
|
|
6
|
-
|
|
7
|
-
import { IController } from "../structures/IController";
|
|
8
|
-
import { IErrorReport } from "../structures/IErrorReport";
|
|
9
|
-
import { INestiaProject } from "../structures/INestiaProject";
|
|
10
|
-
import { IRoute } from "../structures/IRoute";
|
|
11
|
-
import { ITypeTuple } from "../structures/ITypeTuple";
|
|
12
|
-
import { PathUtil } from "../utils/PathUtil";
|
|
13
|
-
import { ExceptionAnalyzer } from "./ExceptionAnalyzer";
|
|
14
|
-
import { GenericAnalyzer } from "./GenericAnalyzer";
|
|
15
|
-
import { ImportAnalyzer } from "./ImportAnalyzer";
|
|
16
|
-
import { PathAnalyzer } from "./PathAnalyzer";
|
|
17
|
-
import { SecurityAnalyzer } from "./SecurityAnalyzer";
|
|
18
|
-
|
|
19
|
-
export namespace ControllerAnalyzer {
|
|
20
|
-
export const analyze =
|
|
21
|
-
(project: INestiaProject) =>
|
|
22
|
-
async (
|
|
23
|
-
sourceFile: ts.SourceFile,
|
|
24
|
-
controller: IController,
|
|
25
|
-
): Promise<IRoute[]> => {
|
|
26
|
-
// FIND CONTROLLER CLASS
|
|
27
|
-
const ret: IRoute[] = [];
|
|
28
|
-
ts.forEachChild(sourceFile, (node) => {
|
|
29
|
-
if (
|
|
30
|
-
ts.isClassDeclaration(node) &&
|
|
31
|
-
node.name?.escapedText === controller.name
|
|
32
|
-
) {
|
|
33
|
-
// ANALYZE THE CONTROLLER
|
|
34
|
-
ret.push(..._Analyze_controller(project)(controller, node));
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
return ret;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/* ---------------------------------------------------------
|
|
42
|
-
CLASS
|
|
43
|
-
--------------------------------------------------------- */
|
|
44
|
-
const _Analyze_controller =
|
|
45
|
-
(project: INestiaProject) =>
|
|
46
|
-
(controller: IController, classNode: ts.ClassDeclaration): IRoute[] => {
|
|
47
|
-
const classType: ts.InterfaceType = project.checker.getTypeAtLocation(
|
|
48
|
-
classNode,
|
|
49
|
-
) as ts.InterfaceType;
|
|
50
|
-
const genericDict: GenericAnalyzer.Dictionary = GenericAnalyzer.analyze(
|
|
51
|
-
project.checker,
|
|
52
|
-
classNode,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const ret: IRoute[] = [];
|
|
56
|
-
for (const property of classType.getProperties()) {
|
|
57
|
-
// GET METHOD DECLARATION
|
|
58
|
-
const declaration: ts.Declaration | undefined =
|
|
59
|
-
(property.declarations || [])[0];
|
|
60
|
-
if (!declaration || !ts.isMethodDeclaration(declaration)) continue;
|
|
61
|
-
|
|
62
|
-
// IDENTIFIER MUST BE
|
|
63
|
-
const identifier = declaration.name;
|
|
64
|
-
if (!ts.isIdentifier(identifier)) continue;
|
|
65
|
-
|
|
66
|
-
// ANALYZED WITH THE REFLECTED-FUNCTION
|
|
67
|
-
const runtime: IController.IFunction | undefined =
|
|
68
|
-
controller.functions.find((f) => f.name === identifier.escapedText);
|
|
69
|
-
if (runtime === undefined) continue;
|
|
70
|
-
|
|
71
|
-
const routes: IRoute[] = _Analyze_function(project)(
|
|
72
|
-
controller,
|
|
73
|
-
genericDict,
|
|
74
|
-
runtime,
|
|
75
|
-
declaration,
|
|
76
|
-
property,
|
|
77
|
-
);
|
|
78
|
-
ret.push(...routes);
|
|
79
|
-
}
|
|
80
|
-
return ret;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/* ---------------------------------------------------------
|
|
84
|
-
FUNCTION
|
|
85
|
-
--------------------------------------------------------- */
|
|
86
|
-
const _Analyze_function =
|
|
87
|
-
(project: INestiaProject) =>
|
|
88
|
-
(
|
|
89
|
-
controller: IController,
|
|
90
|
-
genericDict: GenericAnalyzer.Dictionary,
|
|
91
|
-
func: IController.IFunction,
|
|
92
|
-
declaration: ts.MethodDeclaration,
|
|
93
|
-
symbol: ts.Symbol,
|
|
94
|
-
): IRoute[] => {
|
|
95
|
-
// PREPARE ASSETS
|
|
96
|
-
const type: ts.Type = project.checker.getTypeOfSymbolAtLocation(
|
|
97
|
-
symbol,
|
|
98
|
-
symbol.valueDeclaration!,
|
|
99
|
-
);
|
|
100
|
-
const signature: ts.Signature | undefined =
|
|
101
|
-
project.checker.getSignaturesOfType(type, ts.SignatureKind.Call)[0];
|
|
102
|
-
if (signature === undefined) {
|
|
103
|
-
project.errors.push({
|
|
104
|
-
file: controller.file,
|
|
105
|
-
controller: controller.name,
|
|
106
|
-
function: func.name,
|
|
107
|
-
message: "unable to get the type signature.",
|
|
108
|
-
});
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// SKIP @IGNORE TAG
|
|
113
|
-
const jsDocTags = signature.getJsDocTags();
|
|
114
|
-
if (jsDocTags.some((tag) => tag.name === "ignore")) return [];
|
|
115
|
-
|
|
116
|
-
// EXPLORE CHILDREN TYPES
|
|
117
|
-
const importDict: ImportAnalyzer.Dictionary = new HashMap();
|
|
118
|
-
const parameters: Array<IRoute.IParameter | null> = func.parameters.map(
|
|
119
|
-
(param) =>
|
|
120
|
-
_Analyze_parameter(project)(
|
|
121
|
-
genericDict,
|
|
122
|
-
importDict,
|
|
123
|
-
controller,
|
|
124
|
-
func.name,
|
|
125
|
-
param,
|
|
126
|
-
signature.getParameters()[param.index],
|
|
127
|
-
)!,
|
|
128
|
-
);
|
|
129
|
-
const outputType: ITypeTuple | null = ImportAnalyzer.analyze(
|
|
130
|
-
project.checker,
|
|
131
|
-
genericDict,
|
|
132
|
-
importDict,
|
|
133
|
-
signature.getReturnType(),
|
|
134
|
-
);
|
|
135
|
-
if (
|
|
136
|
-
outputType === null ||
|
|
137
|
-
(project.config.clone !== true &&
|
|
138
|
-
(outputType.typeName === "__type" ||
|
|
139
|
-
outputType.typeName === "__object"))
|
|
140
|
-
) {
|
|
141
|
-
project.errors.push({
|
|
142
|
-
file: controller.file,
|
|
143
|
-
controller: controller.name,
|
|
144
|
-
function: func.name,
|
|
145
|
-
message: "implicit (unnamed) return type.",
|
|
146
|
-
});
|
|
147
|
-
return [];
|
|
148
|
-
} else if (
|
|
149
|
-
func.method === "HEAD" &&
|
|
150
|
-
outputType.typeName !== "void" &&
|
|
151
|
-
outputType.typeName !== "undefined"
|
|
152
|
-
) {
|
|
153
|
-
project.errors.push({
|
|
154
|
-
file: controller.file,
|
|
155
|
-
controller: controller.name,
|
|
156
|
-
function: func.name,
|
|
157
|
-
message: `HEAD method must return void type.`,
|
|
158
|
-
});
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const exceptions = ExceptionAnalyzer.analyze(project)(
|
|
163
|
-
genericDict,
|
|
164
|
-
project.config.propagate === true ? importDict : new HashMap(),
|
|
165
|
-
)(
|
|
166
|
-
controller,
|
|
167
|
-
func,
|
|
168
|
-
)(declaration);
|
|
169
|
-
const imports: [string, string[]][] = importDict
|
|
170
|
-
.toJSON()
|
|
171
|
-
.map((pair) => [pair.first, pair.second.toJSON()]);
|
|
172
|
-
|
|
173
|
-
// CONSIDER SECURITY TAGS
|
|
174
|
-
const security: Record<string, string[]>[] = SecurityAnalyzer.merge(
|
|
175
|
-
...controller.security,
|
|
176
|
-
...func.security,
|
|
177
|
-
...jsDocTags
|
|
178
|
-
.filter((tag) => tag.name === "security")
|
|
179
|
-
.map((tag) =>
|
|
180
|
-
tag.text === undefined
|
|
181
|
-
? [{}]
|
|
182
|
-
: tag.text.map((text) => {
|
|
183
|
-
const line: string[] = text.text
|
|
184
|
-
.split(" ")
|
|
185
|
-
.filter((s) => s.trim())
|
|
186
|
-
.filter((s) => !!s.length);
|
|
187
|
-
if (line.length === 0) return {};
|
|
188
|
-
return {
|
|
189
|
-
[line[0]]: line.slice(1),
|
|
190
|
-
};
|
|
191
|
-
}),
|
|
192
|
-
)
|
|
193
|
-
.flat(),
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
// CONSTRUCT COMMON DATA
|
|
197
|
-
const common: Omit<IRoute, "path" | "accessors"> = {
|
|
198
|
-
...func,
|
|
199
|
-
controller: controller.target,
|
|
200
|
-
parameters: parameters.filter((p) => p !== null) as IRoute.IParameter[],
|
|
201
|
-
output: {
|
|
202
|
-
type: outputType.type,
|
|
203
|
-
typeName: outputType.typeName,
|
|
204
|
-
contentType: func.contentType,
|
|
205
|
-
},
|
|
206
|
-
imports,
|
|
207
|
-
status: func.status,
|
|
208
|
-
target: {
|
|
209
|
-
class: controller.target,
|
|
210
|
-
function: func.target,
|
|
211
|
-
},
|
|
212
|
-
location: (() => {
|
|
213
|
-
const file = declaration.getSourceFile();
|
|
214
|
-
const { line, character } = file.getLineAndCharacterOfPosition(
|
|
215
|
-
declaration.pos,
|
|
216
|
-
);
|
|
217
|
-
return `${path.relative(process.cwd(), file.fileName)}:${line + 1}:${
|
|
218
|
-
character + 1
|
|
219
|
-
}`;
|
|
220
|
-
})(),
|
|
221
|
-
description: CommentFactory.description(symbol),
|
|
222
|
-
operationId: jsDocTags
|
|
223
|
-
.find(({ name }) => name === "operationId")
|
|
224
|
-
?.text?.[0].text.split(" ")[0]
|
|
225
|
-
.trim(),
|
|
226
|
-
jsDocTags: jsDocTags,
|
|
227
|
-
setHeaders: jsDocTags
|
|
228
|
-
.filter(
|
|
229
|
-
(t) =>
|
|
230
|
-
t.text?.length &&
|
|
231
|
-
t.text[0].text &&
|
|
232
|
-
(t.name === "setHeader" || t.name === "assignHeaders"),
|
|
233
|
-
)
|
|
234
|
-
.map((t) =>
|
|
235
|
-
t.name === "setHeader"
|
|
236
|
-
? {
|
|
237
|
-
type: "setter",
|
|
238
|
-
source: t.text![0].text.split(" ")[0].trim(),
|
|
239
|
-
target: t.text![0].text.split(" ")[1]?.trim(),
|
|
240
|
-
}
|
|
241
|
-
: {
|
|
242
|
-
type: "assigner",
|
|
243
|
-
source: t.text![0].text,
|
|
244
|
-
},
|
|
245
|
-
),
|
|
246
|
-
security,
|
|
247
|
-
exceptions,
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// CONFIGURE PATHS
|
|
251
|
-
const pathList: Set<string> = new Set();
|
|
252
|
-
const versions: Array<string | null> = _Analyze_versions(
|
|
253
|
-
project.input.versioning === undefined
|
|
254
|
-
? undefined
|
|
255
|
-
: func.versions ??
|
|
256
|
-
controller.versions ??
|
|
257
|
-
(project.input.versioning?.defaultVersion !== undefined
|
|
258
|
-
? Array.isArray(project.input.versioning?.defaultVersion)
|
|
259
|
-
? project.input.versioning?.defaultVersion
|
|
260
|
-
: [project.input.versioning?.defaultVersion]
|
|
261
|
-
: undefined) ??
|
|
262
|
-
undefined,
|
|
263
|
-
);
|
|
264
|
-
for (const prefix of controller.prefixes)
|
|
265
|
-
for (const cPath of controller.paths)
|
|
266
|
-
for (const filePath of func.paths)
|
|
267
|
-
pathList.add(PathAnalyzer.join(prefix, cPath, filePath));
|
|
268
|
-
|
|
269
|
-
return [...pathList]
|
|
270
|
-
.map((individual) =>
|
|
271
|
-
PathAnalyzer.combinate(project.input.globalPrefix)(
|
|
272
|
-
[...versions].map((v) =>
|
|
273
|
-
v === null
|
|
274
|
-
? null
|
|
275
|
-
: project.input.versioning?.prefix?.length
|
|
276
|
-
? `${project.input.versioning.prefix}${v}`
|
|
277
|
-
: v,
|
|
278
|
-
),
|
|
279
|
-
)({
|
|
280
|
-
method: func.method,
|
|
281
|
-
path: individual,
|
|
282
|
-
}),
|
|
283
|
-
)
|
|
284
|
-
.flat()
|
|
285
|
-
.filter((path) => {
|
|
286
|
-
const escaped: string | null = PathAnalyzer.escape(path);
|
|
287
|
-
if (escaped === null)
|
|
288
|
-
project.errors.push({
|
|
289
|
-
file: controller.file,
|
|
290
|
-
controller: controller.name,
|
|
291
|
-
function: func.name,
|
|
292
|
-
message: `unable to escape the path "${path}".`,
|
|
293
|
-
});
|
|
294
|
-
return escaped !== null;
|
|
295
|
-
})
|
|
296
|
-
.map((path) => ({
|
|
297
|
-
...common,
|
|
298
|
-
path: PathAnalyzer.escape(path)!,
|
|
299
|
-
accessors: [...PathUtil.accessors(path), func.name],
|
|
300
|
-
}));
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const _Analyze_parameter =
|
|
304
|
-
(project: INestiaProject) =>
|
|
305
|
-
(
|
|
306
|
-
genericDict: GenericAnalyzer.Dictionary,
|
|
307
|
-
importDict: ImportAnalyzer.Dictionary,
|
|
308
|
-
controller: IController,
|
|
309
|
-
funcName: string,
|
|
310
|
-
param: IController.IParameter,
|
|
311
|
-
symbol: ts.Symbol,
|
|
312
|
-
): IRoute.IParameter | null => {
|
|
313
|
-
const type: ts.Type = project.checker.getTypeOfSymbolAtLocation(
|
|
314
|
-
symbol,
|
|
315
|
-
symbol.valueDeclaration!,
|
|
316
|
-
);
|
|
317
|
-
const name: string = symbol.getEscapedName().toString();
|
|
318
|
-
|
|
319
|
-
const optional: boolean = !!project.checker.symbolToParameterDeclaration(
|
|
320
|
-
symbol,
|
|
321
|
-
undefined,
|
|
322
|
-
undefined,
|
|
323
|
-
)?.questionToken;
|
|
324
|
-
|
|
325
|
-
const errors: IErrorReport[] = [];
|
|
326
|
-
|
|
327
|
-
// DO NOT SUPPORT BODY PARAMETER
|
|
328
|
-
if (param.category === "body" && param.field !== undefined)
|
|
329
|
-
errors.push({
|
|
330
|
-
file: controller.file,
|
|
331
|
-
controller: controller.name,
|
|
332
|
-
function: funcName,
|
|
333
|
-
message:
|
|
334
|
-
`nestia does not support body field specification. ` +
|
|
335
|
-
`Therefore, erase the "${name}" parameter and ` +
|
|
336
|
-
`re-define a new body decorator accepting full structured message.`,
|
|
337
|
-
});
|
|
338
|
-
if (optional === true && param.category !== "query")
|
|
339
|
-
errors.push({
|
|
340
|
-
file: controller.file,
|
|
341
|
-
controller: controller.name,
|
|
342
|
-
function: funcName,
|
|
343
|
-
message:
|
|
344
|
-
`nestia does not support optional parameter except query parameter. ` +
|
|
345
|
-
`Therefore, erase question mark on the "${name}" parameter, ` +
|
|
346
|
-
`or re-define a new method without the "${name}" parameter.`,
|
|
347
|
-
});
|
|
348
|
-
if (
|
|
349
|
-
optional === true &&
|
|
350
|
-
param.category === "query" &&
|
|
351
|
-
param.field === undefined
|
|
352
|
-
)
|
|
353
|
-
errors.push({
|
|
354
|
-
file: controller.file,
|
|
355
|
-
controller: controller.name,
|
|
356
|
-
function: funcName,
|
|
357
|
-
message:
|
|
358
|
-
`nestia does not support optional query parameter without field specification. ` +
|
|
359
|
-
`Therefore, erase question mark on the "${name}" parameter, ` +
|
|
360
|
-
`or re-define re-define parameters for each query parameters.`,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// GET TYPE NAME
|
|
364
|
-
const tuple: ITypeTuple | null = ImportAnalyzer.analyze(
|
|
365
|
-
project.checker,
|
|
366
|
-
genericDict,
|
|
367
|
-
importDict,
|
|
368
|
-
type,
|
|
369
|
-
);
|
|
370
|
-
if (
|
|
371
|
-
tuple === null ||
|
|
372
|
-
(project.config.clone !== true &&
|
|
373
|
-
(tuple.typeName === "__type" || tuple.typeName === "__object"))
|
|
374
|
-
)
|
|
375
|
-
errors.push({
|
|
376
|
-
file: controller.file,
|
|
377
|
-
controller: controller.name,
|
|
378
|
-
function: funcName,
|
|
379
|
-
message: `implicit (unnamed) parameter type from "${name}".`,
|
|
380
|
-
});
|
|
381
|
-
if (errors.length) {
|
|
382
|
-
project.errors.push(...errors);
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
...param,
|
|
387
|
-
name,
|
|
388
|
-
optional,
|
|
389
|
-
type: tuple!.type,
|
|
390
|
-
typeName: tuple!.typeName,
|
|
391
|
-
};
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
function _Analyze_versions(
|
|
395
|
-
value:
|
|
396
|
-
| Array<Exclude<VersionValue, Array<string | typeof VERSION_NEUTRAL>>>
|
|
397
|
-
| undefined,
|
|
398
|
-
): Array<string | null> {
|
|
399
|
-
if (value === undefined) return [null];
|
|
400
|
-
return value.map((v) => (typeof v === "symbol" ? null : v));
|
|
401
|
-
}
|
|
402
|
-
}
|
|
1
|
+
import { VERSION_NEUTRAL, VersionValue } from "@nestjs/common/interfaces";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { HashMap } from "tstl";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { CommentFactory } from "typia/lib/factories/CommentFactory";
|
|
6
|
+
|
|
7
|
+
import { IController } from "../structures/IController";
|
|
8
|
+
import { IErrorReport } from "../structures/IErrorReport";
|
|
9
|
+
import { INestiaProject } from "../structures/INestiaProject";
|
|
10
|
+
import { IRoute } from "../structures/IRoute";
|
|
11
|
+
import { ITypeTuple } from "../structures/ITypeTuple";
|
|
12
|
+
import { PathUtil } from "../utils/PathUtil";
|
|
13
|
+
import { ExceptionAnalyzer } from "./ExceptionAnalyzer";
|
|
14
|
+
import { GenericAnalyzer } from "./GenericAnalyzer";
|
|
15
|
+
import { ImportAnalyzer } from "./ImportAnalyzer";
|
|
16
|
+
import { PathAnalyzer } from "./PathAnalyzer";
|
|
17
|
+
import { SecurityAnalyzer } from "./SecurityAnalyzer";
|
|
18
|
+
|
|
19
|
+
export namespace ControllerAnalyzer {
|
|
20
|
+
export const analyze =
|
|
21
|
+
(project: INestiaProject) =>
|
|
22
|
+
async (
|
|
23
|
+
sourceFile: ts.SourceFile,
|
|
24
|
+
controller: IController,
|
|
25
|
+
): Promise<IRoute[]> => {
|
|
26
|
+
// FIND CONTROLLER CLASS
|
|
27
|
+
const ret: IRoute[] = [];
|
|
28
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
29
|
+
if (
|
|
30
|
+
ts.isClassDeclaration(node) &&
|
|
31
|
+
node.name?.escapedText === controller.name
|
|
32
|
+
) {
|
|
33
|
+
// ANALYZE THE CONTROLLER
|
|
34
|
+
ret.push(..._Analyze_controller(project)(controller, node));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return ret;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/* ---------------------------------------------------------
|
|
42
|
+
CLASS
|
|
43
|
+
--------------------------------------------------------- */
|
|
44
|
+
const _Analyze_controller =
|
|
45
|
+
(project: INestiaProject) =>
|
|
46
|
+
(controller: IController, classNode: ts.ClassDeclaration): IRoute[] => {
|
|
47
|
+
const classType: ts.InterfaceType = project.checker.getTypeAtLocation(
|
|
48
|
+
classNode,
|
|
49
|
+
) as ts.InterfaceType;
|
|
50
|
+
const genericDict: GenericAnalyzer.Dictionary = GenericAnalyzer.analyze(
|
|
51
|
+
project.checker,
|
|
52
|
+
classNode,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const ret: IRoute[] = [];
|
|
56
|
+
for (const property of classType.getProperties()) {
|
|
57
|
+
// GET METHOD DECLARATION
|
|
58
|
+
const declaration: ts.Declaration | undefined =
|
|
59
|
+
(property.declarations || [])[0];
|
|
60
|
+
if (!declaration || !ts.isMethodDeclaration(declaration)) continue;
|
|
61
|
+
|
|
62
|
+
// IDENTIFIER MUST BE
|
|
63
|
+
const identifier = declaration.name;
|
|
64
|
+
if (!ts.isIdentifier(identifier)) continue;
|
|
65
|
+
|
|
66
|
+
// ANALYZED WITH THE REFLECTED-FUNCTION
|
|
67
|
+
const runtime: IController.IFunction | undefined =
|
|
68
|
+
controller.functions.find((f) => f.name === identifier.escapedText);
|
|
69
|
+
if (runtime === undefined) continue;
|
|
70
|
+
|
|
71
|
+
const routes: IRoute[] = _Analyze_function(project)(
|
|
72
|
+
controller,
|
|
73
|
+
genericDict,
|
|
74
|
+
runtime,
|
|
75
|
+
declaration,
|
|
76
|
+
property,
|
|
77
|
+
);
|
|
78
|
+
ret.push(...routes);
|
|
79
|
+
}
|
|
80
|
+
return ret;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/* ---------------------------------------------------------
|
|
84
|
+
FUNCTION
|
|
85
|
+
--------------------------------------------------------- */
|
|
86
|
+
const _Analyze_function =
|
|
87
|
+
(project: INestiaProject) =>
|
|
88
|
+
(
|
|
89
|
+
controller: IController,
|
|
90
|
+
genericDict: GenericAnalyzer.Dictionary,
|
|
91
|
+
func: IController.IFunction,
|
|
92
|
+
declaration: ts.MethodDeclaration,
|
|
93
|
+
symbol: ts.Symbol,
|
|
94
|
+
): IRoute[] => {
|
|
95
|
+
// PREPARE ASSETS
|
|
96
|
+
const type: ts.Type = project.checker.getTypeOfSymbolAtLocation(
|
|
97
|
+
symbol,
|
|
98
|
+
symbol.valueDeclaration!,
|
|
99
|
+
);
|
|
100
|
+
const signature: ts.Signature | undefined =
|
|
101
|
+
project.checker.getSignaturesOfType(type, ts.SignatureKind.Call)[0];
|
|
102
|
+
if (signature === undefined) {
|
|
103
|
+
project.errors.push({
|
|
104
|
+
file: controller.file,
|
|
105
|
+
controller: controller.name,
|
|
106
|
+
function: func.name,
|
|
107
|
+
message: "unable to get the type signature.",
|
|
108
|
+
});
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// SKIP @IGNORE TAG
|
|
113
|
+
const jsDocTags = signature.getJsDocTags();
|
|
114
|
+
if (jsDocTags.some((tag) => tag.name === "ignore")) return [];
|
|
115
|
+
|
|
116
|
+
// EXPLORE CHILDREN TYPES
|
|
117
|
+
const importDict: ImportAnalyzer.Dictionary = new HashMap();
|
|
118
|
+
const parameters: Array<IRoute.IParameter | null> = func.parameters.map(
|
|
119
|
+
(param) =>
|
|
120
|
+
_Analyze_parameter(project)(
|
|
121
|
+
genericDict,
|
|
122
|
+
importDict,
|
|
123
|
+
controller,
|
|
124
|
+
func.name,
|
|
125
|
+
param,
|
|
126
|
+
signature.getParameters()[param.index],
|
|
127
|
+
)!,
|
|
128
|
+
);
|
|
129
|
+
const outputType: ITypeTuple | null = ImportAnalyzer.analyze(
|
|
130
|
+
project.checker,
|
|
131
|
+
genericDict,
|
|
132
|
+
importDict,
|
|
133
|
+
signature.getReturnType(),
|
|
134
|
+
);
|
|
135
|
+
if (
|
|
136
|
+
outputType === null ||
|
|
137
|
+
(project.config.clone !== true &&
|
|
138
|
+
(outputType.typeName === "__type" ||
|
|
139
|
+
outputType.typeName === "__object"))
|
|
140
|
+
) {
|
|
141
|
+
project.errors.push({
|
|
142
|
+
file: controller.file,
|
|
143
|
+
controller: controller.name,
|
|
144
|
+
function: func.name,
|
|
145
|
+
message: "implicit (unnamed) return type.",
|
|
146
|
+
});
|
|
147
|
+
return [];
|
|
148
|
+
} else if (
|
|
149
|
+
func.method === "HEAD" &&
|
|
150
|
+
outputType.typeName !== "void" &&
|
|
151
|
+
outputType.typeName !== "undefined"
|
|
152
|
+
) {
|
|
153
|
+
project.errors.push({
|
|
154
|
+
file: controller.file,
|
|
155
|
+
controller: controller.name,
|
|
156
|
+
function: func.name,
|
|
157
|
+
message: `HEAD method must return void type.`,
|
|
158
|
+
});
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const exceptions = ExceptionAnalyzer.analyze(project)(
|
|
163
|
+
genericDict,
|
|
164
|
+
project.config.propagate === true ? importDict : new HashMap(),
|
|
165
|
+
)(
|
|
166
|
+
controller,
|
|
167
|
+
func,
|
|
168
|
+
)(declaration);
|
|
169
|
+
const imports: [string, string[]][] = importDict
|
|
170
|
+
.toJSON()
|
|
171
|
+
.map((pair) => [pair.first, pair.second.toJSON()]);
|
|
172
|
+
|
|
173
|
+
// CONSIDER SECURITY TAGS
|
|
174
|
+
const security: Record<string, string[]>[] = SecurityAnalyzer.merge(
|
|
175
|
+
...controller.security,
|
|
176
|
+
...func.security,
|
|
177
|
+
...jsDocTags
|
|
178
|
+
.filter((tag) => tag.name === "security")
|
|
179
|
+
.map((tag) =>
|
|
180
|
+
tag.text === undefined
|
|
181
|
+
? [{}]
|
|
182
|
+
: tag.text.map((text) => {
|
|
183
|
+
const line: string[] = text.text
|
|
184
|
+
.split(" ")
|
|
185
|
+
.filter((s) => s.trim())
|
|
186
|
+
.filter((s) => !!s.length);
|
|
187
|
+
if (line.length === 0) return {};
|
|
188
|
+
return {
|
|
189
|
+
[line[0]]: line.slice(1),
|
|
190
|
+
};
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
.flat(),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// CONSTRUCT COMMON DATA
|
|
197
|
+
const common: Omit<IRoute, "path" | "accessors"> = {
|
|
198
|
+
...func,
|
|
199
|
+
controller: controller.target,
|
|
200
|
+
parameters: parameters.filter((p) => p !== null) as IRoute.IParameter[],
|
|
201
|
+
output: {
|
|
202
|
+
type: outputType.type,
|
|
203
|
+
typeName: outputType.typeName,
|
|
204
|
+
contentType: func.contentType,
|
|
205
|
+
},
|
|
206
|
+
imports,
|
|
207
|
+
status: func.status,
|
|
208
|
+
target: {
|
|
209
|
+
class: controller.target,
|
|
210
|
+
function: func.target,
|
|
211
|
+
},
|
|
212
|
+
location: (() => {
|
|
213
|
+
const file = declaration.getSourceFile();
|
|
214
|
+
const { line, character } = file.getLineAndCharacterOfPosition(
|
|
215
|
+
declaration.pos,
|
|
216
|
+
);
|
|
217
|
+
return `${path.relative(process.cwd(), file.fileName)}:${line + 1}:${
|
|
218
|
+
character + 1
|
|
219
|
+
}`;
|
|
220
|
+
})(),
|
|
221
|
+
description: CommentFactory.description(symbol),
|
|
222
|
+
operationId: jsDocTags
|
|
223
|
+
.find(({ name }) => name === "operationId")
|
|
224
|
+
?.text?.[0].text.split(" ")[0]
|
|
225
|
+
.trim(),
|
|
226
|
+
jsDocTags: jsDocTags,
|
|
227
|
+
setHeaders: jsDocTags
|
|
228
|
+
.filter(
|
|
229
|
+
(t) =>
|
|
230
|
+
t.text?.length &&
|
|
231
|
+
t.text[0].text &&
|
|
232
|
+
(t.name === "setHeader" || t.name === "assignHeaders"),
|
|
233
|
+
)
|
|
234
|
+
.map((t) =>
|
|
235
|
+
t.name === "setHeader"
|
|
236
|
+
? {
|
|
237
|
+
type: "setter",
|
|
238
|
+
source: t.text![0].text.split(" ")[0].trim(),
|
|
239
|
+
target: t.text![0].text.split(" ")[1]?.trim(),
|
|
240
|
+
}
|
|
241
|
+
: {
|
|
242
|
+
type: "assigner",
|
|
243
|
+
source: t.text![0].text,
|
|
244
|
+
},
|
|
245
|
+
),
|
|
246
|
+
security,
|
|
247
|
+
exceptions,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// CONFIGURE PATHS
|
|
251
|
+
const pathList: Set<string> = new Set();
|
|
252
|
+
const versions: Array<string | null> = _Analyze_versions(
|
|
253
|
+
project.input.versioning === undefined
|
|
254
|
+
? undefined
|
|
255
|
+
: func.versions ??
|
|
256
|
+
controller.versions ??
|
|
257
|
+
(project.input.versioning?.defaultVersion !== undefined
|
|
258
|
+
? Array.isArray(project.input.versioning?.defaultVersion)
|
|
259
|
+
? project.input.versioning?.defaultVersion
|
|
260
|
+
: [project.input.versioning?.defaultVersion]
|
|
261
|
+
: undefined) ??
|
|
262
|
+
undefined,
|
|
263
|
+
);
|
|
264
|
+
for (const prefix of controller.prefixes)
|
|
265
|
+
for (const cPath of controller.paths)
|
|
266
|
+
for (const filePath of func.paths)
|
|
267
|
+
pathList.add(PathAnalyzer.join(prefix, cPath, filePath));
|
|
268
|
+
|
|
269
|
+
return [...pathList]
|
|
270
|
+
.map((individual) =>
|
|
271
|
+
PathAnalyzer.combinate(project.input.globalPrefix)(
|
|
272
|
+
[...versions].map((v) =>
|
|
273
|
+
v === null
|
|
274
|
+
? null
|
|
275
|
+
: project.input.versioning?.prefix?.length
|
|
276
|
+
? `${project.input.versioning.prefix}${v}`
|
|
277
|
+
: v,
|
|
278
|
+
),
|
|
279
|
+
)({
|
|
280
|
+
method: func.method,
|
|
281
|
+
path: individual,
|
|
282
|
+
}),
|
|
283
|
+
)
|
|
284
|
+
.flat()
|
|
285
|
+
.filter((path) => {
|
|
286
|
+
const escaped: string | null = PathAnalyzer.escape(path);
|
|
287
|
+
if (escaped === null)
|
|
288
|
+
project.errors.push({
|
|
289
|
+
file: controller.file,
|
|
290
|
+
controller: controller.name,
|
|
291
|
+
function: func.name,
|
|
292
|
+
message: `unable to escape the path "${path}".`,
|
|
293
|
+
});
|
|
294
|
+
return escaped !== null;
|
|
295
|
+
})
|
|
296
|
+
.map((path) => ({
|
|
297
|
+
...common,
|
|
298
|
+
path: PathAnalyzer.escape(path)!,
|
|
299
|
+
accessors: [...PathUtil.accessors(path), func.name],
|
|
300
|
+
}));
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const _Analyze_parameter =
|
|
304
|
+
(project: INestiaProject) =>
|
|
305
|
+
(
|
|
306
|
+
genericDict: GenericAnalyzer.Dictionary,
|
|
307
|
+
importDict: ImportAnalyzer.Dictionary,
|
|
308
|
+
controller: IController,
|
|
309
|
+
funcName: string,
|
|
310
|
+
param: IController.IParameter,
|
|
311
|
+
symbol: ts.Symbol,
|
|
312
|
+
): IRoute.IParameter | null => {
|
|
313
|
+
const type: ts.Type = project.checker.getTypeOfSymbolAtLocation(
|
|
314
|
+
symbol,
|
|
315
|
+
symbol.valueDeclaration!,
|
|
316
|
+
);
|
|
317
|
+
const name: string = symbol.getEscapedName().toString();
|
|
318
|
+
|
|
319
|
+
const optional: boolean = !!project.checker.symbolToParameterDeclaration(
|
|
320
|
+
symbol,
|
|
321
|
+
undefined,
|
|
322
|
+
undefined,
|
|
323
|
+
)?.questionToken;
|
|
324
|
+
|
|
325
|
+
const errors: IErrorReport[] = [];
|
|
326
|
+
|
|
327
|
+
// DO NOT SUPPORT BODY PARAMETER
|
|
328
|
+
if (param.category === "body" && param.field !== undefined)
|
|
329
|
+
errors.push({
|
|
330
|
+
file: controller.file,
|
|
331
|
+
controller: controller.name,
|
|
332
|
+
function: funcName,
|
|
333
|
+
message:
|
|
334
|
+
`nestia does not support body field specification. ` +
|
|
335
|
+
`Therefore, erase the "${name}" parameter and ` +
|
|
336
|
+
`re-define a new body decorator accepting full structured message.`,
|
|
337
|
+
});
|
|
338
|
+
if (optional === true && param.category !== "query")
|
|
339
|
+
errors.push({
|
|
340
|
+
file: controller.file,
|
|
341
|
+
controller: controller.name,
|
|
342
|
+
function: funcName,
|
|
343
|
+
message:
|
|
344
|
+
`nestia does not support optional parameter except query parameter. ` +
|
|
345
|
+
`Therefore, erase question mark on the "${name}" parameter, ` +
|
|
346
|
+
`or re-define a new method without the "${name}" parameter.`,
|
|
347
|
+
});
|
|
348
|
+
if (
|
|
349
|
+
optional === true &&
|
|
350
|
+
param.category === "query" &&
|
|
351
|
+
param.field === undefined
|
|
352
|
+
)
|
|
353
|
+
errors.push({
|
|
354
|
+
file: controller.file,
|
|
355
|
+
controller: controller.name,
|
|
356
|
+
function: funcName,
|
|
357
|
+
message:
|
|
358
|
+
`nestia does not support optional query parameter without field specification. ` +
|
|
359
|
+
`Therefore, erase question mark on the "${name}" parameter, ` +
|
|
360
|
+
`or re-define re-define parameters for each query parameters.`,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// GET TYPE NAME
|
|
364
|
+
const tuple: ITypeTuple | null = ImportAnalyzer.analyze(
|
|
365
|
+
project.checker,
|
|
366
|
+
genericDict,
|
|
367
|
+
importDict,
|
|
368
|
+
type,
|
|
369
|
+
);
|
|
370
|
+
if (
|
|
371
|
+
tuple === null ||
|
|
372
|
+
(project.config.clone !== true &&
|
|
373
|
+
(tuple.typeName === "__type" || tuple.typeName === "__object"))
|
|
374
|
+
)
|
|
375
|
+
errors.push({
|
|
376
|
+
file: controller.file,
|
|
377
|
+
controller: controller.name,
|
|
378
|
+
function: funcName,
|
|
379
|
+
message: `implicit (unnamed) parameter type from "${name}".`,
|
|
380
|
+
});
|
|
381
|
+
if (errors.length) {
|
|
382
|
+
project.errors.push(...errors);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
...param,
|
|
387
|
+
name,
|
|
388
|
+
optional,
|
|
389
|
+
type: tuple!.type,
|
|
390
|
+
typeName: tuple!.typeName,
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
function _Analyze_versions(
|
|
395
|
+
value:
|
|
396
|
+
| Array<Exclude<VersionValue, Array<string | typeof VERSION_NEUTRAL>>>
|
|
397
|
+
| undefined,
|
|
398
|
+
): Array<string | null> {
|
|
399
|
+
if (value === undefined) return [null];
|
|
400
|
+
return value.map((v) => (typeof v === "symbol" ? null : v));
|
|
401
|
+
}
|
|
402
|
+
}
|