diff --git a/packages/core/package.json b/packages/core/package.json index 1bffb8e41..9c94d159b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/core", - "version": "1.2.2", + "version": "1.2.3", "description": "Super-fast validation decorators of NestJS", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/core/src/decorators/TypedParam.ts b/packages/core/src/decorators/TypedParam.ts index 973c05b65..2da8f257b 100644 --- a/packages/core/src/decorators/TypedParam.ts +++ b/packages/core/src/decorators/TypedParam.ts @@ -39,7 +39,7 @@ import { send_bad_request } from "./internal/send_bad_request"; */ export function TypedParam( name: string, - type?: "boolean" | "number" | "string" | "uuid", + type?: "boolean" | "number" | "string" | "uuid" | "date", nullable?: false | true, ): ParameterDecorator { function TypedParam({}: any, context: ExecutionContext) { @@ -73,7 +73,16 @@ export function TypedParam( ), ); return str; - } else return str; + } else if (type === "date") { + if (DATE_PATTERN.test(str) === false) + return send_bad_request(context)( + new BadRequestException( + `Value of the URL parameter "${name}" is not a valid date.`, + ), + ); + return str; + } + else return str; } (TypedParam as any).nullable = !!nullable; (TypedParam as any).type = type; @@ -82,3 +91,4 @@ export function TypedParam( const UUID_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; +const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; \ No newline at end of file diff --git a/packages/core/src/programmers/TypedParamProgrammer.ts b/packages/core/src/programmers/TypedParamProgrammer.ts index 535410c8d..01dae28d4 100644 --- a/packages/core/src/programmers/TypedParamProgrammer.ts +++ b/packages/core/src/programmers/TypedParamProgrammer.ts @@ -91,6 +91,7 @@ const error = (message: string) => const equals = (atomic: string, p: Metadata) => { const name: string = p.getName(); - if (atomic === "string") return name === `"string"` || name === `"uuid"`; + if (atomic === "string") + return name === `"string"` || name === `"uuid"` || name === `"date"`; return `"${atomic}"` === name; }; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 806945f7e..0bf45cb70 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/sdk", - "version": "1.2.7", + "version": "1.2.8", "description": "Nestia SDK and Swagger generator", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/sdk/src/generates/SwaggerGenerator.ts b/packages/sdk/src/generates/SwaggerGenerator.ts index ae965fb2e..560118601 100644 --- a/packages/sdk/src/generates/SwaggerGenerator.ts +++ b/packages/sdk/src/generates/SwaggerGenerator.ts @@ -26,6 +26,16 @@ export namespace SwaggerGenerator { // PREPARE ASSETS const parsed: NodePath.ParsedPath = NodePath.parse(config.output); + const directory: string = NodePath.dirname(parsed.dir); + if (fs.existsSync(directory) === false) + try { + await fs.promises.mkdir(directory); + } catch {} + if (fs.existsSync(directory) === false) + throw new Error( + `Error on NestiaApplication.swagger(): failed to create output directory: ${directory}`, + ); + const location: string = !!parsed.ext ? NodePath.resolve(config.output) : NodePath.join( @@ -283,7 +293,7 @@ export namespace SwaggerGenerator { ); if (schema === null) throw new Error( - `Error on NestiaApplication.sdk(): invalid parameter type on ${route.symbol}#${parameter.name}`, + `Error on NestiaApplication.swagger(): invalid parameter type on ${route.symbol}#${parameter.name}`, ); return { diff --git a/packages/sdk/src/generates/internal/E2eFileProgrammer.ts b/packages/sdk/src/generates/internal/E2eFileProgrammer.ts index 77077fc7a..9862f91eb 100644 --- a/packages/sdk/src/generates/internal/E2eFileProgrammer.ts +++ b/packages/sdk/src/generates/internal/E2eFileProgrammer.ts @@ -15,9 +15,11 @@ export namespace E2eFileProgrammer { for (const instance of tuple[1]) importDict.emplace(tuple[0], false, instance); - const uuid: boolean = route.parameters.some( - (p) => p.category === "param" && p.meta?.type === "uuid", - ); + const additional: string[] = []; + for (const param of route.parameters) + if (param.category === "param") + if (param.meta?.type === "uuid") additional.push(UUID); + else if (param.meta?.type === "date") additional.push(DATE); const content: string = [ ...(route.parameters.length || route.output.name !== "void" ? [ @@ -36,7 +38,7 @@ export namespace E2eFileProgrammer { : [importDict.toScript(props.current)]), "", arrow(config)(route), - ...(uuid ? ["", UUID] : []), + ...(additional.length ? ["", ...additional] : []), ].join("\n"); await fs.promises.writeFile( @@ -78,10 +80,11 @@ export namespace E2eFileProgrammer { (tab: number) => (param: IRoute.IParameter): string => { const middle: string = - param.category === "param" && param.meta?.type === "uuid" + param.category === "param" && + (param.meta?.type === "uuid" || param.meta?.type === "date") ? param.meta.nullable - ? `Math.random() < .2 ? null : uuid()` - : `uuid()` + ? `Math.random() < .2 ? null : ${param.meta.type}()` + : `${param.meta.type}()` : `typia.random<${primitive(config)(param.type.name)}>()`; return `${" ".repeat(4 * tab)}${middle},`; }; @@ -122,3 +125,11 @@ const UUID = `const uuid = (): string => const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); });`; +const DATE = `const date = (): string => { + const date: Date = new Date(Math.floor(Math.random() * Date.now() * 2)); + return [ + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, "0"), + date.getDate().toString().padStart(2, "0"), + ].join("-"); +}`; diff --git a/test/features/param/src/api/functional/param/index.ts b/test/features/param/src/api/functional/param/index.ts index 9900d8e79..56233cb5c 100644 --- a/test/features/param/src/api/functional/param/index.ts +++ b/test/features/param/src/api/functional/param/index.ts @@ -185,4 +185,148 @@ export namespace literal { return `/param/${encodeURIComponent(value ?? "null")}/literal`; } +} + +/** + * @controller TypedParamController.uuid() + * @path GET /param/:value/uuid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export function uuid + ( + connection: IConnection, + value: string + ): Promise +{ + return Fetcher.fetch + ( + connection, + uuid.ENCRYPTED, + uuid.METHOD, + uuid.path(value) + ); +} +export namespace uuid +{ + export type Output = Primitive; + + export const METHOD = "GET" as const; + export const PATH: string = "/param/:value/uuid"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export function path(value: string): string + { + return `/param/${encodeURIComponent(value ?? "null")}/uuid`; + } +} + +/** + * @controller TypedParamController.date() + * @path GET /param/:value/date + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export function date + ( + connection: IConnection, + value: string + ): Promise +{ + return Fetcher.fetch + ( + connection, + date.ENCRYPTED, + date.METHOD, + date.path(value) + ); +} +export namespace date +{ + export type Output = Primitive; + + export const METHOD = "GET" as const; + export const PATH: string = "/param/:value/date"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export function path(value: string): string + { + return `/param/${encodeURIComponent(value ?? "null")}/date`; + } +} + +/** + * @controller TypedParamController.uuid_nullable() + * @path GET /param/:value/uuid_nullable + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export function uuid_nullable + ( + connection: IConnection, + value: null | string + ): Promise +{ + return Fetcher.fetch + ( + connection, + uuid_nullable.ENCRYPTED, + uuid_nullable.METHOD, + uuid_nullable.path(value) + ); +} +export namespace uuid_nullable +{ + export type Output = Primitive; + + export const METHOD = "GET" as const; + export const PATH: string = "/param/:value/uuid_nullable"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export function path(value: null | string): string + { + return `/param/${encodeURIComponent(value ?? "null")}/uuid_nullable`; + } +} + +/** + * @controller TypedParamController.date_nullable() + * @path GET /param/:value/date_nullable + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export function date_nullable + ( + connection: IConnection, + value: null | string + ): Promise +{ + return Fetcher.fetch + ( + connection, + date_nullable.ENCRYPTED, + date_nullable.METHOD, + date_nullable.path(value) + ); +} +export namespace date_nullable +{ + export type Output = Primitive; + + export const METHOD = "GET" as const; + export const PATH: string = "/param/:value/date_nullable"; + export const ENCRYPTED: Fetcher.IEncrypted = { + request: false, + response: false, + }; + + export function path(value: null | string): string + { + return `/param/${encodeURIComponent(value ?? "null")}/date_nullable`; + } } \ No newline at end of file diff --git a/test/features/param/src/controllers/TypedParamController.ts b/test/features/param/src/controllers/TypedParamController.ts index b2dd97912..178f0ba20 100644 --- a/test/features/param/src/controllers/TypedParamController.ts +++ b/test/features/param/src/controllers/TypedParamController.ts @@ -32,4 +32,28 @@ export class TypedParamController { ): "A" | "B" | "C" { return value; } + + @core.TypedRoute.Get(":value/uuid") + public uuid(@core.TypedParam("value", "uuid") value: string): string { + return value; + } + + @core.TypedRoute.Get(":value/date") + public date(@core.TypedParam("value", "date") value: string): string { + return value; + } + + @core.TypedRoute.Get(":value/uuid_nullable") + public uuid_nullable( + @core.TypedParam("value", "uuid") value: string | null, + ): string | null { + return value; + } + + @core.TypedRoute.Get(":value/date_nullable") + public date_nullable( + @core.TypedParam("value", "date") value: string | null, + ): string | null { + return value; + } } diff --git a/test/features/param/src/test/features/api/automated/test_api_param_date.ts b/test/features/param/src/test/features/api/automated/test_api_param_date.ts new file mode 100644 index 000000000..acf3c43be --- /dev/null +++ b/test/features/param/src/test/features/api/automated/test_api_param_date.ts @@ -0,0 +1,23 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; + +export const test_api_param_date = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.param.date( + connection, + date(), + ); + typia.assert(output); +}; + +const date = (): string => { + const date: Date = new Date(Math.floor(Math.random() * Date.now() * 2)); + return [ + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, "0"), + date.getDate().toString().padStart(2, "0"), + ].join("-"); +} \ No newline at end of file diff --git a/test/features/param/src/test/features/api/automated/test_api_param_date_nullable.ts b/test/features/param/src/test/features/api/automated/test_api_param_date_nullable.ts new file mode 100644 index 000000000..a92247d30 --- /dev/null +++ b/test/features/param/src/test/features/api/automated/test_api_param_date_nullable.ts @@ -0,0 +1,23 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; + +export const test_api_param_date_nullable = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.param.date_nullable( + connection, + Math.random() < .2 ? null : date(), + ); + typia.assert(output); +}; + +const date = (): string => { + const date: Date = new Date(Math.floor(Math.random() * Date.now() * 2)); + return [ + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, "0"), + date.getDate().toString().padStart(2, "0"), + ].join("-"); +} \ No newline at end of file diff --git a/test/features/param/src/test/features/api/automated/test_api_param_uuid.ts b/test/features/param/src/test/features/api/automated/test_api_param_uuid.ts new file mode 100644 index 000000000..555f1b76f --- /dev/null +++ b/test/features/param/src/test/features/api/automated/test_api_param_uuid.ts @@ -0,0 +1,21 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; + +export const test_api_param_uuid = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.param.uuid( + connection, + uuid(), + ); + typia.assert(output); +}; + +const uuid = (): string => + "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); \ No newline at end of file diff --git a/test/features/param/src/test/features/api/automated/test_api_param_uuid_nullable.ts b/test/features/param/src/test/features/api/automated/test_api_param_uuid_nullable.ts new file mode 100644 index 000000000..bc0ccb216 --- /dev/null +++ b/test/features/param/src/test/features/api/automated/test_api_param_uuid_nullable.ts @@ -0,0 +1,21 @@ +import typia, { Primitive } from "typia"; + +import api from "./../../../../api"; + +export const test_api_param_uuid_nullable = async ( + connection: api.IConnection +): Promise => { + const output: Primitive = + await api.functional.param.uuid_nullable( + connection, + Math.random() < .2 ? null : uuid(), + ); + typia.assert(output); +}; + +const uuid = (): string => + "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); \ No newline at end of file diff --git a/test/features/param/src/test/features/api/test_api_param_date.ts b/test/features/param/src/test/features/api/test_api_param_date.ts new file mode 100644 index 000000000..0249f9ed4 --- /dev/null +++ b/test/features/param/src/test/features/api/test_api_param_date.ts @@ -0,0 +1,27 @@ +import { TestValidator } from "@nestia/e2e"; + +import api from "@api"; + +export const test_api_param_date = async ( + connection: api.IConnection, +): Promise => { + const date = random(); + const value = await api.functional.param.date(connection, date); + TestValidator.equals("date")(date)(value); + + await TestValidator.error("null")(() => + api.functional.param.date(connection, null!), + ); + await TestValidator.error("invalid")(() => + api.functional.param.date(connection, "20140102"), + ); +}; + +const random = () => { + const date: Date = new Date(Math.floor(Math.random() * Date.now() * 2)); + return [ + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, "0"), + date.getDate().toString().padStart(2, "0"), + ].join("-"); +}; diff --git a/test/features/param/src/test/features/api/test_api_param_date_nullable.ts b/test/features/param/src/test/features/api/test_api_param_date_nullable.ts new file mode 100644 index 000000000..36c03fdd8 --- /dev/null +++ b/test/features/param/src/test/features/api/test_api_param_date_nullable.ts @@ -0,0 +1,28 @@ +import { TestValidator } from "@nestia/e2e"; + +import api from "@api"; + +export const test_api_param_date_nullable = async ( + connection: api.IConnection, +): Promise => { + const date = random(); + const value = await api.functional.param.date_nullable(connection, date); + TestValidator.equals("date")(date)(value!); + + TestValidator.equals("null")( + await api.functional.param.date_nullable(connection, null), + )(null); + + await TestValidator.error("invalid")(() => + api.functional.param.date_nullable(connection, "20140102"), + ); +}; + +const random = () => { + const date: Date = new Date(Math.floor(Math.random() * Date.now() * 2)); + return [ + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, "0"), + date.getDate().toString().padStart(2, "0"), + ].join("-"); +}; diff --git a/test/features/param/src/test/features/api/test_api_param_uuid.ts b/test/features/param/src/test/features/api/test_api_param_uuid.ts new file mode 100644 index 000000000..63e1a0d6d --- /dev/null +++ b/test/features/param/src/test/features/api/test_api_param_uuid.ts @@ -0,0 +1,20 @@ +import { v4 } from "uuid"; + +import { TestValidator } from "@nestia/e2e"; + +import api from "@api"; + +export const test_api_param_uuid = async ( + connection: api.IConnection, +): Promise => { + const uuid = v4(); + const value = await api.functional.param.uuid(connection, uuid); + TestValidator.equals("uuid")(uuid)(value); + + await TestValidator.error("null")(() => + api.functional.param.uuid(connection, null!), + ); + await TestValidator.error("invalid")(() => + api.functional.param.uuid(connection, "12345678"), + ); +}; diff --git a/test/features/param/src/test/features/api/test_api_param_uuid_nullable.ts b/test/features/param/src/test/features/api/test_api_param_uuid_nullable.ts new file mode 100644 index 000000000..a6f1ecb1c --- /dev/null +++ b/test/features/param/src/test/features/api/test_api_param_uuid_nullable.ts @@ -0,0 +1,21 @@ +import { v4 } from "uuid"; + +import { TestValidator } from "@nestia/e2e"; + +import api from "@api"; + +export const test_api_param_uuid_nullable = async ( + connection: api.IConnection, +): Promise => { + const uuid = v4(); + const value = await api.functional.param.uuid_nullable(connection, uuid); + TestValidator.equals("uuid")(uuid)(value!); + + TestValidator.equals("null")( + await api.functional.param.uuid_nullable(connection, null), + )(null); + + await TestValidator.error("invalid")(() => + api.functional.param.uuid_nullable(connection, "12345678"), + ); +}; diff --git a/test/features/param/swagger.json b/test/features/param/swagger.json index 570d89428..c7b10018a 100644 --- a/test/features/param/swagger.json +++ b/test/features/param/swagger.json @@ -213,6 +213,134 @@ "x-nestia-namespace": "param.literal.literal", "x-nestia-jsDocTags": [] } + }, + "/param/{value}/uuid": { + "get": { + "tags": [], + "parameters": [ + { + "name": "value", + "in": "path", + "description": "", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "x-nestia-encrypted": false + } + }, + "x-nestia-namespace": "param.uuid.uuid", + "x-nestia-jsDocTags": [] + } + }, + "/param/{value}/date": { + "get": { + "tags": [], + "parameters": [ + { + "name": "value", + "in": "path", + "description": "", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "x-nestia-encrypted": false + } + }, + "x-nestia-namespace": "param.date.date", + "x-nestia-jsDocTags": [] + } + }, + "/param/{value}/uuid_nullable": { + "get": { + "tags": [], + "parameters": [ + { + "name": "value", + "in": "path", + "description": "", + "schema": { + "type": "string", + "nullable": true + }, + "required": true + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string", + "nullable": true + } + } + }, + "x-nestia-encrypted": false + } + }, + "x-nestia-namespace": "param.uuid_nullable.uuid_nullable", + "x-nestia-jsDocTags": [] + } + }, + "/param/{value}/date_nullable": { + "get": { + "tags": [], + "parameters": [ + { + "name": "value", + "in": "path", + "description": "", + "schema": { + "type": "string", + "nullable": true + }, + "required": true + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string", + "nullable": true + } + } + }, + "x-nestia-encrypted": false + } + }, + "x-nestia-namespace": "param.date_nullable.date_nullable", + "x-nestia-jsDocTags": [] + } } }, "components": { diff --git a/website/pages/docs/core/TypedParam.mdx b/website/pages/docs/core/TypedParam.mdx index 50e9dcc46..e2c9efe39 100644 --- a/website/pages/docs/core/TypedParam.mdx +++ b/website/pages/docs/core/TypedParam.mdx @@ -6,7 +6,7 @@ import { Tabs, Tab } from 'nextra-theme-docs' export function TypedParam(name: string): ParameterDecorator; export function TypedParam( nane: string, - type: "uuid" + type: "uuid" | "date" ): ParameterDecorator; ``` @@ -16,7 +16,7 @@ Type safe path parameter decorator. It's almost same with original `@Param()` function of `NestJS`, however, `@TypedParam()` is more type safe. -As `@TypedParam()` can anlayze source code in the compilation level, it can specify parameter type by itself. Also, while `NestJS` cannot distinguish `nullable` type and consider every parameter value as a `string` type, `@TypedParam()` can do it. Furthermore, `@TypedParam()` provides special type `"uuid"`. +As `@TypedParam()` can anlayze source code in the compilation level, it can specify parameter type by itself. Also, while `NestJS` cannot distinguish `nullable` type and consider every parameter value as a `string` type, `@TypedParam()` can do it. Furthermore, `@TypedParam()` provides special type `"uuid"` and `"date"`. Let's read below example code, and see how `@TypedParam()` works. @@ -135,7 +135,7 @@ exports.ParametersController = ParametersController; Just call `@TypedParam()` function on the path paremeter, that's all. -If you want to special parameter type `"uuid"`, write it as the second argument. +If you want to special parameter type `"uuid"` or `"date"`, write it as the second argument. When wrong typed value comes, 400 bad request error would be thrown. @@ -149,6 +149,7 @@ When wrong typed value comes, 400 bad request error would be thrown. - `number` - `string` - `uuid` + - `date`: YYYY-MM-DD Also, `@TypedParam()` allows nullable like `number | null`, but undefindable type is not. diff --git a/website/public/sitemap-0.xml b/website/public/sitemap-0.xml index 24539b8d3..2a7f679ec 100644 --- a/website/public/sitemap-0.xml +++ b/website/public/sitemap-0.xml @@ -1,14 +1,14 @@ -https://nestia.io/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/core/TypedBody/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/core/TypedParam/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/core/TypedQuery/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/core/TypedRoute/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/pure/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/sdk/e2e/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/sdk/sdk/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/sdk/swagger/2023-05-21T12:06:17.244Zdaily0.7 -https://nestia.io/docs/setup/2023-05-21T12:06:17.244Zdaily0.7 +https://nestia.io/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/core/TypedBody/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/core/TypedParam/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/core/TypedQuery/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/core/TypedRoute/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/pure/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/sdk/e2e/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/sdk/sdk/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/sdk/swagger/2023-05-23T04:33:45.802Zdaily0.7 +https://nestia.io/docs/setup/2023-05-23T04:33:45.802Zdaily0.7 \ No newline at end of file