From 03b394d7f55338510cbad4e8a45398df04320c02 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Fri, 12 Jul 2024 16:39:58 +0900 Subject: [PATCH] New decorator `@SwaggerExample()`. ```typescript import core from "@nestia/core"; import { Controller } from "@nestjs/common"; import typia, { tags } from "typia"; import { v4 } from "uuid"; import { IBbsArticle } from "@api/lib/structures/IBbsArticle"; @Controller("bbs/articles") export class BbsArticlesController { /** * Create an article. * * @param request Request object from express. Must be disappeared in SDK * @param input Content to store * @returns Newly archived article * * @author Samchon * @warning This is an fake API */ @core.SwaggerExample.Response(typia.random()) @core.TypedRoute.Post() public async create( @core.SwaggerExample.Parameter(typia.random()) @core.SwaggerExample.Parameter("x", typia.random()) @core.SwaggerExample.Parameter("y", typia.random()) @core.SwaggerExample.Parameter("z", typia.random()) @core.TypedBody() input: IBbsArticle.ICreate, ): Promise { const output: IBbsArticle = { ...typia.random(), ...input, }; return output; } @core.SwaggerExample.Response(typia.random()) @core.SwaggerExample.Response("a", typia.random()) @core.SwaggerExample.Response("b", typia.random()) @core.TypedRoute.Put(":id") public async update( @core.SwaggerExample.Parameter(v4()) @core.TypedParam("id") id: string & tags.Format<"uuid">, @core.SwaggerExample.Parameter(typia.random()) @core.TypedBody() input: IBbsArticle.IUpdate, ): Promise { return { ...typia.random(), ...input, id, }; } } ``` --- package.json | 2 +- packages/core/package.json | 6 +- .../core/src/decorators/SwaggerExample.ts | 100 +++++ .../core/src/decorators/WebSocketRoute.ts | 2 +- packages/core/src/module.ts | 1 + packages/fetcher/package.json | 2 +- packages/sdk/package.json | 10 +- .../analyses/ReflectHttpOperationAnalyzer.ts | 19 + .../analyses/TypedHttpOperationAnalyzer.ts | 2 + .../internal/SwaggerSchemaGenerator.ts | 12 + .../src/structures/IReflectHttpOperation.ts | 12 + .../sdk/src/structures/ITypedHttpRoute.ts | 2 + .../src/controllers/SecurityController.ts | 1 - .../features/swagger-example/nestia.config.ts | 16 + test/features/swagger-example/src/Backend.ts | 27 ++ .../swagger-example/src/api/HttpError.ts | 1 + .../swagger-example/src/api/IConnection.ts | 1 + .../swagger-example/src/api/Primitive.ts | 1 + .../src/api/functional/bbs/articles/index.ts | 112 ++++++ .../src/api/functional/bbs/index.ts | 7 + .../src/api/functional/index.ts | 7 + .../features/swagger-example/src/api/index.ts | 4 + .../swagger-example/src/api/module.ts | 5 + .../src/api/structures/IBbsArticle.ts | 43 ++ .../src/controllers/BbsArticlesController.ts | 55 +++ .../src/test/features/test_swagger_example.ts | 54 +++ .../swagger-example/src/test/index.ts | 49 +++ test/features/swagger-example/swagger.json | 370 ++++++++++++++++++ test/features/swagger-example/tsconfig.json | 98 +++++ test/package.json | 8 +- 30 files changed, 1013 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/decorators/SwaggerExample.ts create mode 100644 test/features/swagger-example/nestia.config.ts create mode 100644 test/features/swagger-example/src/Backend.ts create mode 100644 test/features/swagger-example/src/api/HttpError.ts create mode 100644 test/features/swagger-example/src/api/IConnection.ts create mode 100644 test/features/swagger-example/src/api/Primitive.ts create mode 100644 test/features/swagger-example/src/api/functional/bbs/articles/index.ts create mode 100644 test/features/swagger-example/src/api/functional/bbs/index.ts create mode 100644 test/features/swagger-example/src/api/functional/index.ts create mode 100644 test/features/swagger-example/src/api/index.ts create mode 100644 test/features/swagger-example/src/api/module.ts create mode 100644 test/features/swagger-example/src/api/structures/IBbsArticle.ts create mode 100644 test/features/swagger-example/src/controllers/BbsArticlesController.ts create mode 100644 test/features/swagger-example/src/test/features/test_swagger_example.ts create mode 100644 test/features/swagger-example/src/test/index.ts create mode 100644 test/features/swagger-example/swagger.json create mode 100644 test/features/swagger-example/tsconfig.json diff --git a/package.json b/package.json index 95747d85b..238c85d55 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@nestia/station", - "version": "3.7.0-dev.20240711", + "version": "3.7.0-dev.20240712", "description": "Nestia station", "scripts": { "build": "node build/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index a31ce9903..46e8d2d38 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/core", - "version": "3.7.0-dev.20240711", + "version": "3.7.0-dev.20240712", "description": "Super-fast validation decorators of NestJS", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -36,7 +36,7 @@ }, "homepage": "https://nestia.io", "dependencies": { - "@nestia/fetcher": "^3.7.0-dev.20240711", + "@nestia/fetcher": "^3.7.0-dev.20240712", "@nestjs/common": ">=7.0.1", "@nestjs/core": ">=7.0.1", "@samchon/openapi": "^0.3.4", @@ -53,7 +53,7 @@ "ws": "^7.5.3" }, "peerDependencies": { - "@nestia/fetcher": ">=3.7.0-dev.20240711", + "@nestia/fetcher": ">=3.7.0-dev.20240712", "@nestjs/common": ">=7.0.1", "@nestjs/core": ">=7.0.1", "reflect-metadata": ">=0.1.12", diff --git a/packages/core/src/decorators/SwaggerExample.ts b/packages/core/src/decorators/SwaggerExample.ts new file mode 100644 index 000000000..84625787a --- /dev/null +++ b/packages/core/src/decorators/SwaggerExample.ts @@ -0,0 +1,100 @@ +export namespace SwaggerExample { + export function Response(value: T): MethodDecorator; + export function Response(key: string, value: T): MethodDecorator; + export function Response(...args: any[]): MethodDecorator { + return function SwaggerExampleResponse( + _target: Object, + _propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, + ): TypedPropertyDescriptor { + emplaceValue(emplaceOfResponse(descriptor))(args); + return descriptor; + }; + } + + export function Parameter(value: T): ParameterDecorator; + export function Parameter(key: string, value: T): ParameterDecorator; + export function Parameter(...args: any[]): ParameterDecorator { + return function SwaggerExampleParameter( + target: Object, + propertyKey: string | symbol | undefined, + index: number, + ): void { + emplaceValue(emplaceOfParameter(target, propertyKey ?? "", index))(args); + }; + } + + export interface IData { + examples?: Record; + example?: T; + index?: number; + } +} + +const emplaceValue = + (data: SwaggerExample.IData) => + (args: any[]) => { + if (args.length === 1) data.example = args[0]; + else { + const key: string = args[0]; + const value: T = args[1]; + data.examples ??= {}; + data.examples[key] = value; + } + }; + +const emplaceOfResponse = ( + descriptor: TypedPropertyDescriptor, +): SwaggerExample.IData => { + const oldbie: SwaggerExample.IData | undefined = Reflect.getMetadata( + "nestia/SwaggerExample/Response", + descriptor.value, + ); + if (oldbie !== undefined) return oldbie; + const newbie: SwaggerExample.IData = {}; + Reflect.defineMetadata( + "nestia/SwaggerExample/Response", + newbie, + descriptor.value, + ); + return newbie; +}; + +const emplaceOfParameter = ( + target: Object, + propertyKey: string | symbol, + index: number, +): SwaggerExample.IData => { + const array: SwaggerExample.IData[] = emplaceArrayOfParameters( + target, + propertyKey, + ); + const oldibe: SwaggerExample.IData | undefined = array.find( + (e) => e.index === index, + ); + if (oldibe !== undefined) return oldibe; + + const data: SwaggerExample.IData = { index }; + array.push(data); + return data; +}; + +const emplaceArrayOfParameters = ( + target: Object, + propertyKey: string | symbol, +): SwaggerExample.IData[] => { + const array: SwaggerExample.IData[] | undefined = Reflect.getMetadata( + "nestia/SwaggerExample/Parameters", + target, + propertyKey, + ); + if (array !== undefined) return array; + const newbie: SwaggerExample.IData[] = []; + Reflect.defineMetadata( + "nestia/SwaggerExample/Parameters", + newbie, + target, + propertyKey, + ); + return newbie; +}; diff --git a/packages/core/src/decorators/WebSocketRoute.ts b/packages/core/src/decorators/WebSocketRoute.ts index e85281f18..1881b6f72 100644 --- a/packages/core/src/decorators/WebSocketRoute.ts +++ b/packages/core/src/decorators/WebSocketRoute.ts @@ -48,7 +48,7 @@ export function WebSocketRoute( _target: Object, _propertyKey: string | symbol, descriptor: TypedPropertyDescriptor, - ) { + ): TypedPropertyDescriptor { Reflect.defineMetadata( "nestia/WebSocketRoute", { diff --git a/packages/core/src/module.ts b/packages/core/src/module.ts index 7e206055f..b0cf800af 100644 --- a/packages/core/src/module.ts +++ b/packages/core/src/module.ts @@ -15,6 +15,7 @@ export * from "./decorators/EncryptedBody"; export * from "./decorators/EncryptedModule"; export * from "./decorators/PlainBody"; export * from "./decorators/SwaggerCustomizer"; +export * from "./decorators/SwaggerExample"; export * from "./adaptors/WebSocketAdaptor"; export * from "./decorators/WebSocketRoute"; diff --git a/packages/fetcher/package.json b/packages/fetcher/package.json index a4e969731..95fe1907d 100644 --- a/packages/fetcher/package.json +++ b/packages/fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/fetcher", - "version": "3.7.0-dev.20240711", + "version": "3.7.0-dev.20240712", "description": "Fetcher library of Nestia SDK", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 84d1b7169..1a7cc9255 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/sdk", - "version": "3.7.0-dev.20240711", + "version": "3.7.0-dev.20240712", "description": "Nestia SDK and Swagger generator", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -32,8 +32,8 @@ }, "homepage": "https://nestia.io", "dependencies": { - "@nestia/fetcher": "^3.7.0-dev.20240711", - "@nestia/core": "^3.7.0-dev.20240711", + "@nestia/fetcher": "^3.7.0-dev.20240712", + "@nestia/core": "^3.7.0-dev.20240712", "@samchon/openapi": "^0.3.4", "cli": "^1.0.1", "get-function-location": "^2.0.0", @@ -47,8 +47,8 @@ "typia": "^6.4.3" }, "peerDependencies": { - "@nestia/fetcher": ">=3.7.0-dev.20240711", - "@nestia/core": ">=3.7.0-dev.20240711", + "@nestia/fetcher": ">=3.7.0-dev.20240712", + "@nestia/core": ">=3.7.0-dev.20240712", "@nestjs/common": ">=7.0.1", "@nestjs/core": ">=7.0.1", "reflect-metadata": ">=0.1.12", diff --git a/packages/sdk/src/analyses/ReflectHttpOperationAnalyzer.ts b/packages/sdk/src/analyses/ReflectHttpOperationAnalyzer.ts index 42f5ba116..0524195ea 100644 --- a/packages/sdk/src/analyses/ReflectHttpOperationAnalyzer.ts +++ b/packages/sdk/src/analyses/ReflectHttpOperationAnalyzer.ts @@ -1,3 +1,4 @@ +import { SwaggerExample } from "@nestia/core"; import { HEADERS_METADATA, HTTP_CODE_METADATA, @@ -65,6 +66,19 @@ export namespace ReflectHttpOperationAnalyzer { _Analyze_http_parameter(...tuple); if (child !== null) output.push(child); } + const examples: SwaggerExample.IData[] = + Reflect.getMetadata( + "nestia/SwaggerExample/Parameters", + props.controller.prototype, + props.name, + ) ?? []; + for (const p of output) { + const e = examples.find((elem) => elem.index === p.index); + if (e !== undefined) { + p.example = e.example; + p.examples = e.examples; + } + } return output.sort((x, y) => x.index - y.index); })(); @@ -82,6 +96,8 @@ export namespace ReflectHttpOperationAnalyzer { } // DO CONSTRUCT + const example: SwaggerExample.IData | undefined = + Reflect.getMetadata("nestia/SwaggerExample/Response", props.function); const meta: IReflectHttpOperation = { protocol: "http", function: props.function, @@ -122,6 +138,9 @@ export namespace ReflectHttpOperationAnalyzer { []), ]), ], + ...(example + ? { example: example.example, examples: example.examples } + : {}), }; // VALIDATE PATH ARGUMENTS diff --git a/packages/sdk/src/analyses/TypedHttpOperationAnalyzer.ts b/packages/sdk/src/analyses/TypedHttpOperationAnalyzer.ts index 1b4614704..7038f3add 100644 --- a/packages/sdk/src/analyses/TypedHttpOperationAnalyzer.ts +++ b/packages/sdk/src/analyses/TypedHttpOperationAnalyzer.ts @@ -183,6 +183,8 @@ export namespace TypedHttpOperationAnalyzer { ), security, exceptions, + example: props.operation.example, + examples: props.operation.examples, }; // CONFIGURE PATHS diff --git a/packages/sdk/src/generates/internal/SwaggerSchemaGenerator.ts b/packages/sdk/src/generates/internal/SwaggerSchemaGenerator.ts index 6751fadca..8e15751bc 100644 --- a/packages/sdk/src/generates/internal/SwaggerSchemaGenerator.ts +++ b/packages/sdk/src/generates/internal/SwaggerSchemaGenerator.ts @@ -143,6 +143,10 @@ export namespace SwaggerSchemaGenerator { : { [route.output.contentType]: { schema: coalesce(props)(result), + ...{ + example: route.example, + examples: route.examples, + }, }, }, ...(props.config.additional === true @@ -205,6 +209,10 @@ export namespace SwaggerSchemaGenerator { content: { [contentType]: { schema, + ...{ + example: param.example, + examples: param.examples, + }, }, }, required: true, @@ -365,6 +373,10 @@ export namespace SwaggerSchemaGenerator { jsDocTags: p.jsDocTags, kind: "title", }), + ...{ + example: p.example, + examples: p.examples, + }, }; }; diff --git a/packages/sdk/src/structures/IReflectHttpOperation.ts b/packages/sdk/src/structures/IReflectHttpOperation.ts index 8ba1b6dea..c01e44e07 100644 --- a/packages/sdk/src/structures/IReflectHttpOperation.ts +++ b/packages/sdk/src/structures/IReflectHttpOperation.ts @@ -19,6 +19,8 @@ export interface IReflectHttpOperation { number | "2XX" | "3XX" | "4XX" | "5XX", IReflectHttpOperation.IException >; + example?: any; + examples?: Record; swaggerTags: string[]; } export namespace IReflectHttpOperation { @@ -34,6 +36,8 @@ export namespace IReflectHttpOperation { index: number; name: string; field: string | undefined; + example?: any; + examples?: Record; } export interface IHeadersParameter { custom: true; @@ -41,6 +45,8 @@ export namespace IReflectHttpOperation { index: number; name: string; field: string | undefined; + example?: any; + examples?: Record; } export interface IQueryParameter { custom: true; @@ -48,6 +54,8 @@ export namespace IReflectHttpOperation { index: number; name: string; field: string | undefined; + example?: any; + examples?: Record; } export interface IBodyParameter { custom: true; @@ -61,6 +69,8 @@ export namespace IReflectHttpOperation { | "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain"; + example?: any; + examples?: Record; } export interface IPathParameter { custom: true; @@ -68,6 +78,8 @@ export namespace IReflectHttpOperation { index: number; name: string; field: string | undefined; + example?: any; + examples?: Record; } export interface IException { diff --git a/packages/sdk/src/structures/ITypedHttpRoute.ts b/packages/sdk/src/structures/ITypedHttpRoute.ts index 462570512..d034d5772 100644 --- a/packages/sdk/src/structures/ITypedHttpRoute.ts +++ b/packages/sdk/src/structures/ITypedHttpRoute.ts @@ -32,6 +32,8 @@ export interface ITypedHttpRoute { number | "2XX" | "3XX" | "4XX" | "5XX", ITypedHttpRoute.IOutput >; + example?: any; + examples?: Record; swaggerTags: string[]; } diff --git a/test/features/security/src/controllers/SecurityController.ts b/test/features/security/src/controllers/SecurityController.ts index d70d4b63b..3ae39f1c3 100644 --- a/test/features/security/src/controllers/SecurityController.ts +++ b/test/features/security/src/controllers/SecurityController.ts @@ -4,7 +4,6 @@ import { ApiBasicAuth, ApiBearerAuth, ApiOAuth2, - ApiSecurity, } from "@nestjs/swagger"; import typia from "typia"; diff --git a/test/features/swagger-example/nestia.config.ts b/test/features/swagger-example/nestia.config.ts new file mode 100644 index 000000000..47ca3e019 --- /dev/null +++ b/test/features/swagger-example/nestia.config.ts @@ -0,0 +1,16 @@ +import { INestiaConfig } from "@nestia/sdk"; + +export const NESTIA_CONFIG: INestiaConfig = { + input: ["src/controllers"], + output: "src/api", + swagger: { + output: "swagger.json", + beautify: true, + security: { + bearer: { + type: "apiKey", + }, + }, + }, +}; +export default NESTIA_CONFIG; diff --git a/test/features/swagger-example/src/Backend.ts b/test/features/swagger-example/src/Backend.ts new file mode 100644 index 000000000..be2c2d840 --- /dev/null +++ b/test/features/swagger-example/src/Backend.ts @@ -0,0 +1,27 @@ +import core from "@nestia/core"; +import { INestApplication } from "@nestjs/common"; +import { NestFactory } from "@nestjs/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/swagger-example/src/api/HttpError.ts b/test/features/swagger-example/src/api/HttpError.ts new file mode 100644 index 000000000..5df328ae4 --- /dev/null +++ b/test/features/swagger-example/src/api/HttpError.ts @@ -0,0 +1 @@ +export { HttpError } from "@nestia/fetcher"; diff --git a/test/features/swagger-example/src/api/IConnection.ts b/test/features/swagger-example/src/api/IConnection.ts new file mode 100644 index 000000000..107bdb8f8 --- /dev/null +++ b/test/features/swagger-example/src/api/IConnection.ts @@ -0,0 +1 @@ +export type { IConnection } from "@nestia/fetcher"; diff --git a/test/features/swagger-example/src/api/Primitive.ts b/test/features/swagger-example/src/api/Primitive.ts new file mode 100644 index 000000000..60d394424 --- /dev/null +++ b/test/features/swagger-example/src/api/Primitive.ts @@ -0,0 +1 @@ +export type { Primitive } from "@nestia/fetcher"; diff --git a/test/features/swagger-example/src/api/functional/bbs/articles/index.ts b/test/features/swagger-example/src/api/functional/bbs/articles/index.ts new file mode 100644 index 000000000..92535e89a --- /dev/null +++ b/test/features/swagger-example/src/api/functional/bbs/articles/index.ts @@ -0,0 +1,112 @@ +/** + * @packageDocumentation + * @module api.functional.bbs.articles + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import type { Format } from "typia/lib/tags/Format"; + +import type { IBbsArticle } from "../../../structures/IBbsArticle"; + +/** + * Create an article. + * + * @param input Content to store + * @returns Newly archived article + * @author Samchon + * @warning This is an fake API + * + * @controller BbsArticlesController.create + * @path POST /bbs/articles + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function create( + connection: IConnection, + input: create.Input, +): Promise { + return PlainFetcher.fetch( + { + ...connection, + headers: { + ...connection.headers, + "Content-Type": "application/json", + }, + }, + { + ...create.METADATA, + template: create.METADATA.path, + path: create.path(), + }, + input, + ); +} +export namespace create { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "POST", + path: "/bbs/articles", + request: { + type: "application/json", + encrypted: false, + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = () => "/bbs/articles"; +} + +/** + * @controller BbsArticlesController.update + * @path PUT /bbs/articles/:id + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function update( + connection: IConnection, + id: string & Format<"uuid">, + input: update.Input, +): Promise { + return PlainFetcher.fetch( + { + ...connection, + headers: { + ...connection.headers, + "Content-Type": "application/json", + }, + }, + { + ...update.METADATA, + template: update.METADATA.path, + path: update.path(id), + }, + input, + ); +} +export namespace update { + export type Input = Primitive>; + export type Output = Primitive; + + export const METADATA = { + method: "PUT", + path: "/bbs/articles/:id", + request: { + type: "application/json", + encrypted: false, + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (id: string & Format<"uuid">) => + `/bbs/articles/${encodeURIComponent(id ?? "null")}`; +} diff --git a/test/features/swagger-example/src/api/functional/bbs/index.ts b/test/features/swagger-example/src/api/functional/bbs/index.ts new file mode 100644 index 000000000..7a891f888 --- /dev/null +++ b/test/features/swagger-example/src/api/functional/bbs/index.ts @@ -0,0 +1,7 @@ +/** + * @packageDocumentation + * @module api.functional.bbs + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +export * as articles from "./articles"; diff --git a/test/features/swagger-example/src/api/functional/index.ts b/test/features/swagger-example/src/api/functional/index.ts new file mode 100644 index 000000000..26113199a --- /dev/null +++ b/test/features/swagger-example/src/api/functional/index.ts @@ -0,0 +1,7 @@ +/** + * @packageDocumentation + * @module api.functional + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +export * as bbs from "./bbs"; diff --git a/test/features/swagger-example/src/api/index.ts b/test/features/swagger-example/src/api/index.ts new file mode 100644 index 000000000..1705f43c8 --- /dev/null +++ b/test/features/swagger-example/src/api/index.ts @@ -0,0 +1,4 @@ +import * as api from "./module"; + +export * from "./module"; +export default api; diff --git a/test/features/swagger-example/src/api/module.ts b/test/features/swagger-example/src/api/module.ts new file mode 100644 index 000000000..dbb6e9a51 --- /dev/null +++ b/test/features/swagger-example/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/swagger-example/src/api/structures/IBbsArticle.ts b/test/features/swagger-example/src/api/structures/IBbsArticle.ts new file mode 100644 index 000000000..74fa6e1d8 --- /dev/null +++ b/test/features/swagger-example/src/api/structures/IBbsArticle.ts @@ -0,0 +1,43 @@ +export interface IBbsArticle extends IBbsArticle.ICreate { + /** + * @format uuid + */ + id: string; + + /** + * @format date-time + */ + created_at: string; +} +export namespace IBbsArticle { + export interface ICreate { + /** + * @minLength 3 + * @maxLength 50 + */ + title: string; + body: string; + files: IAttachmentFile[]; + } + + export type IUpdate = Partial; +} + +export interface IAttachmentFile { + /** + * @minLengt 1 + * @maxLength 255 + */ + name: string | null; + + /** + * @minLength 1 + * @maxLength 8 + */ + extension: string | null; + + /** + * @format uri + */ + url: string; +} diff --git a/test/features/swagger-example/src/controllers/BbsArticlesController.ts b/test/features/swagger-example/src/controllers/BbsArticlesController.ts new file mode 100644 index 000000000..3f3aa218d --- /dev/null +++ b/test/features/swagger-example/src/controllers/BbsArticlesController.ts @@ -0,0 +1,55 @@ +import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import typia, { tags } from "typia"; +import { v4 } from "uuid"; + +import { IBbsArticle } from "@api/lib/structures/IBbsArticle"; + +@Controller("bbs/articles") +export class BbsArticlesController { + /** + * Create an article. + * + * @param request Request object from express. Must be disappeared in SDK + * @param input Content to store + * @returns Newly archived article + * + * @author Samchon + * @warning This is an fake API + */ + @core.SwaggerExample.Response(typia.random()) + @core.TypedRoute.Post() + public async create( + @core.SwaggerExample.Parameter(typia.random()) + @core.SwaggerExample.Parameter("x", typia.random()) + @core.SwaggerExample.Parameter("y", typia.random()) + @core.SwaggerExample.Parameter("z", typia.random()) + @core.TypedBody() + input: IBbsArticle.ICreate, + ): Promise { + const output: IBbsArticle = { + ...typia.random(), + ...input, + }; + return output; + } + + @core.SwaggerExample.Response(typia.random()) + @core.SwaggerExample.Response("a", typia.random()) + @core.SwaggerExample.Response("b", typia.random()) + @core.TypedRoute.Put(":id") + public async update( + @core.SwaggerExample.Parameter(v4()) + @core.TypedParam("id") + id: string & tags.Format<"uuid">, + @core.SwaggerExample.Parameter(typia.random()) + @core.TypedBody() + input: IBbsArticle.IUpdate, + ): Promise { + return { + ...typia.random(), + ...input, + id, + }; + } +} diff --git a/test/features/swagger-example/src/test/features/test_swagger_example.ts b/test/features/swagger-example/src/test/features/test_swagger_example.ts new file mode 100644 index 000000000..56606af84 --- /dev/null +++ b/test/features/swagger-example/src/test/features/test_swagger_example.ts @@ -0,0 +1,54 @@ +import { OpenApi } from "@samchon/openapi"; +import fs from "fs"; +import typia, { tags } from "typia"; + +import { IBbsArticle } from "@api/lib/structures/IBbsArticle"; + +export const test_swagger_example = async (): Promise => { + const swagger: any = JSON.parse( + await fs.promises.readFile(`${__dirname}/../../../swagger.json`, "utf-8"), + ); + typia.assert(swagger); + + // BbsArticlesController.create() + typia.assert<{ + example: IBbsArticle; + }>( + swagger.paths["/bbs/articles"].post.responses["201"].content[ + "application/json" + ], + ); + typia.assert<{ + example: IBbsArticle.ICreate; + examples: { + x: IBbsArticle.ICreate; + y: IBbsArticle.ICreate; + z: IBbsArticle.ICreate; + }; + }>( + swagger.paths["/bbs/articles"].post.requestBody.content["application/json"], + ); + + // BbsArticlesController.update() + typia.assert<{ + example: IBbsArticle; + examples: { + a: IBbsArticle; + b: IBbsArticle; + }; + }>( + swagger.paths["/bbs/articles/{id}"].put.responses["200"].content[ + "application/json" + ], + ); + typia.assert<{ + example: string & tags.Format<"uuid">; + }>(swagger.paths["/bbs/articles/{id}"].put.parameters[0]); + typia.assert<{ + example: IBbsArticle.IUpdate; + }>( + swagger.paths["/bbs/articles/{id}"].put.requestBody.content[ + "application/json" + ], + ); +}; diff --git a/test/features/swagger-example/src/test/index.ts b/test/features/swagger-example/src/test/index.ts new file mode 100644 index 000000000..1a8e4372b --- /dev/null +++ b/test/features/swagger-example/src/test/index.ts @@ -0,0 +1,49 @@ +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), + }, + }, + ], + location: `${__dirname}/features`, + onComplete: (exec) => { + const elapsed: number = + new Date(exec.completed_at).getTime() - + new Date(exec.started_at).getTime(); + console.log(` - ${exec.name}: ${elapsed.toLocaleString()} ms`); + }, + }); + await server.close(); + + console.log(__dirname, __dirname.substring(__dirname.length - 2)); + + 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/swagger-example/swagger.json b/test/features/swagger-example/swagger.json new file mode 100644 index 000000000..aa5ef58c4 --- /dev/null +++ b/test/features/swagger-example/swagger.json @@ -0,0 +1,370 @@ +{ + "openapi": "3.1.0", + "servers": [ + { + "url": "https://github.com/samchon/nestia", + "description": "insert your server url" + } + ], + "info": { + "version": "3.7.0-dev.20240711", + "title": "@samchon/nestia-test", + "description": "Test program of Nestia", + "license": { + "name": "MIT" + } + }, + "paths": { + "/bbs/articles": { + "post": { + "tags": [], + "parameters": [], + "requestBody": { + "description": "Content to store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle.ICreate" + }, + "example": { + "title": "wanfi", + "body": "dihsfonqr", + "files": [ + { + "name": null, + "extension": null, + "url": "https://xkjzcsqgah.mac" + }, + { + "name": null, + "extension": "ib", + "url": "https://hjbkzabvyu.tif" + }, + { + "name": null, + "extension": null, + "url": "https://hgfibjpkhm.wqw" + } + ] + }, + "examples": { + "z": { + "title": "niuzm", + "body": "vjhlv", + "files": [ + { + "name": "kswkxztzbvpoxuupjqdtsvwfkqwmxzyvxysehydczvzwmmpdlaxsttxtnvezzzodlkpsdfsrhhbzuofrcmlettknvjdgdkkhqo", + "extension": "ew", + "url": "https://aqsgnjktsl.aca" + }, + { + "name": "gnwumdwsaldlmzhkgacrxrtvjyynwndltwmqfcxokhjpsqwrbedfjbsfzacdoicaqoxlqbtwntkdbanhnozeyfkcvjipdrokqgvvfbowtckgzsvdmcbhdelrvwmiqbonterexgsxxbeepqgsexiytgvxzniivekgpfikv", + "extension": null, + "url": "https://aggfmwklav.ijm" + }, + { + "name": "zgmfcdxldnprbxrlhqidfpzofjamgcyvzfrtlzquqjitlczyqnuqcmkbwxueerffstugbokobjgcrejzejbixsthbemqvwmkwbxhleafajqukfnucjauwdsneapgosjnxtjwlgzyaobbaxvgavwmjwhggiird", + "extension": "nyi", + "url": "https://sbufdobevq.gec" + } + ] + }, + "y": { + "title": "mzgy", + "body": "uymevmpdas", + "files": [ + { + "name": "ptouplqcgjcqitusvvhnszwowspyxcpvdmtdgnyhxyxydnca", + "extension": null, + "url": "https://verdfefmjz.xhg" + }, + { + "name": "mjitvwkigmahrcvdhrqobraiipcblptprqoozhetzzzlktfdc", + "extension": null, + "url": "https://jqqizhyzom.tte" + }, + { + "name": "reswhbdndrlcozgkwwywvkjgycowdoqneqwhopcvhrgtmalpbgfzhvrfwdbdixyhxdvtzzdwmciwpjyadcpnkoyksyqkpphhlirjxsmkvymigaqkaybmiavrrbaopobepjgkmvoaszilkkpdnjytdwatmsrvfiuodqdmmqharqekemjslifyawmckxcbzycyxatzaionxqxoyqnnbohscelnlimnjjbjfohcwyqydoud", + "extension": "y", + "url": "https://pjmpsvxacs.smz" + } + ] + }, + "x": { + "title": "tiwfhcbficjthjsvhkxvcrmk", + "body": "ksakxgg", + "files": [] + } + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Newly archived article", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle" + }, + "example": { + "id": "471cf11d-fe9a-44ec-95fe-9b36a177af20", + "created_at": "2024-07-12T03:10:03.714Z", + "title": "hbojqusfxdxvdeumyugbt", + "body": "bbjwbm", + "files": [ + { + "name": null, + "extension": null, + "url": "https://tdregslzcx.rbw" + }, + { + "name": "ngzzatngkilxidnygwarylmeootqeptfciryqazlsonievuxwargfvoisyhwvaoiiprxqgzysyxynxlkvylwkanhcztsqostlhtrecxuullppeszurangxxqwgjxjfqsippawrugziwdotpqpztvfpvzjvopspxxrpiaogongkdgati", + "extension": null, + "url": "https://sfmxgrvujc.dpk" + } + ] + } + } + } + } + }, + "summary": "Create an article", + "description": "Create an article." + } + }, + "/bbs/articles/{id}": { + "put": { + "tags": [], + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "example": "67595ae0-0f6a-4cc6-a0a3-2ad88d186199" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartialIBbsArticle.ICreate" + }, + "example": { + "title": "yysmaicfrvejhzg", + "files": [] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IBbsArticle" + }, + "example": { + "id": "e313fb9a-5105-43e2-8f8c-fd625afb667b", + "created_at": "2024-06-30T01:01:50.475Z", + "title": "ogrjjzcaanrypgenncvdnlehukaoaj", + "body": "iyduciryf", + "files": [ + { + "name": null, + "extension": null, + "url": "https://wnwxqgwygx.vwq" + }, + { + "name": null, + "extension": null, + "url": "https://bknprzutya.kgf" + }, + { + "name": "znhhzvhymyuybosvhteocafvhigeeufyyouasqxvxwsbwnjxwohriihuwbirgrzfvby", + "extension": null, + "url": "https://rdxqfffvss.orf" + } + ] + }, + "examples": { + "b": { + "id": "9624d317-b124-4cbc-8ee0-4ce817df45e2", + "created_at": "2024-07-13T07:30:24.155Z", + "title": "prxcmgnwiohign", + "body": "ilvyb", + "files": [ + { + "name": null, + "extension": null, + "url": "https://bkmcmcvanp.toi" + }, + { + "name": null, + "extension": "xqyu", + "url": "https://pqiytxcovf.fnj" + }, + { + "name": "qxsiowaknproxtrmecuwwdsenrteikthzughrnkjlgyorwsdqjrz", + "extension": "oqnyw", + "url": "https://ciavlfjiyo.mju" + } + ] + }, + "a": { + "id": "b1637c66-155a-4e65-9f7f-2c9774ac14d7", + "created_at": "2024-07-12T20:26:05.146Z", + "title": "xahjxavbornuphosaqojvi", + "body": "kenhbvfz", + "files": [ + { + "name": "fxpbzvjffasupllrbzmwlgsdbryyybgfhnfgqvvgshvpweafebhvtagtcyoruneimzxwsrniqviqefbcbydsnmtilpnawoacaqrqlenypnmjzzgggwwvuixxdcybeppzmcvdshzvjmegxzjuybyxcbdeuksgyptbnhcrmfxcslebhcjzsutgdhfktbvceusaeezbvijppsagyrxifvxvxfddleklhcebdvlyjogjfnabxddnaicwfvisxaywu", + "extension": "dydhfuyx", + "url": "https://byoyitpyli.vjq" + } + ] + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "IBbsArticle.ICreate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "body": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IAttachmentFile" + } + } + }, + "required": [ + "title", + "body", + "files" + ] + }, + "IAttachmentFile": { + "type": "object", + "properties": { + "name": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "maxLength": 255 + } + ] + }, + "extension": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1, + "maxLength": 8 + } + ] + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "extension", + "url" + ] + }, + "IBbsArticle": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "body": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IAttachmentFile" + } + } + }, + "required": [ + "id", + "created_at", + "title", + "body", + "files" + ] + }, + "PartialIBbsArticle.ICreate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "body": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IAttachmentFile" + } + } + }, + "description": "Make all properties in T optional" + } + }, + "securitySchemes": { + "bearer": { + "type": "apiKey", + "in": "header", + "name": "Authorization" + } + } + }, + "tags": [], + "x-samchon-emended": true +} \ No newline at end of file diff --git a/test/features/swagger-example/tsconfig.json b/test/features/swagger-example/tsconfig.json new file mode 100644 index 000000000..c33dfa28f --- /dev/null +++ b/test/features/swagger-example/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 42b0b0ba9..388d5c2fe 100644 --- a/test/package.json +++ b/test/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@samchon/nestia-test", - "version": "3.7.0-dev.20240711", + "version": "3.7.0-dev.20240712", "description": "Test program of Nestia", "main": "index.js", "scripts": { @@ -26,7 +26,7 @@ }, "homepage": "https://nestia.io", "devDependencies": { - "@nestia/sdk": "^3.7.0-dev.20240711", + "@nestia/sdk": "^3.7.0-dev.20240712", "@nestjs/swagger": "^7.1.2", "@samchon/openapi": "^0.3.4", "@types/express": "^4.17.17", @@ -40,9 +40,9 @@ }, "dependencies": { "@fastify/multipart": "^8.1.0", - "@nestia/core": "^3.7.0-dev.20240711", + "@nestia/core": "^3.7.0-dev.20240712", "@nestia/e2e": "^0.7.0", - "@nestia/fetcher": "^3.7.0-dev.20240711", + "@nestia/fetcher": "^3.7.0-dev.20240712", "@nestjs/common": "^10.3.5", "@nestjs/core": "^10.3.5", "@nestjs/platform-express": "^10.3.5",