@nestia/sdk 2.3.0-dev.20231019 → 2.3.0

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