diff --git a/benchmark/package.json b/benchmark/package.json index b86f313d8..ac66963d0 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -24,8 +24,8 @@ }, "homepage": "https://nestia.io", "dependencies": { - "@nestia/core": "^1.6.3", - "typia": "^4.2.1" + "@nestia/core": "^1.6.4", + "typia": "^4.2.3" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 57f41a5a1..3d017d26b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,8 +36,8 @@ "inquirer": "^8.2.5" }, "devDependencies": { - "@nestia/core": "^1.6.3", - "@nestia/sdk": "^1.6.3", + "@nestia/core": "^1.6.4", + "@nestia/sdk": "^1.6.4", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/inquirer": "^9.0.3", "@types/node": "^18.11.16", diff --git a/packages/core/package.json b/packages/core/package.json index bfe106b70..5416fd355 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/core", - "version": "1.6.3", + "version": "1.6.4", "description": "Super-fast validation decorators of NestJS", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -34,7 +34,7 @@ }, "homepage": "https://nestia.io", "dependencies": { - "@nestia/fetcher": "^1.6.3", + "@nestia/fetcher": "^1.6.4", "@nestjs/common": ">= 7.0.1", "@nestjs/core": ">= 7.0.1", "@nestjs/platform-express": ">= 7.0.1", @@ -44,10 +44,10 @@ "raw-body": ">= 2.0.0", "reflect-metadata": ">= 0.1.12", "rxjs": ">= 6.0.0", - "typia": "^4.2.1" + "typia": "^4.2.3" }, "peerDependencies": { - "@nestia/fetcher": ">= 1.6.3", + "@nestia/fetcher": ">= 1.6.4", "@nestjs/common": ">= 7.0.1", "@nestjs/core": ">= 7.0.1", "@nestjs/platform-express": ">= 7.0.1", @@ -56,7 +56,7 @@ "reflect-metadata": ">= 0.1.12", "rxjs": ">= 6.0.0", "typescript": ">= 4.8.0", - "typia": ">= 4.2.1" + "typia": ">= 4.2.3" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.0.0", diff --git a/packages/core/src/decorators/TypedException.ts b/packages/core/src/decorators/TypedException.ts new file mode 100644 index 000000000..c63ddeed6 --- /dev/null +++ b/packages/core/src/decorators/TypedException.ts @@ -0,0 +1,90 @@ +import "reflect-metadata"; + +/** + * > You must configure the generic argument `T` + * + * Exception decorator. + * + * `TypedException` is a decorator function describing HTTP exception and its type + * which could be occured in the method. + * + * For reference, this decorator function does not affect to the method's behavior, + * but only affects to the swagger documents generation. Also, it does not affect to + * the SDK library generation yet, but will be used in the future. + * + * @param status Status number or pattern like "2XX", "3XX", "4XX", "5XX" + * @param description Description about the exception + * @returns Method decorator + * + * @author Jeongho Nam - https://github.com/samchon + * @deprecated + */ +export function TypedException( + status: number | "2XX" | "3XX" | "4XX" | "5XX", + description?: string | undefined, +): never; + +/** + * Exception decorator. + * + * `TypedException` is a decorator function describing HTTP exception and its type + * which could be occured in the method. + * + * For reference, this decorator function does not affect to the method's behavior, + * but only affects to the swagger documents generation. Also, it does not affect to + * the SDK library generation yet, but will be used in the future. + * + * @template T Type of the exception + * @param status Status number or pattern like "2XX", "3XX", "4XX", "5XX" + * @param description Description about the exception + * @returns Method decorator + * + * @author Jeongho Nam - https://github.com/samchon + */ +export function TypedException( + status: number | "2XX" | "3XX" | "4XX" | "5XX", + description?: string | undefined, +): MethodDecorator; + +/** + * @internal + */ +export function TypedException( + status: number | "2XX" | "3XX" | "4XX" | "5XX", + description?: string | undefined, + type?: string | undefined, +): MethodDecorator { + return function TypedException( + target: Object | T, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, + ) { + const array: IProps[] = (() => { + const oldbie: IProps[] | undefined = Reflect.getMetadata( + `swagger/TypedException`, + (target as any)[propertyKey], + ); + if (oldbie !== undefined) return oldbie; + + const newbie: IProps[] = []; + Reflect.defineMetadata( + `swagger/TypedException`, + newbie, + (target as any)[propertyKey], + ); + return newbie; + })(); + array.push({ + status, + description, + type: type!, + }); + return descriptor; + }; +} + +interface IProps { + status: number | "2XX" | "3XX" | "4XX" | "5XX"; + description?: string | undefined; + type: string; +} diff --git a/packages/core/src/module.ts b/packages/core/src/module.ts index a150097b8..8145c9046 100644 --- a/packages/core/src/module.ts +++ b/packages/core/src/module.ts @@ -6,6 +6,7 @@ export * from "./decorators/EncryptedRoute"; export * from "./utils/ExceptionManager"; export * from "./decorators/PlainBody"; export * from "./decorators/TypedBody"; +export * from "./decorators/TypedException"; export * from "./decorators/TypedHeaders"; export * from "./decorators/TypedParam"; export * from "./decorators/TypedRoute"; diff --git a/packages/core/src/programmers/TypedExceptionProgrammer.ts b/packages/core/src/programmers/TypedExceptionProgrammer.ts new file mode 100644 index 000000000..0b996d0af --- /dev/null +++ b/packages/core/src/programmers/TypedExceptionProgrammer.ts @@ -0,0 +1,40 @@ +import ts from "typescript"; + +import { INestiaTransformProject } from "../options/INestiaTransformProject"; + +export namespace TypedExceptionProgrammer { + export const generate = + ({ checker }: INestiaTransformProject) => + (expression: ts.CallExpression): ts.CallExpression => { + // CHECK GENERIC ARGUMENT EXISTENCE + if (!expression.typeArguments?.[0]) throw new Error(NOT_SPECIFIED); + + // GET TYPE INFO + const node: ts.TypeNode = expression.typeArguments[0]; + const type: ts.Type = checker.getTypeFromTypeNode(node); + + if (type.isTypeParameter()) throw new Error(NO_GENERIC_ARGUMENT); + + // CHECK DUPLICATED TRNASFORMATION + if (expression.arguments.length === 3) return expression; + + // DO TRANSFORM + const name: string = node.getFullText().trim(); + return ts.factory.updateCallExpression( + expression, + expression.expression, + expression.typeArguments, + [ + expression.arguments[0], + expression.arguments[1] ?? + ts.factory.createIdentifier("undefined"), + ts.factory.createStringLiteral(name), + ], + ); + }; +} + +const NOT_SPECIFIED = + "Error on @nestia.core.TypedException(): generic argument is not specified."; +const NO_GENERIC_ARGUMENT = + "Error on @nestia.core.TypedException(): non-specified generic argument."; diff --git a/packages/core/src/transformers/MethodTransformer.ts b/packages/core/src/transformers/MethodTransformer.ts index bab264847..3bfedf8d4 100644 --- a/packages/core/src/transformers/MethodTransformer.ts +++ b/packages/core/src/transformers/MethodTransformer.ts @@ -1,7 +1,8 @@ import ts from "typescript"; import { INestiaTransformProject } from "../options/INestiaTransformProject"; -import { MethodDecoratorTransformer } from "./MethodDecoratorTransformer"; +import { TypedExceptionTransformer } from "./TypedExceptionTransformer"; +import { TypedRouteTransformer } from "./TypedRouteTransformer"; export namespace MethodTransformer { export const transform = @@ -23,15 +24,16 @@ export namespace MethodTransformer { if (escaped === undefined) return method; + const operator = (deco: ts.Decorator): ts.Decorator => { + deco = TypedExceptionTransformer.transform(project)(deco); + deco = TypedRouteTransformer.transform(project)(escaped)(deco); + return deco; + }; if (ts.getDecorators !== undefined) return ts.factory.updateMethodDeclaration( method, (method.modifiers || []).map((mod) => - ts.isDecorator(mod) - ? MethodDecoratorTransformer.transform(project)( - escaped, - )(mod) - : mod, + ts.isDecorator(mod) ? operator(mod) : mod, ), method.asteriskToken, method.name, @@ -44,11 +46,7 @@ export namespace MethodTransformer { // eslint-disable-next-line return (ts.factory.updateMethodDeclaration as any)( method, - decorators.map((deco) => - MethodDecoratorTransformer.transform(project)(escaped)( - deco, - ), - ), + decorators.map(operator), (method as any).modifiers, method.asteriskToken, method.name, diff --git a/packages/core/src/transformers/TypedExceptionTransformer.ts b/packages/core/src/transformers/TypedExceptionTransformer.ts new file mode 100644 index 000000000..558b043b1 --- /dev/null +++ b/packages/core/src/transformers/TypedExceptionTransformer.ts @@ -0,0 +1,51 @@ +import path from "path"; +import ts from "typescript"; + +import { INestiaTransformProject } from "../options/INestiaTransformProject"; +import { TypedExceptionProgrammer } from "../programmers/TypedExceptionProgrammer"; + +export namespace TypedExceptionTransformer { + export const transform = + (project: INestiaTransformProject) => + (decorator: ts.Decorator): ts.Decorator => { + if (!ts.isCallExpression(decorator.expression)) return decorator; + + // CHECK SIGNATURE + const signature: ts.Signature | undefined = + project.checker.getResolvedSignature(decorator.expression); + if (!signature || !signature.declaration) return decorator; + + // CHECK TO BE TRANSFORMED + const done: boolean = (() => { + // CHECK FILENAME + const location: string = path.resolve( + signature.declaration.getSourceFile().fileName, + ); + if (location.indexOf(LIB_PATH) === -1 && location !== SRC_PATH) + return false; + + // CHECK DUPLICATED + return decorator.expression.arguments.length !== 3; + })(); + if (done === false) return decorator; + + // DO TRANSFORM + return ts.factory.createDecorator( + TypedExceptionProgrammer.generate(project)( + decorator.expression, + ), + ); + }; + + const LIB_PATH = path.join( + "node_modules", + "@nestia", + "core", + "lib", + "decorators", + `TypedException.d.ts`, + ); + const SRC_PATH = path.resolve( + path.join(__dirname, "..", "decorators", `TypedException.ts`), + ); +} diff --git a/packages/core/src/transformers/MethodDecoratorTransformer.ts b/packages/core/src/transformers/TypedRouteTransformer.ts similarity index 98% rename from packages/core/src/transformers/MethodDecoratorTransformer.ts rename to packages/core/src/transformers/TypedRouteTransformer.ts index a7a1b776c..34534e780 100644 --- a/packages/core/src/transformers/MethodDecoratorTransformer.ts +++ b/packages/core/src/transformers/TypedRouteTransformer.ts @@ -4,7 +4,7 @@ import ts from "typescript"; import { INestiaTransformProject } from "../options/INestiaTransformProject"; import { TypedRouteProgrammer } from "../programmers/TypedRouteProgrammer"; -export namespace MethodDecoratorTransformer { +export namespace TypedRouteTransformer { export const transform = (project: INestiaTransformProject) => (type: ts.Type) => diff --git a/packages/fetcher/package.json b/packages/fetcher/package.json index f6b4103be..3276f2adb 100644 --- a/packages/fetcher/package.json +++ b/packages/fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/fetcher", - "version": "1.6.3", + "version": "1.6.4", "description": "Fetcher library of Nestia SDK", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/fetcher/src/HttpError.ts b/packages/fetcher/src/HttpError.ts index 5a02c2dab..28ecc44a0 100644 --- a/packages/fetcher/src/HttpError.ts +++ b/packages/fetcher/src/HttpError.ts @@ -53,7 +53,12 @@ export class HttpError extends Error { * @returns JSON object of the `HttpError`. */ public toJSON(): HttpError.IProps { - if (this.body_ === NOT_YET) this.body_ = JSON.parse(this.message); + if (this.body_ === NOT_YET) + try { + this.body_ = JSON.parse(this.message); + } catch { + this.body_ = this.message; + } return { method: this.method, path: this.path, diff --git a/packages/migrate/assets/input/fireblocks.json b/packages/migrate/assets/input/fireblocks.json index c35bafd28..32ebdb19b 100644 --- a/packages/migrate/assets/input/fireblocks.json +++ b/packages/migrate/assets/input/fireblocks.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Fireblocks API", - "version": "1.6.3", + "version": "1.0.0", "contact": { "email": "support@fireblocks.com" } diff --git a/packages/migrate/package.json b/packages/migrate/package.json index edbcc9a17..ce0b9919f 100644 --- a/packages/migrate/package.json +++ b/packages/migrate/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/migrate", - "version": "0.2.10", + "version": "0.2.11", "description": "Migration program from swagger to NestJS", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -30,8 +30,8 @@ }, "homepage": "https://github.com/samchon/nestia#readme", "devDependencies": { - "@nestia/core": "^1.6.3", - "@nestia/fetcher": "^1.6.3", + "@nestia/core": "^1.6.4", + "@nestia/fetcher": "^1.6.4", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/node": "^20.3.3", "prettier": "^2.8.8", @@ -45,7 +45,7 @@ "typescript-transform-paths": "^3.4.6" }, "dependencies": { - "typia": "^4.2.1" + "typia": "^4.2.3" }, "files": [ "lib", diff --git a/packages/migrate/src/programmers/RouteProgrammer.ts b/packages/migrate/src/programmers/RouteProgrammer.ts index deb49629f..e35fd0347 100644 --- a/packages/migrate/src/programmers/RouteProgrammer.ts +++ b/packages/migrate/src/programmers/RouteProgrammer.ts @@ -17,10 +17,10 @@ export namespace RouteProgrammer { const body = emplaceBodySchema(emplaceReference(swagger)("body"))( route.requestBody, ); - const response = emplaceBodySchema( + const success = emplaceBodySchema( emplaceReference(swagger)("response"), )(route.responses?.["201"] ?? route.responses?.["200"]); - if (body === false || response === false) { + if (body === false || success === false) { console.log( `Failed to migrate ${props.method.toUpperCase()} ${ props.path @@ -203,7 +203,25 @@ export namespace RouteProgrammer { })), query, body, - response, + success, + exceptions: Object.fromEntries( + Object.entries(route.responses ?? {}) + .filter( + ([key, value]) => + key !== "200" && + key !== "201" && + !!value.content?.["application/json"], + ) + .map(([key, value]) => [ + key, + { + description: value.description, + schema: + value.content?.["application/json"] + ?.schema ?? {}, + }, + ]), + ), description: describe(route), "x-nestia-jsDocTags": route["x-nestia-jsDocTags"], }; @@ -308,9 +326,9 @@ export namespace RouteProgrammer { (components: ISwaggerComponents) => (references: ISwaggerSchema.IReference[]) => (route: IMigrateRoute): string => { - const output: string = route.response + const output: string = route.success ? SchemaProgrammer.write(components)(references)( - route.response.schema, + route.success.schema, ) : "void"; @@ -319,13 +337,13 @@ export namespace RouteProgrammer { StringUtil.capitalize(route.method), )}(${JSON.stringify(route.path)})`; const decorator: string[] = - route.response?.["x-nestia-encrypted"] === true + route.success?.["x-nestia-encrypted"] === true ? [ `@${importer("@nestia/core")( "EncryptedRoute", )}.${methoder((str) => str)}`, ] - : route.response?.type === "text/plain" + : route.success?.type === "text/plain" ? [ `@${importer("@nestjs/common")( "Header", @@ -345,6 +363,17 @@ export namespace RouteProgrammer { "TypedRoute", )}.${methoder((str) => str)}`, ]; + for (const [key, value] of Object.entries(route.exceptions ?? {})) + decorator.push( + `@${importer("@nestia/core")( + "TypedException", + )}<${SchemaProgrammer.write(components)(references)( + value.schema, + )}>(${ + isNaN(Number(key)) ? JSON.stringify(key) : key + }, ${JSON.stringify(value.description)})`, + ); + const content: string[] = [ ...(route.description ? [ diff --git a/packages/migrate/src/structures/IMigrateRoute.ts b/packages/migrate/src/structures/IMigrateRoute.ts index abca61da0..547450ae2 100644 --- a/packages/migrate/src/structures/IMigrateRoute.ts +++ b/packages/migrate/src/structures/IMigrateRoute.ts @@ -10,7 +10,8 @@ export interface IMigrateRoute { headers: ISwaggerSchema | null; query: ISwaggerSchema | null; body: IMigrateRoute.IBody | null; - response: IMigrateRoute.IBody | null; + success: IMigrateRoute.IBody | null; + exceptions: Record; description?: string; "x-nestia-jsDocTags"?: IJsDocTagInfo[]; } @@ -25,4 +26,8 @@ export namespace IMigrateRoute { schema: ISwaggerSchema; "x-nestia-encrypted"?: boolean; } + export interface IException { + description?: string; + schema: ISwaggerSchema; + } } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 871a739d2..a83dcf673 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/sdk", - "version": "1.6.3", + "version": "1.6.4", "description": "Nestia SDK and Swagger generator", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -35,7 +35,7 @@ }, "homepage": "https://nestia.io", "dependencies": { - "@nestia/fetcher": "^1.6.3", + "@nestia/fetcher": "^1.6.4", "cli": "^1.0.1", "glob": "^7.2.0", "path-to-regexp": "^6.2.1", @@ -44,16 +44,16 @@ "tsconfck": "^2.0.1", "tsconfig-paths": "^4.1.1", "tstl": "^2.5.13", - "typia": "^4.2.1" + "typia": "^4.2.3" }, "peerDependencies": { - "@nestia/fetcher": ">= 1.6.3", + "@nestia/fetcher": ">= 1.6.4", "@nestjs/common": ">= 7.0.1", "@nestjs/core": ">= 7.0.1", "reflect-metadata": ">= 0.1.12", "ts-node": ">= 10.6.0", "typescript": ">= 4.8.0", - "typia": ">= 4.2.1" + "typia": ">= 4.2.3" }, "devDependencies": { "@nestjs/common": ">= 7.0.1", diff --git a/packages/sdk/src/analyses/ControllerAnalyzer.ts b/packages/sdk/src/analyses/ControllerAnalyzer.ts index 17a14a3a4..49886638f 100644 --- a/packages/sdk/src/analyses/ControllerAnalyzer.ts +++ b/packages/sdk/src/analyses/ControllerAnalyzer.ts @@ -8,6 +8,7 @@ import { IController } from "../structures/IController"; import { IRoute } from "../structures/IRoute"; import { ITypeTuple } from "../structures/ITypeTuple"; import { PathUtil } from "../utils/PathUtil"; +import { ExceptionAnalyzer } from "./ExceptionAnalyzer"; import { GenericAnalyzer } from "./GenericAnalyzer"; import { ImportAnalyzer } from "./ImportAnalyzer"; import { PathAnalyzer } from "./PathAnalyzer"; @@ -89,7 +90,7 @@ export namespace ControllerAnalyzer { controller: IController, genericDict: GenericAnalyzer.Dictionary, func: IController.IFunction, - declaration: ts.Declaration, + declaration: ts.MethodDeclaration, symbol: ts.Symbol, ): IRoute[] { // PREPARE ASSETS @@ -101,7 +102,6 @@ export namespace ControllerAnalyzer { type, ts.SignatureKind.Call, )[0]; - if (signature === undefined) throw new Error( `Error on ControllerAnalyzer.analyze(): unable to get the signature from the ${controller.name}.${func.name}().`, @@ -206,6 +206,10 @@ export namespace ControllerAnalyzer { }, ), security, + exceptions: ExceptionAnalyzer.analyze(checker)( + genericDict, + importDict, + )(func)(declaration), }; // CONFIGURE PATHS @@ -232,9 +236,6 @@ export namespace ControllerAnalyzer { })); } - /* --------------------------------------------------------- - PARAMETER - --------------------------------------------------------- */ function _Analyze_parameter( checker: ts.TypeChecker, genericDict: GenericAnalyzer.Dictionary, diff --git a/packages/sdk/src/analyses/ExceptionAnalyzer.ts b/packages/sdk/src/analyses/ExceptionAnalyzer.ts new file mode 100644 index 000000000..5099a8d5f --- /dev/null +++ b/packages/sdk/src/analyses/ExceptionAnalyzer.ts @@ -0,0 +1,102 @@ +import path from "path"; +import ts from "typescript"; + +import { IController } from "../structures/IController"; +import { IRoute } from "../structures/IRoute"; +import { ITypeTuple } from "../structures/ITypeTuple"; +import { GenericAnalyzer } from "./GenericAnalyzer"; +import { ImportAnalyzer } from "./ImportAnalyzer"; + +export namespace ExceptionAnalyzer { + export const analyze = + (checker: ts.TypeChecker) => + ( + genericDict: GenericAnalyzer.Dictionary, + importDict: ImportAnalyzer.Dictionary, + ) => + (func: IController.IFunction) => + ( + declaration: ts.MethodDeclaration, + ): Record => { + const output: Record< + number | "2XX" | "3XX" | "4XX" | "5XX", + IRoute.IOutput + > = {} as any; + for (const decorator of declaration.modifiers ?? []) + if (ts.isDecorator(decorator)) + analyzeTyped(checker)(genericDict, importDict)(func)( + output, + )(decorator); + return output; + }; + + const analyzeTyped = + (checker: ts.TypeChecker) => + ( + genericDict: GenericAnalyzer.Dictionary, + importDict: ImportAnalyzer.Dictionary, + ) => + (func: IController.IFunction) => + ( + output: Record< + number | "2XX" | "3XX" | "4XX" | "5XX", + IRoute.IOutput + >, + ) => + (decorator: ts.Decorator): boolean => { + // CHECK DECORATOR + if (!ts.isCallExpression(decorator.expression)) return false; + else if ((decorator.expression.typeArguments ?? []).length !== 1) + return false; + + // CHECK SIGNATURE + const signature: ts.Signature | undefined = + checker.getResolvedSignature(decorator.expression); + if (!signature || !signature.declaration) return false; + else if ( + path + .resolve(signature.declaration.getSourceFile().fileName) + .indexOf(TYPED_EXCEPTION_PATH) === -1 + ) + return false; + + // GET TYPE INFO + const node: ts.TypeNode = decorator.expression.typeArguments![0]; + const type: ts.Type = checker.getTypeFromTypeNode(node); + if (type.isTypeParameter()) + throw new Error( + "Error on @nestia.core.TypedException(): non-specified generic argument.", + ); + + const tuple: ITypeTuple | null = ImportAnalyzer.analyze( + checker, + genericDict, + importDict, + type, + ); + if (tuple === null) return false; + + // DO ASSIGN + const matched: IController.IException[] = Object.entries( + func.exceptions, + ) + .filter(([_key, value]) => value.type === tuple.name) + .map(([_key, value]) => value); + for (const m of matched) + output[m.status] = { + ...tuple, + contentType: "application/json", + description: m.description, + }; + return true; + }; +} + +const TYPED_EXCEPTION_PATH = path.join( + "node_modules", + "@nestia", + "core", + "lib", + "decorators", + "TypedException.d.ts", +); diff --git a/packages/sdk/src/analyses/ReflectAnalyzer.ts b/packages/sdk/src/analyses/ReflectAnalyzer.ts index a67028636..e4528e30b 100644 --- a/packages/sdk/src/analyses/ReflectAnalyzer.ts +++ b/packages/sdk/src/analyses/ReflectAnalyzer.ts @@ -89,7 +89,7 @@ export namespace ReflectAnalyzer { name, paths, functions: [], - security: _Get_security(creator), + security: _Get_securities(creator), }; // PARSE CHILDREN DATA @@ -126,12 +126,25 @@ export namespace ReflectAnalyzer { else return value; } - function _Get_security(value: any): Record[] { + function _Get_securities(value: any): Record[] { const entire: Record[] | undefined = Reflect.getMetadata("swagger/apiSecurity", value); return entire ? SecurityAnalyzer.merge(...entire) : []; } + function _Get_exceptions( + value: any, + ): Record { + const entire: IController.IException[] | undefined = + Reflect.getMetadata("swagger/TypedException", value); + return Object.fromEntries( + (entire ?? []).map((exp) => [exp.status, exp]), + ) as Record< + number | "2XX" | "3XX" | "4XX" | "5XX", + IController.IException + >; + } + /* --------------------------------------------------------- FUNCTION --------------------------------------------------------- */ @@ -213,7 +226,8 @@ export namespace ReflectAnalyzer { typeof h?.value === "string" && h.name.toLowerCase() === "content-type", )?.value ?? "application/json", - security: _Get_security(proto), + security: _Get_securities(proto), + exceptions: _Get_exceptions(proto), }; // VALIDATE PATH ARGUMENTS diff --git a/packages/sdk/src/generates/SwaggerGenerator.ts b/packages/sdk/src/generates/SwaggerGenerator.ts index f142e5f24..9e8435301 100644 --- a/packages/sdk/src/generates/SwaggerGenerator.ts +++ b/packages/sdk/src/generates/SwaggerGenerator.ts @@ -1,7 +1,6 @@ import fs from "fs"; import NodePath from "path"; import { Singleton } from "tstl/thread/Singleton"; -import { VariadicSingleton } from "tstl/thread/VariadicSingleton"; import ts from "typescript"; import typia, { IJsonApplication, IJsonComponents, IJsonSchema } from "typia"; @@ -537,14 +536,19 @@ export namespace SwaggerGenerator { const contentType = parameter.custom ? parameter.contentType : "application/json"; + const description = get_parametric_description( + route, + "param", + parameter.name, + ); return { description: - warning - .get(parameter.custom && parameter.encrypted) - .get("request") + - (get_parametric_description(route, "param", parameter.name) ?? - ""), + parameter.custom && parameter.encrypted + ? `${warning.get(!!description).get("request")}${ + description ?? "" + }` + : description, content: { [contentType]: { schema, @@ -561,7 +565,68 @@ export namespace SwaggerGenerator { tupleList: Array, route: IRoute, ): ISwaggerRoute.IResponseBody { - // OUTPUT WITH SUCCESS STATUS + const output: ISwaggerRoute.IResponseBody = {}; + + //---- + // EXCEPTION STATUSES + //---- + // FROM DECORATOR + for (const [status, exp] of Object.entries(route.exceptions)) { + const schema = generate_schema( + checker, + collection, + tupleList, + exp.type, + ); + if (schema !== null) + output[status] = { + description: exp.description, + content: { + "application/json": { schema }, + }, + }; + } + + // FROM COMMENT TAGS + for (const tag of route.tags) { + if (tag.name !== "throw" && tag.name !== "throws") continue; + + const text: string | undefined = tag.text?.find( + (elem) => elem.kind === "text", + )?.text; + if (text === undefined) continue; + + const elements: string[] = text.split(" ").map((str) => str.trim()); + const status: string = elements[0]; + if ( + isNaN(Number(status)) && + status !== "2XX" && + status !== "3XX" && + status !== "4XX" && + status !== "5XX" + ) + continue; + + const description: string | undefined = + elements.length === 1 ? undefined : elements.slice(1).join(" "); + const oldbie = output[status]; + if (oldbie !== undefined && oldbie.description === undefined) + oldbie.description = description; + else if (oldbie === undefined) + output[status] = { + description, + content: { + "application/json": { + schema: {}, + }, + }, + }; + } + + //---- + // SUCCESS + //---- + // STATUS & SCHEMA const status: string = route.status !== undefined ? String(route.status) @@ -576,61 +641,28 @@ export namespace SwaggerGenerator { tupleList, route.output.type, ); - const success: ISwaggerRoute.IResponseBody = { - [status]: { - description: - warning.get(route.encrypted).get("response", route.method) + - (get_parametric_description(route, "return") ?? - get_parametric_description(route, "returns") ?? - ""), - content: - schema === null || route.output.name === "void" - ? undefined - : { - [route.output.contentType]: { - schema, - }, + + // DO ASSIGN + const description = + get_parametric_description(route, "return") ?? + get_parametric_description(route, "returns"); + output[status] = { + description: route.encrypted + ? `${warning.get(!!description).get("response", route.method)}${ + description ?? "" + }` + : description, + content: + schema === null || route.output.name === "void" + ? undefined + : { + [route.output.contentType]: { + schema, }, - "x-nestia-encrypted": route.encrypted, - }, + }, + "x-nestia-encrypted": route.encrypted, }; - - // EXCEPTION STATUSES - const exceptions: ISwaggerRoute.IResponseBody = Object.fromEntries( - route.tags - .filter( - (tag) => - (tag.name === "throw" || tag.name === "throws") && - tag.text && - tag.text.find( - (elem) => - elem.kind === "text" && - isNaN( - Number( - elem.text - .split(" ") - .map((str) => str.trim())[0], - ), - ) === false, - ) !== undefined, - ) - .map((tag) => { - const text: string = tag.text!.find( - (elem) => elem.kind === "text", - )!.text; - const elements: string[] = text - .split(" ") - .map((str) => str.trim()); - - return [ - elements[0], - { - description: elements.slice(1).join(" "), - }, - ]; - }), - ); - return { ...exceptions, ...success }; + return output; } /* --------------------------------------------------------- @@ -692,35 +724,31 @@ const required = (type: ts.Type): boolean => { ); }; -const warning = new VariadicSingleton((encrypted: boolean) => { - if (encrypted === false) return new Singleton(() => ""); - - return new VariadicSingleton( - (type: "request" | "response", method?: string) => { - const summary = - type === "request" - ? "Request body must be encrypted." - : "Response data have been encrypted."; - - const component = - type === "request" - ? "[EncryptedBody](https://github.com/samchon/@nestia/core#encryptedbody)" - : `[EncryptedRoute.${method![0].toUpperCase()}.${method! - .substring(1) - .toLowerCase()}](https://github.com/samchon/@nestia/core#encryptedroute)`; - - return `## Warning -${summary} - -The ${type} body data would be encrypted as "AES-128(256) / CBC mode / PKCS#5 Padding / Base64 Encoding", through the ${component} component. - -Therefore, just utilize this swagger editor only for referencing. If you need to call the real API, using [SDK](https://github.com/samchon/nestia#software-development-kit) would be much better. - ------------------ - -`; - }, - ); +const warning = new Singleton((described: boolean) => { + return new Singleton((type: "request" | "response", method?: string) => { + const summary = + type === "request" + ? "Request body must be encrypted." + : "Response data have been encrypted."; + const component = + type === "request" + ? "[EncryptedBody](https://github.com/samchon/@nestia/core#encryptedbody)" + : `[EncryptedRoute.${method![0].toUpperCase()}.${method! + .substring(1) + .toLowerCase()}](https://github.com/samchon/@nestia/core#encryptedroute)`; + + const content: string[] = [ + "## Warning", + "", + summary, + "", + `The ${type} body data would be encrypted as "AES-128(256) / CBC mode / PKCS#5 Padding / Base64 Encoding", through the ${component} component.`, + "", + `Therefore, just utilize this swagger editor only for referencing. If you need to call the real API, using [SDK](https://github.com/samchon/nestia#software-development-kit) would be much better.`, + ]; + if (described === true) content.push("----------------", ""); + return content.join("\n"); + }); }); interface ISchemaTuple { diff --git a/packages/sdk/src/generates/internal/SdkFunctionProgrammer.ts b/packages/sdk/src/generates/internal/SdkFunctionProgrammer.ts index 3731efbe7..a5638c5ce 100644 --- a/packages/sdk/src/generates/internal/SdkFunctionProgrammer.ts +++ b/packages/sdk/src/generates/internal/SdkFunctionProgrammer.ts @@ -198,6 +198,8 @@ export namespace SdkFunctionProgrammer { const comments: string[] = route.description ? route.description.split("\n") : []; + + // COMMENT TAGS const tags: IJsDocTagInfo[] = route.tags.filter( (tag) => tag.name !== "param" || @@ -214,7 +216,24 @@ export namespace SdkFunctionProgrammer { comments.push("", ...new Set(content)); } - // COMPLETE THE COMMENT + // EXCEPTIONS + for (const [key, value] of Object.entries(route.exceptions)) { + if ( + comments.some( + (str) => + str.startsWith(`@throw ${key}`) || + str.startsWith(`@throws ${key}`), + ) + ) + continue; + comments.push( + value.description + ? `@throws ${key} ${value.description.split("\n")[0]}` + : `@throws ${key}`, + ); + } + + // POSTFIX if (!!comments.length) comments.push(""); comments.push( `@controller ${route.symbol}`, diff --git a/packages/sdk/src/structures/IController.ts b/packages/sdk/src/structures/IController.ts index c6d9d9853..ce8dc2865 100644 --- a/packages/sdk/src/structures/IController.ts +++ b/packages/sdk/src/structures/IController.ts @@ -19,6 +19,10 @@ export namespace IController { type?: string; contentType: "application/json" | "text/plain"; security: Record[]; + exceptions: Record< + number | "2XX" | "3XX" | "4XX" | "5XX", + IController.IException + >; } export type IParameter = @@ -68,4 +72,10 @@ export namespace IController { nullable: boolean; }; } + + export interface IException { + type: string; + status: number | "2XX" | "3XX" | "4XX" | "5XX"; + description: string | undefined; + } } diff --git a/packages/sdk/src/structures/IRoute.ts b/packages/sdk/src/structures/IRoute.ts index d9b325761..5ccf1d36c 100644 --- a/packages/sdk/src/structures/IRoute.ts +++ b/packages/sdk/src/structures/IRoute.ts @@ -24,6 +24,7 @@ export interface IRoute { | { type: "assigner"; source: string } >; security: Record[]; + exceptions: Record; } export namespace IRoute { @@ -32,6 +33,7 @@ export namespace IRoute { type: ITypeTuple; }; export interface IOutput extends ITypeTuple { + description?: string; contentType: "application/json" | "text/plain"; } } diff --git a/packages/sdk/src/structures/ISwaggerRoute.ts b/packages/sdk/src/structures/ISwaggerRoute.ts index 3567f2bfc..88001462b 100644 --- a/packages/sdk/src/structures/ISwaggerRoute.ts +++ b/packages/sdk/src/structures/ISwaggerRoute.ts @@ -23,7 +23,7 @@ export namespace ISwaggerRoute { description?: string; } export interface IRequestBody { - description: string; + description?: string; content: IContent; required: true; "x-nestia-encrypted": boolean; @@ -31,7 +31,7 @@ export namespace ISwaggerRoute { export type IResponseBody = Record< string, { - description: string; + description?: string; content?: IContent; "x-nestia-encrypted"?: boolean; } diff --git a/test/features/exception-filter/swagger.json b/test/features/exception-filter/swagger.json index 086b338ad..54c2cd9c3 100644 --- a/test/features/exception-filter/swagger.json +++ b/test/features/exception-filter/swagger.json @@ -20,7 +20,6 @@ "tags": [], "parameters": [], "requestBody": { - "description": "", "content": { "application/json": { "schema": { @@ -33,7 +32,6 @@ }, "responses": { "201": { - "description": "", "content": { "application/json": { "schema": { @@ -55,7 +53,6 @@ "parameters": [], "responses": { "200": { - "description": "", "x-nestia-encrypted": false } }, @@ -81,7 +78,6 @@ ], "responses": { "200": { - "description": "", "x-nestia-encrypted": false } }, @@ -106,7 +102,6 @@ ], "responses": { "200": { - "description": "", "content": { "application/json": { "schema": { @@ -128,7 +123,6 @@ "parameters": [], "responses": { "200": { - "description": "", "x-nestia-encrypted": false } }, @@ -143,7 +137,6 @@ "parameters": [], "responses": { "200": { - "description": "", "x-nestia-encrypted": false } }, @@ -158,7 +151,6 @@ "parameters": [], "responses": { "200": { - "description": "", "content": { "application/json": { "schema": { diff --git a/test/features/exception/nestia.config.ts b/test/features/exception/nestia.config.ts new file mode 100644 index 000000000..597f66973 --- /dev/null +++ b/test/features/exception/nestia.config.ts @@ -0,0 +1,16 @@ +import { INestiaConfig } from "@nestia/sdk"; + +export const NESTIA_CONFIG: INestiaConfig = { + input: ["src/controllers"], + output: "src/api", + e2e: "src/test", + swagger: { + output: "swagger.json", + security: { + bearer: { + type: "apiKey", + }, + }, + }, +}; +export default NESTIA_CONFIG; diff --git a/test/features/exception/src/Backend.ts b/test/features/exception/src/Backend.ts new file mode 100644 index 000000000..8ab51b70f --- /dev/null +++ b/test/features/exception/src/Backend.ts @@ -0,0 +1,28 @@ +import { INestApplication } from "@nestjs/common"; +import { NestFactory } from "@nestjs/core"; + +import core from "@nestia/core"; + +export class Backend { + private application_?: INestApplication; + + public async open(): Promise { + this.application_ = await NestFactory.create( + await core.EncryptedModule.dynamic(__dirname + "/controllers", { + key: "A".repeat(32), + iv: "B".repeat(16), + }), + { logger: false }, + ); + await this.application_.listen(37_000); + } + + public async close(): Promise { + if (this.application_ === undefined) return; + + const app = this.application_; + await app.close(); + + delete this.application_; + } +} diff --git a/test/features/exception/src/api/HttpError.ts b/test/features/exception/src/api/HttpError.ts new file mode 100644 index 000000000..5df328ae4 --- /dev/null +++ b/test/features/exception/src/api/HttpError.ts @@ -0,0 +1 @@ +export { HttpError } from "@nestia/fetcher"; diff --git a/test/features/exception/src/api/IConnection.ts b/test/features/exception/src/api/IConnection.ts new file mode 100644 index 000000000..107bdb8f8 --- /dev/null +++ b/test/features/exception/src/api/IConnection.ts @@ -0,0 +1 @@ +export type { IConnection } from "@nestia/fetcher"; diff --git a/test/features/exception/src/api/Primitive.ts b/test/features/exception/src/api/Primitive.ts new file mode 100644 index 000000000..60d394424 --- /dev/null +++ b/test/features/exception/src/api/Primitive.ts @@ -0,0 +1 @@ +export type { Primitive } from "@nestia/fetcher"; diff --git a/test/features/exception/src/api/functional/exception/index.ts b/test/features/exception/src/api/functional/exception/index.ts new file mode 100644 index 000000000..c34085c13 --- /dev/null +++ b/test/features/exception/src/api/functional/exception/index.ts @@ -0,0 +1,147 @@ +/** + * @packageDocumentation + * @module api.functional.exception + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import { Fetcher } from "@nestia/fetcher"; +import type { IConnection, Primitive } from "@nestia/fetcher"; + +import type { IBbsArticle } from "./../../structures/IBbsArticle"; + +/** + * @throws 400 invalid request + * @throws 404 unable to find the matched section + * @throws 428 + * @throws 5XX internal server error + * + * @controller ExceptionController.typed() + * @path POST /exception/:section/typed + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function typed( + connection: IConnection, + section: string, + input: typed.Input, +): Promise { + return Fetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + typed.ENCRYPTED, + typed.METHOD, + typed.path(section), + input, + ); +} +export namespace typed { + export type Input = Primitive; + export type Output = Primitive; + + export const METHOD = "POST" as const; + export const PATH: string = "/exception/:section/typed"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export const path = (section: string): string => { + return `/exception/${encodeURIComponent(section ?? "null")}/typed`; + } +} + +/** + * + * @throws 400 invalid request + * @throws 404 unable to find the matched section + * @throw 428 unable to process the request + * @throw 5XX internal server error + * + * @controller ExceptionController.tags() + * @path POST /exception/:section/tags + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function tags( + connection: IConnection, + section: string, + input: tags.Input, +): Promise { + return Fetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + tags.ENCRYPTED, + tags.METHOD, + tags.path(section), + input, + ); +} +export namespace tags { + export type Input = Primitive; + export type Output = Primitive; + + export const METHOD = "POST" as const; + export const PATH: string = "/exception/:section/tags"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export const path = (section: string): string => { + return `/exception/${encodeURIComponent(section ?? "null")}/tags`; + } +} + +/** + * + * @throws 400 invalid request + * @throws 404 unable to find the matched section + * @throw 428 unable to process the request + * @throw 5XX internal server error + * + * @controller ExceptionController.composite() + * @path POST /exception/:section/composite + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function composite( + connection: IConnection, + section: string, + input: composite.Input, +): Promise { + return Fetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + composite.ENCRYPTED, + composite.METHOD, + composite.path(section), + input, + ); +} +export namespace composite { + export type Input = Primitive; + export type Output = Primitive; + + export const METHOD = "POST" as const; + export const PATH: string = "/exception/:section/composite"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export const path = (section: string): string => { + return `/exception/${encodeURIComponent(section ?? "null")}/composite`; + } +} \ No newline at end of file diff --git a/test/features/exception/src/api/functional/health/index.ts b/test/features/exception/src/api/functional/health/index.ts new file mode 100644 index 000000000..c51a94f1c --- /dev/null +++ b/test/features/exception/src/api/functional/health/index.ts @@ -0,0 +1,37 @@ +/** + * @packageDocumentation + * @module api.functional.health + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import { Fetcher } from "@nestia/fetcher"; +import type { IConnection } from "@nestia/fetcher"; + +/** + * @controller HealthController.get() + * @path GET /health + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function get( + connection: IConnection, +): Promise { + return Fetcher.fetch( + connection, + get.ENCRYPTED, + get.METHOD, + get.path(), + ); +} +export namespace get { + + export const METHOD = "GET" as const; + export const PATH: string = "/health"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export const path = (): string => { + return `/health`; + } +} \ No newline at end of file diff --git a/test/features/exception/src/api/functional/index.ts b/test/features/exception/src/api/functional/index.ts new file mode 100644 index 000000000..44283b833 --- /dev/null +++ b/test/features/exception/src/api/functional/index.ts @@ -0,0 +1,9 @@ +/** + * @packageDocumentation + * @module api.functional + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +export * as exception from "./exception"; +export * as health from "./health"; +export * as performance from "./performance"; \ No newline at end of file diff --git a/test/features/exception/src/api/functional/performance/index.ts b/test/features/exception/src/api/functional/performance/index.ts new file mode 100644 index 000000000..bee136f70 --- /dev/null +++ b/test/features/exception/src/api/functional/performance/index.ts @@ -0,0 +1,40 @@ +/** + * @packageDocumentation + * @module api.functional.performance + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import { Fetcher } from "@nestia/fetcher"; +import type { IConnection, Primitive } from "@nestia/fetcher"; + +import type { IPerformance } from "./../../structures/IPerformance"; + +/** + * @controller PerformanceController.get() + * @path GET /performance + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function get( + connection: IConnection, +): Promise { + return Fetcher.fetch( + connection, + get.ENCRYPTED, + get.METHOD, + get.path(), + ); +} +export namespace get { + export type Output = Primitive; + + export const METHOD = "GET" as const; + export const PATH: string = "/performance"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export const path = (): string => { + return `/performance`; + } +} \ No newline at end of file diff --git a/test/features/exception/src/api/index.ts b/test/features/exception/src/api/index.ts new file mode 100644 index 000000000..1705f43c8 --- /dev/null +++ b/test/features/exception/src/api/index.ts @@ -0,0 +1,4 @@ +import * as api from "./module"; + +export * from "./module"; +export default api; diff --git a/test/features/exception/src/api/module.ts b/test/features/exception/src/api/module.ts new file mode 100644 index 000000000..dbb6e9a51 --- /dev/null +++ b/test/features/exception/src/api/module.ts @@ -0,0 +1,5 @@ +export type * from "./IConnection"; +export type * from "./Primitive"; +export * from "./HttpError"; + +export * as functional from "./functional"; diff --git a/test/features/exception/src/api/structures/IBbsArticle.ts b/test/features/exception/src/api/structures/IBbsArticle.ts new file mode 100644 index 000000000..aa8085b02 --- /dev/null +++ b/test/features/exception/src/api/structures/IBbsArticle.ts @@ -0,0 +1,41 @@ +export interface IBbsArticle extends IBbsArticle.IStore { + /** + * @format uuid + */ + id: string; + + /** + * @format date-time + */ + created_at: string; +} +export namespace IBbsArticle { + export interface IStore { + /** + * @minLength 3 + * @maxLength 50 + */ + title: string; + body: string; + files: IAttachmentFile[]; + } +} + +export interface IAttachmentFile { + /** + * @minLengt 1 + * @maxLength 255 + */ + name: string | null; + + /** + * @minLength 1 + * @maxLength 8 + */ + extension: string | null; + + /** + * @format url + */ + url: string; +} diff --git a/test/features/exception/src/api/structures/IInternalServerError.ts b/test/features/exception/src/api/structures/IInternalServerError.ts new file mode 100644 index 000000000..7950ca434 --- /dev/null +++ b/test/features/exception/src/api/structures/IInternalServerError.ts @@ -0,0 +1,5 @@ +export interface IInternalServerError { + name: string; + message: string; + stack: string[]; +} diff --git a/test/features/exception/src/api/structures/INotFound.ts b/test/features/exception/src/api/structures/INotFound.ts new file mode 100644 index 000000000..daf9eaf15 --- /dev/null +++ b/test/features/exception/src/api/structures/INotFound.ts @@ -0,0 +1,5 @@ +export interface INotFound { + schema: string; + table: string; + id: string; +} diff --git a/test/features/exception/src/api/structures/IPerformance.ts b/test/features/exception/src/api/structures/IPerformance.ts new file mode 100644 index 000000000..a9e9336e1 --- /dev/null +++ b/test/features/exception/src/api/structures/IPerformance.ts @@ -0,0 +1,10 @@ +/** + * Performance info. + * + * @author Samchon + */ +export interface IPerformance { + cpu: NodeJS.CpuUsage; + memory: NodeJS.MemoryUsage; + resource: NodeJS.ResourceUsage; +} diff --git a/test/features/exception/src/api/structures/ISystem.ts b/test/features/exception/src/api/structures/ISystem.ts new file mode 100644 index 000000000..68f05ca92 --- /dev/null +++ b/test/features/exception/src/api/structures/ISystem.ts @@ -0,0 +1,81 @@ +/** + * System Information. + * + * @author Jeongho Nam + */ +export interface ISystem { + /** + * Random Unique ID. + */ + uid: number; + + /** + * `process.argv` + */ + arguments: string[]; + + /** + * Git commit info. + */ + commit: ISystem.ICommit; + + /** + * `package.json` + */ + package: ISystem.IPackage; + + /** + * Creation time of this server. + */ + created_at: string; +} + +export namespace ISystem { + /** + * Git commit info. + */ + export interface ICommit { + shortHash: string; + branch: string; + hash: string; + subject: string; + sanitizedSubject: string; + body: string; + author: ICommit.IUser; + committer: ICommit.IUser; + authored_at: string; + commited_at: string; + notes?: string; + tags: string[]; + } + export namespace ICommit { + /** + * Git user account info. + */ + export interface IUser { + name: string; + email: string; + } + } + + /** + * NPM package info. + */ + export interface IPackage { + name: string; + version: string; + description: string; + main?: string; + typings?: string; + scripts: Record; + repository: { type: "git"; url: string }; + author: string; + license: string; + bugs: { url: string }; + homepage: string; + devDependencies?: Record; + dependencies: Record; + publishConfig?: { registry: string }; + files?: string[]; + } +} diff --git a/test/features/exception/src/api/structures/IUnprocessibleEntity.ts b/test/features/exception/src/api/structures/IUnprocessibleEntity.ts new file mode 100644 index 000000000..a6652d037 --- /dev/null +++ b/test/features/exception/src/api/structures/IUnprocessibleEntity.ts @@ -0,0 +1,3 @@ +export interface IUnprocessibleEntity { + reason: string; +} diff --git a/test/features/exception/src/controllers/ExceptionController.ts b/test/features/exception/src/controllers/ExceptionController.ts new file mode 100644 index 000000000..4233d96aa --- /dev/null +++ b/test/features/exception/src/controllers/ExceptionController.ts @@ -0,0 +1,68 @@ +import { Controller } from "@nestjs/common"; +import { ApiResponse } from "@nestjs/swagger"; +import typia, { TypeGuardError } from "typia"; + +import { + TypedBody, + TypedException, + TypedParam, + TypedRoute, +} from "@nestia/core"; + +import { IBbsArticle } from "@api/lib/structures/IBbsArticle"; +import { IInternalServerError } from "@api/lib/structures/IInternalServerError"; +import { INotFound } from "@api/lib/structures/INotFound"; +import { IUnprocessibleEntity } from "@api/lib/structures/IUnprocessibleEntity"; + +@Controller("exception") +export class ExceptionController { + @TypedException(400, "invalid request") + @TypedException(404, "unable to find the matched section") + @TypedException(428) + @TypedException("5XX", "internal server error") + @TypedRoute.Post(":section/typed") + public async typed( + @TypedParam("section") section: string, + @TypedBody() input: IBbsArticle.IStore, + ): Promise { + section; + input; + return typia.random(); + } + + /** + * @throws 400 invalid request + * @throws 404 unable to find the matched section + * @throw 428 unable to process the request + * @throw 5XX internal server error + */ + @TypedRoute.Post(":section/tags") + public async tags( + @TypedParam("section") section: string, + @TypedBody() input: IBbsArticle.IStore, + ): Promise { + section; + input; + return typia.random(); + } + + /** + * @throws 400 invalid request + * @throws 404 unable to find the matched section + * @throw 428 unable to process the request + * @throw 5XX internal server error + */ + @TypedException(400, "invalid request") + @TypedException(404) + @TypedException(428) + @TypedException("5XX") + @TypedRoute.Post(":section/composite") + public async composite( + @TypedParam("section") section: string, + @TypedBody() input: IBbsArticle.IStore, + ): Promise { + section; + input; + return typia.random(); + } +} diff --git a/test/features/exception/src/controllers/HealthController.ts b/test/features/exception/src/controllers/HealthController.ts new file mode 100644 index 000000000..a9ce3563b --- /dev/null +++ b/test/features/exception/src/controllers/HealthController.ts @@ -0,0 +1,9 @@ +import { Controller } from "@nestjs/common"; + +import core from "@nestia/core"; + +@Controller("health") +export class HealthController { + @core.TypedRoute.Get() + public get(): void {} +} diff --git a/test/features/exception/src/controllers/PerformanceController.ts b/test/features/exception/src/controllers/PerformanceController.ts new file mode 100644 index 000000000..798d64fae --- /dev/null +++ b/test/features/exception/src/controllers/PerformanceController.ts @@ -0,0 +1,17 @@ +import { Controller } from "@nestjs/common"; + +import core from "@nestia/core"; + +import { IPerformance } from "@api/lib/structures/IPerformance"; + +@Controller("performance") +export class PerformanceController { + @core.TypedRoute.Get() + public async get(): Promise { + return { + cpu: process.cpuUsage(), + memory: process.memoryUsage(), + resource: process.resourceUsage(), + }; + } +} diff --git a/test/features/exception/src/test/features/api/automated/test_api_exception_composite.ts b/test/features/exception/src/test/features/api/automated/test_api_exception_composite.ts new file mode 100644 index 000000000..a76f91871 --- /dev/null +++ b/test/features/exception/src/test/features/api/automated/test_api_exception_composite.ts @@ -0,0 +1,16 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; +import type { IBbsArticle } from "./../../../../api/structures/IBbsArticle"; + +export const test_api_exception_composite = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.exception.composite( + connection, + typia.random>(), + typia.random>(), + ); + typia.assert(output); +}; \ No newline at end of file diff --git a/test/features/exception/src/test/features/api/automated/test_api_exception_tags.ts b/test/features/exception/src/test/features/api/automated/test_api_exception_tags.ts new file mode 100644 index 000000000..040253dbd --- /dev/null +++ b/test/features/exception/src/test/features/api/automated/test_api_exception_tags.ts @@ -0,0 +1,16 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; +import type { IBbsArticle } from "./../../../../api/structures/IBbsArticle"; + +export const test_api_exception_tags = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.exception.tags( + connection, + typia.random>(), + typia.random>(), + ); + typia.assert(output); +}; \ No newline at end of file diff --git a/test/features/exception/src/test/features/api/automated/test_api_exception_typed.ts b/test/features/exception/src/test/features/api/automated/test_api_exception_typed.ts new file mode 100644 index 000000000..17cb7581d --- /dev/null +++ b/test/features/exception/src/test/features/api/automated/test_api_exception_typed.ts @@ -0,0 +1,16 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; +import type { IBbsArticle } from "./../../../../api/structures/IBbsArticle"; + +export const test_api_exception_typed = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.exception.typed( + connection, + typia.random>(), + typia.random>(), + ); + typia.assert(output); +}; \ No newline at end of file diff --git a/test/features/exception/src/test/features/api/automated/test_api_health_get.ts b/test/features/exception/src/test/features/api/automated/test_api_health_get.ts new file mode 100644 index 000000000..a043247a5 --- /dev/null +++ b/test/features/exception/src/test/features/api/automated/test_api_health_get.ts @@ -0,0 +1,9 @@ +import api from "./../../../../api"; + +export const test_api_health_get = async ( + connection: api.IConnection +): Promise => { + await api.functional.health.get( + connection, + ); +}; \ No newline at end of file diff --git a/test/features/exception/src/test/features/api/automated/test_api_performance_get.ts b/test/features/exception/src/test/features/api/automated/test_api_performance_get.ts new file mode 100644 index 000000000..f557fbb61 --- /dev/null +++ b/test/features/exception/src/test/features/api/automated/test_api_performance_get.ts @@ -0,0 +1,14 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; +import type { IPerformance } from "./../../../../api/structures/IPerformance"; + +export const test_api_performance_get = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.performance.get( + connection, + ); + typia.assert(output); +}; \ No newline at end of file diff --git a/test/features/exception/src/test/features/api/test_api_health_check.ts b/test/features/exception/src/test/features/api/test_api_health_check.ts new file mode 100644 index 000000000..c34703c71 --- /dev/null +++ b/test/features/exception/src/test/features/api/test_api_health_check.ts @@ -0,0 +1,5 @@ +import api from "@api"; + +export const test_api_monitor_health_check = ( + connection: api.IConnection, +): Promise => api.functional.health.get(connection); diff --git a/test/features/exception/src/test/features/api/test_api_performance.ts b/test/features/exception/src/test/features/api/test_api_performance.ts new file mode 100644 index 000000000..55b6d2ca7 --- /dev/null +++ b/test/features/exception/src/test/features/api/test_api_performance.ts @@ -0,0 +1,13 @@ +import typia from "typia"; + +import api from "@api"; +import { IPerformance } from "@api/lib/structures/IPerformance"; + +export const test_api_monitor_performance = async ( + connection: api.IConnection, +): Promise => { + const performance: IPerformance = await api.functional.performance.get( + connection, + ); + typia.assert(performance); +}; diff --git a/test/features/exception/src/test/index.ts b/test/features/exception/src/test/index.ts new file mode 100644 index 000000000..339bdd6a4 --- /dev/null +++ b/test/features/exception/src/test/index.ts @@ -0,0 +1,40 @@ +import { DynamicExecutor } from "@nestia/e2e"; + +import { Backend } from "../Backend"; + +async function main(): Promise { + const server: Backend = new Backend(); + await server.open(); + + const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ + extension: __filename.substring(__filename.length - 2), + prefix: "test", + parameters: () => [ + { + host: "http://127.0.0.1:37000", + encryption: { + key: "A".repeat(32), + iv: "B".repeat(16), + }, + }, + ], + })(`${__dirname}/features`); + await server.close(); + + const exceptions: Error[] = report.executions + .filter((exec) => exec.error !== null) + .map((exec) => exec.error!); + if (exceptions.length === 0) { + console.log("Success"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + } else { + for (const exp of exceptions) console.log(exp); + console.log("Failed"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + process.exit(-1); + } +} +main().catch((exp) => { + console.log(exp); + process.exit(-1); +}); diff --git a/test/features/exception/swagger.json b/test/features/exception/swagger.json new file mode 100644 index 000000000..0b1741a9c --- /dev/null +++ b/test/features/exception/swagger.json @@ -0,0 +1,973 @@ +{ + "openapi": "3.0.1", + "servers": [ + { + "url": "https://github.com/samchon/nestia", + "description": "insert your server url" + } + ], + "info": { + "version": "0.0.0", + "title": "@nestia/test", + "description": "Test program of Nestia", + "license": { + "name": "MIT" + } + }, + "paths": { + "/exception/{section}/typed": { + "post": { + "tags": [], + "parameters": [ + { + "name": "section", + "in": "path", + "description": "", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle.IStore" + } + } + }, + "required": true, + "x-nestia-encrypted": false + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle" + } + } + }, + "x-nestia-encrypted": false + }, + "400": { + "description": "invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeGuardError" + } + } + } + }, + "404": { + "description": "unable to find the matched section", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/INotFound" + } + } + } + }, + "428": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IUnprocessibleEntity" + } + } + } + }, + "5XX": { + "description": "internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IInternalServerError" + } + } + } + } + }, + "x-nestia-namespace": "exception.typed.typed", + "x-nestia-jsDocTags": [], + "x-nestia-method": "POST" + } + }, + "/exception/{section}/tags": { + "post": { + "tags": [], + "parameters": [ + { + "name": "section", + "in": "path", + "description": "", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle.IStore" + } + } + }, + "required": true, + "x-nestia-encrypted": false + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle" + } + } + }, + "x-nestia-encrypted": false + }, + "400": { + "description": "invalid request", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "unable to find the matched section", + "content": { + "application/json": { + "schema": {} + } + } + }, + "428": { + "description": "unable to process the request", + "content": { + "application/json": { + "schema": {} + } + } + }, + "5XX": { + "description": "internal server error", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "x-nestia-namespace": "exception.tags.tags", + "x-nestia-jsDocTags": [ + { + "name": "throws", + "text": [ + { + "text": "400 invalid request", + "kind": "text" + } + ] + }, + { + "name": "throws", + "text": [ + { + "text": "404 unable to find the matched section", + "kind": "text" + } + ] + }, + { + "name": "throw", + "text": [ + { + "text": "428 unable to process the request", + "kind": "text" + } + ] + }, + { + "name": "throw", + "text": [ + { + "text": "5XX internal server error", + "kind": "text" + } + ] + } + ], + "x-nestia-method": "POST" + } + }, + "/exception/{section}/composite": { + "post": { + "tags": [], + "parameters": [ + { + "name": "section", + "in": "path", + "description": "", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle.IStore" + } + } + }, + "required": true, + "x-nestia-encrypted": false + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle" + } + } + }, + "x-nestia-encrypted": false + }, + "400": { + "description": "invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeGuardError" + } + } + } + }, + "404": { + "description": "unable to find the matched section", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/INotFound" + } + } + } + }, + "428": { + "description": "unable to process the request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IUnprocessibleEntity" + } + } + } + }, + "5XX": { + "description": "internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IInternalServerError" + } + } + } + } + }, + "x-nestia-namespace": "exception.composite.composite", + "x-nestia-jsDocTags": [ + { + "name": "throws", + "text": [ + { + "text": "400 invalid request", + "kind": "text" + } + ] + }, + { + "name": "throws", + "text": [ + { + "text": "404 unable to find the matched section", + "kind": "text" + } + ] + }, + { + "name": "throw", + "text": [ + { + "text": "428 unable to process the request", + "kind": "text" + } + ] + }, + { + "name": "throw", + "text": [ + { + "text": "5XX internal server error", + "kind": "text" + } + ] + } + ], + "x-nestia-method": "POST" + } + }, + "/health": { + "get": { + "tags": [], + "parameters": [], + "responses": { + "200": { + "x-nestia-encrypted": false + } + }, + "x-nestia-namespace": "health.get", + "x-nestia-jsDocTags": [], + "x-nestia-method": "GET" + } + }, + "/performance": { + "get": { + "tags": [], + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IPerformance" + } + } + }, + "x-nestia-encrypted": false + } + }, + "x-nestia-namespace": "performance.get", + "x-nestia-jsDocTags": [], + "x-nestia-method": "GET" + } + } + }, + "components": { + "schemas": { + "IBbsArticle.IStore": { + "type": "object", + "properties": { + "title": { + "x-typia-metaTags": [ + { + "kind": "minLength", + "value": 3 + }, + { + "kind": "maxLength", + "value": 50 + } + ], + "x-typia-jsDocTags": [ + { + "name": "minLength", + "text": [ + { + "text": "3", + "kind": "text" + } + ] + }, + { + "name": "maxLength", + "text": [ + { + "text": "50", + "kind": "text" + } + ] + } + ], + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "body": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "files": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "array", + "items": { + "$ref": "#/components/schemas/IAttachmentFile" + } + } + }, + "nullable": false, + "required": [ + "title", + "body", + "files" + ], + "x-typia-jsDocTags": [] + }, + "IAttachmentFile": { + "type": "object", + "properties": { + "name": { + "x-typia-metaTags": [ + { + "kind": "maxLength", + "value": 255 + } + ], + "x-typia-jsDocTags": [ + { + "name": "minLengt", + "text": [ + { + "text": "1", + "kind": "text" + } + ] + }, + { + "name": "maxLength", + "text": [ + { + "text": "255", + "kind": "text" + } + ] + } + ], + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "maxLength": 255, + "nullable": true + }, + "extension": { + "x-typia-metaTags": [ + { + "kind": "minLength", + "value": 1 + }, + { + "kind": "maxLength", + "value": 8 + } + ], + "x-typia-jsDocTags": [ + { + "name": "minLength", + "text": [ + { + "text": "1", + "kind": "text" + } + ] + }, + { + "name": "maxLength", + "text": [ + { + "text": "8", + "kind": "text" + } + ] + } + ], + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "minLength": 1, + "maxLength": 8, + "nullable": true + }, + "url": { + "x-typia-metaTags": [ + { + "kind": "format", + "value": "url" + } + ], + "x-typia-jsDocTags": [ + { + "name": "format", + "text": [ + { + "text": "url", + "kind": "text" + } + ] + } + ], + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "format": "url" + } + }, + "nullable": false, + "required": [ + "name", + "extension", + "url" + ], + "x-typia-jsDocTags": [] + }, + "TypeGuardError": { + "type": "object", + "properties": { + "method": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "path": { + "x-typia-required": false, + "x-typia-optional": false, + "type": "string" + }, + "expected": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "value": { + "x-typia-required": true, + "x-typia-optional": false + }, + "name": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "message": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "stack": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + } + }, + "nullable": false, + "required": [ + "method", + "expected", + "value", + "name", + "message" + ], + "x-typia-jsDocTags": [] + }, + "INotFound": { + "type": "object", + "properties": { + "schema": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "table": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "id": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + } + }, + "nullable": false, + "required": [ + "schema", + "table", + "id" + ], + "x-typia-jsDocTags": [] + }, + "IUnprocessibleEntity": { + "type": "object", + "properties": { + "reason": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + } + }, + "nullable": false, + "required": [ + "reason" + ], + "x-typia-jsDocTags": [] + }, + "IInternalServerError": { + "type": "object", + "properties": { + "name": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "message": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "stack": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "array", + "items": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + } + } + }, + "nullable": false, + "required": [ + "name", + "message", + "stack" + ], + "x-typia-jsDocTags": [] + }, + "IBbsArticle": { + "type": "object", + "properties": { + "id": { + "x-typia-metaTags": [ + { + "kind": "format", + "value": "uuid" + } + ], + "x-typia-jsDocTags": [ + { + "name": "format", + "text": [ + { + "text": "uuid", + "kind": "text" + } + ] + } + ], + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "format": "uuid" + }, + "created_at": { + "x-typia-metaTags": [ + { + "kind": "format", + "value": "datetime" + } + ], + "x-typia-jsDocTags": [ + { + "name": "format", + "text": [ + { + "text": "date-time", + "kind": "text" + } + ] + } + ], + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "format": "date-time" + }, + "title": { + "x-typia-metaTags": [ + { + "kind": "minLength", + "value": 3 + }, + { + "kind": "maxLength", + "value": 50 + } + ], + "x-typia-jsDocTags": [ + { + "name": "minLength", + "text": [ + { + "text": "3", + "kind": "text" + } + ] + }, + { + "name": "maxLength", + "text": [ + { + "text": "50", + "kind": "text" + } + ] + } + ], + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "body": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "files": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "array", + "items": { + "$ref": "#/components/schemas/IAttachmentFile" + } + } + }, + "nullable": false, + "required": [ + "id", + "created_at", + "title", + "body", + "files" + ], + "x-typia-jsDocTags": [] + }, + "IPerformance": { + "type": "object", + "properties": { + "cpu": { + "$ref": "#/components/schemas/_singlequote_process_singlequote_.global.NodeJS.CpuUsage" + }, + "memory": { + "$ref": "#/components/schemas/_singlequote_process_singlequote_.global.NodeJS.MemoryUsage" + }, + "resource": { + "$ref": "#/components/schemas/_singlequote_process_singlequote_.global.NodeJS.ResourceUsage" + } + }, + "nullable": false, + "required": [ + "cpu", + "memory", + "resource" + ], + "description": "Performance info.", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ + { + "text": "Samchon", + "kind": "text" + } + ] + } + ] + }, + "_singlequote_process_singlequote_.global.NodeJS.CpuUsage": { + "type": "object", + "properties": { + "user": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "system": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + } + }, + "nullable": false, + "required": [ + "user", + "system" + ], + "x-typia-jsDocTags": [] + }, + "_singlequote_process_singlequote_.global.NodeJS.MemoryUsage": { + "type": "object", + "properties": { + "rss": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "heapTotal": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "heapUsed": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "external": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "arrayBuffers": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + } + }, + "nullable": false, + "required": [ + "rss", + "heapTotal", + "heapUsed", + "external", + "arrayBuffers" + ], + "x-typia-jsDocTags": [] + }, + "_singlequote_process_singlequote_.global.NodeJS.ResourceUsage": { + "type": "object", + "properties": { + "fsRead": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "fsWrite": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "involuntaryContextSwitches": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "ipcReceived": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "ipcSent": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "majorPageFault": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "maxRSS": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "minorPageFault": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "sharedMemorySize": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "signalsCount": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "swappedOut": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "systemCPUTime": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "unsharedDataSize": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "unsharedStackSize": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "userCPUTime": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "voluntaryContextSwitches": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + } + }, + "nullable": false, + "required": [ + "fsRead", + "fsWrite", + "involuntaryContextSwitches", + "ipcReceived", + "ipcSent", + "majorPageFault", + "maxRSS", + "minorPageFault", + "sharedMemorySize", + "signalsCount", + "swappedOut", + "systemCPUTime", + "unsharedDataSize", + "unsharedStackSize", + "userCPUTime", + "voluntaryContextSwitches" + ], + "x-typia-jsDocTags": [] + } + }, + "securitySchemes": { + "bearer": { + "type": "apiKey", + "in": "header", + "name": "Authorization" + } + } + } +} \ No newline at end of file diff --git a/test/features/exception/tsconfig.json b/test/features/exception/tsconfig.json new file mode 100644 index 000000000..c33dfa28f --- /dev/null +++ b/test/features/exception/tsconfig.json @@ -0,0 +1,98 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */// "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "@api": ["./src/api"], + "@api/lib/*": ["./src/api/*"], + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. *//* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "plugins": [ + { "transform": "typescript-transform-paths" }, + { "transform": "typia/lib/transform" }, + { "transform": "@nestia/core/lib/transform" }, + ], + } + } \ No newline at end of file diff --git a/test/package.json b/test/package.json index e69e7b18f..5eb3969cc 100644 --- a/test/package.json +++ b/test/package.json @@ -25,7 +25,7 @@ }, "homepage": "https://nestia.io", "devDependencies": { - "@nestia/migrate": "../packages/migrate/nestia-migrate-0.2.9.tgz", + "@nestia/migrate": "../packages/migrate/nestia-migrate-0.2.11.tgz", "@nestjs/swagger": "^7.1.2", "@trivago/prettier-plugin-sort-imports": "^4.0.0", "@types/express": "^4.17.17", @@ -42,9 +42,9 @@ "typia": "^4.2.1", "uuid": "^9.0.0", "nestia": "../packages/cli/nestia-4.4.0.tgz", - "@nestia/core": "../packages/core/nestia-core-1.6.3.tgz", + "@nestia/core": "../packages/core/nestia-core-1.6.4.tgz", "@nestia/e2e": "../packages/e2e/nestia-e2e-0.3.6.tgz", - "@nestia/fetcher": "../packages/fetcher/nestia-fetcher-1.6.3.tgz", - "@nestia/sdk": "../packages/sdk/nestia-sdk-1.6.3.tgz" + "@nestia/fetcher": "../packages/fetcher/nestia-fetcher-1.6.4.tgz", + "@nestia/sdk": "../packages/sdk/nestia-sdk-1.6.4.tgz" } } \ No newline at end of file