diff --git a/package.json b/package.json index d12b2d86e..21ceb54fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@nestia/station", - "version": "3.1.0-dev.20240429", + "version": "3.1.1", "description": "Nestia station", "scripts": { "build": "node build/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index 89bf602ee..59d0c8784 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/core", - "version": "3.1.0-dev.20240429", + "version": "3.1.1", "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.1.0-dev.20240429", + "@nestia/fetcher": "^3.1.1", "@nestjs/common": ">=7.0.1", "@nestjs/core": ">=7.0.1", "@samchon/openapi": "^0.1.21", @@ -52,7 +52,7 @@ "ws": "^7.5.3" }, "peerDependencies": { - "@nestia/fetcher": ">=3.1.0-dev.20240429", + "@nestia/fetcher": ">=3.1.1", "@nestjs/common": ">=7.0.1", "@nestjs/core": ">=7.0.1", "reflect-metadata": ">=0.1.12", diff --git a/packages/core/src/adaptors/WebSocketAdaptor.ts b/packages/core/src/adaptors/WebSocketAdaptor.ts index 67d919fde..9604cf126 100644 --- a/packages/core/src/adaptors/WebSocketAdaptor.ts +++ b/packages/core/src/adaptors/WebSocketAdaptor.ts @@ -80,7 +80,7 @@ export class WebSocketAdaptor { return; } } - await acceptor.reject(1002, `Cannot GET ${path}`); + await acceptor.reject(1002, `WebSocket API not found`); }, ), ); @@ -147,7 +147,7 @@ const visitApplication = async ( ` methods:`, ...e.methods.map((m) => [ - ` - name: ${m.name}:`, + ` - name: ${m.name}`, ` file: ${m.source}:${m.line}:${m.column}`, ` reasons:`, ...m.messages.map( @@ -205,7 +205,7 @@ const visitController = async (props: { : undefined, modulePrefix: props.modulePrefix, }; - for (const mk of Object.getOwnPropertyNames(controller.prototype).filter( + for (const mk of getOwnPropertyNames(controller.prototype).filter( (key) => key !== "constructor" && typeof controller.prototype[key] === "function", )) { @@ -373,6 +373,16 @@ const visitMethod = (props: { }; const wrapPaths = (value: string[]) => (value.length === 0 ? [""] : value); +const getOwnPropertyNames = (prototype: any): string[] => { + const result: Set = new Set(); + const iterate = (m: any) => { + if (m === null) return; + for (const k of Object.getOwnPropertyNames(m)) result.add(k); + iterate(Object.getPrototypeOf(m)); + }; + iterate(prototype); + return Array.from(result); +}; interface Entry { key: string; diff --git a/packages/core/src/decorators/WebSocketRoute.ts b/packages/core/src/decorators/WebSocketRoute.ts index 80253743d..e1e0fda4c 100644 --- a/packages/core/src/decorators/WebSocketRoute.ts +++ b/packages/core/src/decorators/WebSocketRoute.ts @@ -10,7 +10,7 @@ import { validate_request_query } from "./internal/validate_request_query"; /** * WebSocket route decorator. * - * `@WebSocketRoute` is a route decorator function for WebSocket routes. + * `@WebSocketRoute()` is a route decorator function for WebSocket routes. * If you want to define a WebSocket route with this `@WebSocketRoute` decorator, * please don't forget to call the {@link WebSocketAdaptor.upgrade} function * to the {@link INestApplication} instance. diff --git a/packages/core/src/transformers/WebSocketRouteTransformer.ts b/packages/core/src/transformers/WebSocketRouteTransformer.ts index 3f31890ce..aeec0be41 100644 --- a/packages/core/src/transformers/WebSocketRouteTransformer.ts +++ b/packages/core/src/transformers/WebSocketRouteTransformer.ts @@ -57,7 +57,8 @@ export namespace WebSocketRouteTransformer { if ( param.type ?.getText() - .split(".") + .split("<")[0] + ?.split(".") .at(-1) ?.startsWith("WebAcceptor") !== true ) @@ -67,8 +68,12 @@ export namespace WebSocketRouteTransformer { ); } else if (category === "Driver") { if ( - param.type?.getText().split(".").at(-1)?.startsWith("Driver") !== - true + param.type + ?.getText() + .split("<")[0] + ?.split(".") + .at(-1) + ?.startsWith("Driver") !== true ) report( param, diff --git a/packages/fetcher/package.json b/packages/fetcher/package.json index f9b108f93..1b39fd6c0 100644 --- a/packages/fetcher/package.json +++ b/packages/fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/fetcher", - "version": "3.1.0-dev.20240429", + "version": "3.1.1", "description": "Fetcher library of Nestia SDK", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/migrate/package.json b/packages/migrate/package.json index 13eb8075d..4a1657ca1 100644 --- a/packages/migrate/package.json +++ b/packages/migrate/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/migrate", - "version": "0.13.9", + "version": "0.13.10", "description": "Migration program from swagger to NestJS", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -34,9 +34,9 @@ }, "homepage": "https://nestia.io", "devDependencies": { - "@nestia/core": "^3.0.5", + "@nestia/core": "^3.1.1", "@nestia/e2e": "^0.4.3", - "@nestia/fetcher": "^3.0.4", + "@nestia/fetcher": "^3.1.1", "@nestjs/common": "^10.3.5", "@nestjs/core": "^10.3.5", "@nestjs/platform-express": "^10.3.5", @@ -58,7 +58,7 @@ "typescript-transform-paths": "^3.4.6" }, "dependencies": { - "@samchon/openapi": "^0.1.19", + "@samchon/openapi": "^0.1.21", "commander": "10.0.0", "inquirer": "8.2.5", "prettier": "^3.2.5", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 18bce5be9..43ce990e5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nestia/sdk", - "version": "3.1.0-dev.20240429", + "version": "3.1.1", "description": "Nestia SDK and Swagger generator", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -32,7 +32,7 @@ }, "homepage": "https://nestia.io", "dependencies": { - "@nestia/fetcher": "^3.1.0-dev.20240429", + "@nestia/fetcher": "^3.1.1", "@samchon/openapi": "^0.1.21", "cli": "^1.0.1", "get-function-location": "^2.0.0", @@ -46,7 +46,7 @@ "typia": "^6.0.3" }, "peerDependencies": { - "@nestia/fetcher": ">=3.1.0-dev.20240429", + "@nestia/fetcher": ">=3.1.1", "@nestjs/common": ">=7.0.1", "@nestjs/core": ">=7.0.1", "reflect-metadata": ">=0.1.12", diff --git a/packages/sdk/src/analyses/TypedWebSocketOperationAnalyzer.ts b/packages/sdk/src/analyses/TypedWebSocketOperationAnalyzer.ts index 3cf146527..ede4275ca 100644 --- a/packages/sdk/src/analyses/TypedWebSocketOperationAnalyzer.ts +++ b/packages/sdk/src/analyses/TypedWebSocketOperationAnalyzer.ts @@ -87,6 +87,7 @@ export namespace TypedWebSocketOperationAnalyzer { }`; })(), description: CommentFactory.description(props.symbol), + jsDocTags, }; // CONFIGURE PATHS diff --git a/packages/sdk/src/generates/internal/SdkHttpFunctionProgrammer.ts b/packages/sdk/src/generates/internal/SdkHttpFunctionProgrammer.ts index e764e328a..3cebba0c9 100644 --- a/packages/sdk/src/generates/internal/SdkHttpFunctionProgrammer.ts +++ b/packages/sdk/src/generates/internal/SdkHttpFunctionProgrammer.ts @@ -22,8 +22,8 @@ export namespace SdkHttpFunctionProgrammer { query: ITypedHttpRoute.IParameter | undefined; input: ITypedHttpRoute.IParameter | undefined; }, - ): ts.FunctionDeclaration => { - return ts.factory.createFunctionDeclaration( + ): ts.FunctionDeclaration => + ts.factory.createFunctionDeclaration( [ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword), @@ -68,7 +68,6 @@ export namespace SdkHttpFunctionProgrammer { true, ), ); - }; const write_body = (config: INestiaConfig) => diff --git a/packages/sdk/src/generates/internal/SdkWebSocketRouteProgrammer.ts b/packages/sdk/src/generates/internal/SdkWebSocketRouteProgrammer.ts index ec9f2eb6b..55d95f3c8 100644 --- a/packages/sdk/src/generates/internal/SdkWebSocketRouteProgrammer.ts +++ b/packages/sdk/src/generates/internal/SdkWebSocketRouteProgrammer.ts @@ -1,8 +1,10 @@ import ts from "typescript"; +import { IJsDocTagInfo } from "typia"; import { IdentifierFactory } from "typia/lib/factories/IdentifierFactory"; import { INestiaProject } from "../../structures/INestiaProject"; import { ITypedWebSocketRoute } from "../../structures/ITypedWebSocketRoute"; +import { FilePrinter } from "./FilePrinter"; import { ImportDictionary } from "./ImportDictionary"; import { SdkImportWizard } from "./SdkImportWizard"; import { SdkTypeProgrammer } from "./SdkTypeProgrammer"; @@ -13,10 +15,46 @@ export namespace SdkWebSocketRouteProgrammer { (project: INestiaProject) => (importer: ImportDictionary) => (route: ITypedWebSocketRoute): ts.Statement[] => [ - writeFunction(project)(importer)(route), + FilePrinter.description( + writeFunction(project)(importer)(route), + writeDescription(route), + ), SdkWebSocketNamespaceProgrammer.write(project)(importer)(route), ]; + const writeDescription = (route: ITypedWebSocketRoute): string => { + // MAIN DESCRIPTION + const comments: string[] = route.description + ? route.description.split("\n") + : []; + + // COMMENT TAGS + const tags: IJsDocTagInfo[] = route.jsDocTags.filter( + (tag) => + tag.name !== "param" || + route.parameters + .filter((p) => p.category === "param" || p.category === "query") + .some((p) => p.name === tag.text?.[0]?.text), + ); + if (tags.length !== 0) { + const content: string[] = tags.map((t) => + t.text?.length + ? `@${t.name} ${t.text.map((e) => e.text).join("")}` + : `@${t.name}`, + ); + comments.push("", ...new Set(content)); + } + + // POSTFIX + if (!!comments.length) comments.push(""); + comments.push( + `@controller ${route.controller.name}.${route.name}`, + `@path ${route.path}`, + `@nestia Generated by Nestia - https://github.com/samchon/nestia`, + ); + return comments.join("\n"); + }; + const writeFunction = (project: INestiaProject) => (importer: ImportDictionary) => @@ -178,67 +216,58 @@ const local = ); const joinPath = (caller: ts.Expression) => - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createCallExpression( + ts.factory.createTemplateExpression(ts.factory.createTemplateHead("", ""), [ + ts.factory.createTemplateSpan( + ts.factory.createConditionalExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( ts.factory.createPropertyAccessExpression( - ts.factory.createTemplateExpression( - ts.factory.createTemplateHead("", ""), - [ - ts.factory.createTemplateSpan( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier("connection"), - ts.factory.createIdentifier("host"), - ), - ts.factory.createTemplateMiddle("/", "/"), - ), - ts.factory.createTemplateSpan( - caller, - ts.factory.createTemplateTail("", ""), - ), - ], - ), - ts.factory.createIdentifier("split"), + ts.factory.createIdentifier("connection"), + ts.factory.createIdentifier("host"), ), - undefined, - [ts.factory.createStringLiteral("/")], + ts.factory.createIdentifier("endsWith"), ), - ts.factory.createIdentifier("filter"), + undefined, + [ts.factory.createStringLiteral("/")], ), - undefined, - [ - ts.factory.createArrowFunction( - undefined, - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("str"), - undefined, - undefined, - undefined, - ), - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createPrefixUnaryExpression( - ts.SyntaxKind.ExclamationToken, - ts.factory.createPrefixUnaryExpression( - ts.SyntaxKind.ExclamationToken, - ts.factory.createIdentifier("str"), - ), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("connection"), + ts.factory.createIdentifier("host"), ), + ts.factory.createIdentifier("substring"), ), - ], + undefined, + [ + ts.factory.createNumericLiteral("0"), + ts.factory.createBinaryExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("connection"), + ts.factory.createIdentifier("host"), + ), + ts.factory.createIdentifier("length"), + ), + ts.factory.createToken(ts.SyntaxKind.MinusToken), + ts.factory.createNumericLiteral("1"), + ), + ], + ), + ts.factory.createToken(ts.SyntaxKind.ColonToken), + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("connection"), + ts.factory.createIdentifier("host"), + ), ), - ts.factory.createIdentifier("join"), + ts.factory.createTemplateMiddle("", ""), + ), + ts.factory.createTemplateSpan( + caller, + ts.factory.createTemplateTail("", ""), ), - undefined, - [ts.factory.createStringLiteral("/")], - ); + ]); const getPathParameterType = (project: INestiaProject) => (importer: ImportDictionary) => diff --git a/packages/sdk/src/structures/ITypedWebSocketRoute.ts b/packages/sdk/src/structures/ITypedWebSocketRoute.ts index e83eda1f3..5d9372604 100644 --- a/packages/sdk/src/structures/ITypedWebSocketRoute.ts +++ b/packages/sdk/src/structures/ITypedWebSocketRoute.ts @@ -17,6 +17,7 @@ export interface ITypedWebSocketRoute { location: string; description?: string; + jsDocTags: ts.JSDocTagInfo[]; } export namespace ITypedWebSocketRoute { export type IParameter = diff --git a/test/features/fastify/src/Backend.ts b/test/features/fastify/src/Backend.ts index 3c7c34852..ddf78d960 100644 --- a/test/features/fastify/src/Backend.ts +++ b/test/features/fastify/src/Backend.ts @@ -1,12 +1,10 @@ import core from "@nestia/core"; +import { INestApplication } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; -import { - FastifyAdapter, - NestFastifyApplication, -} from "@nestjs/platform-fastify"; +import { FastifyAdapter } from "@nestjs/platform-fastify"; export class Backend { - private application_?: NestFastifyApplication; + private application_?: INestApplication; public async open(): Promise { this.application_ = await NestFactory.create( @@ -17,6 +15,7 @@ export class Backend { new FastifyAdapter(), { logger: false }, ); + await core.WebSocketAdaptor.upgrade(this.application_); await this.application_.listen(37_000); } diff --git a/test/features/fastify/src/api/functional/calculate/index.ts b/test/features/fastify/src/api/functional/calculate/index.ts new file mode 100644 index 000000000..63a264f5f --- /dev/null +++ b/test/features/fastify/src/api/functional/calculate/index.ts @@ -0,0 +1,48 @@ +/** + * @packageDocumentation + * @module api.functional.calculate + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection } from "@nestia/fetcher"; +import { WebConnector } from "tgrid"; +import type { Driver } from "tgrid"; + +import type { ICalculator } from "../../structures/ICalculator"; +import type { IListener } from "../../structures/IListener"; +import type { IPrecision } from "../../structures/IPrecision"; + +/** + * @controller CalculateController.connect + * @path /calculate + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function connect( + connection: IConnection, + provider: connect.Provider, +): Promise { + const connector: WebConnector< + connect.Header, + connect.Provider, + connect.Listener + > = new WebConnector(connection.headers ?? ({} as any), provider); + await connector.connect( + `${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${connect.path()}`, + ); + const driver: Driver = connector.getDriver(); + return { + connector, + driver, + }; +} +export namespace connect { + export type Output = { + connector: WebConnector; + driver: Driver; + }; + export type Header = IPrecision; + export type Provider = IListener; + export type Listener = ICalculator; + + export const path = () => "/calculate"; +} diff --git a/test/features/fastify/src/api/functional/index.ts b/test/features/fastify/src/api/functional/index.ts index 02af46581..d98ec24b4 100644 --- a/test/features/fastify/src/api/functional/index.ts +++ b/test/features/fastify/src/api/functional/index.ts @@ -5,6 +5,7 @@ */ //================================================================ export * as bbs from "./bbs"; +export * as calculate from "./calculate"; export * as health from "./health"; export * as performance from "./performance"; export * as plain from "./plain"; diff --git a/test/features/fastify/src/api/structures/ICalculator.ts b/test/features/fastify/src/api/structures/ICalculator.ts new file mode 100644 index 000000000..b949326c5 --- /dev/null +++ b/test/features/fastify/src/api/structures/ICalculator.ts @@ -0,0 +1,6 @@ +export interface ICalculator { + plus(x: number, y: number): number; + minus(x: number, y: number): number; + multiply(x: number, y: number): number; + divide(x: number, y: number): number; +} diff --git a/test/features/fastify/src/api/structures/IListener.ts b/test/features/fastify/src/api/structures/IListener.ts new file mode 100644 index 000000000..27bf00791 --- /dev/null +++ b/test/features/fastify/src/api/structures/IListener.ts @@ -0,0 +1,11 @@ +export interface IListener { + on(event: IListener.IEvent): void; +} +export namespace IListener { + export interface IEvent { + operator: "plus" | "minus" | "multiply" | "divide"; + x: number; + y: number; + z: number; + } +} diff --git a/test/features/fastify/src/api/structures/IPrecision.ts b/test/features/fastify/src/api/structures/IPrecision.ts new file mode 100644 index 000000000..5fbe8947d --- /dev/null +++ b/test/features/fastify/src/api/structures/IPrecision.ts @@ -0,0 +1,3 @@ +export interface IPrecision { + value: number; +} diff --git a/test/features/fastify/src/controllers/CalculateController.ts b/test/features/fastify/src/controllers/CalculateController.ts new file mode 100644 index 000000000..a9c8b0319 --- /dev/null +++ b/test/features/fastify/src/controllers/CalculateController.ts @@ -0,0 +1,41 @@ +import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import { Driver, WebAcceptor } from "tgrid"; + +import { ICalculator } from "@api/lib/structures/ICalculator"; +import { IListener } from "@api/lib/structures/IListener"; +import { IPrecision } from "@api/lib/structures/IPrecision"; + +@Controller("calculate") +export class CalculateController { + @core.WebSocketRoute() + public async connect( + @core.WebSocketRoute.Acceptor() + adaptor: WebAcceptor, + @core.WebSocketRoute.Driver() + driver: Driver, + ): Promise { + await adaptor.accept({ + plus: (x, y) => { + const z: number = x + y; + driver.on({ operator: "plus", x, y, z }).catch(() => {}); + return z; + }, + minus: (x, y) => { + const z: number = x - y; + driver.on({ operator: "minus", x, y, z }).catch(() => {}); + return z; + }, + multiply: (x, y) => { + const z: number = x * y; + driver.on({ operator: "multiply", x, y, z }).catch(() => {}); + return z; + }, + divide: (x, y) => { + const z: number = x / y; + driver.on({ operator: "divide", x, y, z }).catch(() => {}); + return z; + }, + }); + } +} diff --git a/test/features/fastify/src/test/features/api/test_api_calculate.ts b/test/features/fastify/src/test/features/api/test_api_calculate.ts new file mode 100644 index 000000000..218336673 --- /dev/null +++ b/test/features/fastify/src/test/features/api/test_api_calculate.ts @@ -0,0 +1,51 @@ +import { TestValidator } from "@nestia/e2e"; +import { sleep_for } from "tstl"; +import typia from "typia"; + +import api from "@api"; +import { IListener } from "@api/lib/structures/IListener"; + +export const test_api_calculate = async ( + connection: api.IConnection, +): Promise => { + const events: IListener.IEvent[] = []; + const listener: IListener = { + on: (e) => events.push(e), + }; + const { connector, driver } = await api.functional.calculate.connect( + connection, + listener, + ); + const expected: IListener.IEvent[] = new Array(100).fill(0).map(() => { + const operator = typia.random(); + const x: number = 10; + const y: number = 5; + return { + operator, + x, + y, + z: + operator === "plus" + ? x + y + : operator === "minus" + ? x - y + : operator === "divide" + ? x / y + : operator === "multiply" + ? x * y + : 0, + }; + }); + try { + for (const e of expected) { + const z: number = await driver[e.operator](e.x, e.y); + TestValidator.equals("result")(z)(e.z); + } + await sleep_for(100); + TestValidator.equals("events")(events)(expected); + } catch (exp) { + throw exp; + } finally { + await connector.close(); + } +}; diff --git a/test/features/websocket/src/api/functional/calculate/index.ts b/test/features/websocket/src/api/functional/calculate/index.ts index d43b8cc72..63a264f5f 100644 --- a/test/features/websocket/src/api/functional/calculate/index.ts +++ b/test/features/websocket/src/api/functional/calculate/index.ts @@ -12,6 +12,11 @@ import type { ICalculator } from "../../structures/ICalculator"; import type { IListener } from "../../structures/IListener"; import type { IPrecision } from "../../structures/IPrecision"; +/** + * @controller CalculateController.connect + * @path /calculate + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ export async function connect( connection: IConnection, provider: connect.Provider, @@ -22,10 +27,7 @@ export async function connect( connect.Listener > = new WebConnector(connection.headers ?? ({} as any), provider); await connector.connect( - `${connection.host}/${connect.path()}` - .split("/") - .filter((str) => !!str) - .join("/"), + `${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${connect.path()}`, ); const driver: Driver = connector.getDriver(); return { diff --git a/test/features/websocket/src/controllers/CalculateController.ts b/test/features/websocket/src/controllers/CalculateController.ts index a9c8b0319..f5c46bea9 100644 --- a/test/features/websocket/src/controllers/CalculateController.ts +++ b/test/features/websocket/src/controllers/CalculateController.ts @@ -1,41 +1,3 @@ -import core from "@nestia/core"; -import { Controller } from "@nestjs/common"; -import { Driver, WebAcceptor } from "tgrid"; +import { CalculateControllerBase } from "./CalculateControllerBase"; -import { ICalculator } from "@api/lib/structures/ICalculator"; -import { IListener } from "@api/lib/structures/IListener"; -import { IPrecision } from "@api/lib/structures/IPrecision"; - -@Controller("calculate") -export class CalculateController { - @core.WebSocketRoute() - public async connect( - @core.WebSocketRoute.Acceptor() - adaptor: WebAcceptor, - @core.WebSocketRoute.Driver() - driver: Driver, - ): Promise { - await adaptor.accept({ - plus: (x, y) => { - const z: number = x + y; - driver.on({ operator: "plus", x, y, z }).catch(() => {}); - return z; - }, - minus: (x, y) => { - const z: number = x - y; - driver.on({ operator: "minus", x, y, z }).catch(() => {}); - return z; - }, - multiply: (x, y) => { - const z: number = x * y; - driver.on({ operator: "multiply", x, y, z }).catch(() => {}); - return z; - }, - divide: (x, y) => { - const z: number = x / y; - driver.on({ operator: "divide", x, y, z }).catch(() => {}); - return z; - }, - }); - } -} +export class CalculateController extends CalculateControllerBase("calculate") {} diff --git a/test/features/websocket/src/controllers/CalculateControllerBase.ts b/test/features/websocket/src/controllers/CalculateControllerBase.ts new file mode 100644 index 000000000..a9191cd68 --- /dev/null +++ b/test/features/websocket/src/controllers/CalculateControllerBase.ts @@ -0,0 +1,44 @@ +import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import { Driver, WebAcceptor } from "tgrid"; + +import { ICalculator } from "@api/lib/structures/ICalculator"; +import { IListener } from "@api/lib/structures/IListener"; +import { IPrecision } from "@api/lib/structures/IPrecision"; + +export function CalculateControllerBase(path: string) { + @Controller(path) + abstract class CalculateControllerBase { + @core.WebSocketRoute() + public async connect( + @core.WebSocketRoute.Acceptor() + adaptor: WebAcceptor, + @core.WebSocketRoute.Driver() + driver: Driver, + ): Promise { + await adaptor.accept({ + plus: (x, y) => { + const z: number = x + y; + driver.on({ operator: "plus", x, y, z }).catch(() => {}); + return z; + }, + minus: (x, y) => { + const z: number = x - y; + driver.on({ operator: "minus", x, y, z }).catch(() => {}); + return z; + }, + multiply: (x, y) => { + const z: number = x * y; + driver.on({ operator: "multiply", x, y, z }).catch(() => {}); + return z; + }, + divide: (x, y) => { + const z: number = x / y; + driver.on({ operator: "divide", x, y, z }).catch(() => {}); + return z; + }, + }); + } + } + return CalculateControllerBase; +} diff --git a/test/features/websocket/swagger.json b/test/features/websocket/swagger.json index 585b1fddf..fe5bf9c29 100644 --- a/test/features/websocket/swagger.json +++ b/test/features/websocket/swagger.json @@ -1 +1 @@ -{"openapi":"3.1.0","servers":[{"url":"https://github.com/samchon/nestia","description":"insert your server url"}],"info":{"version":"3.1.0-dev.20240429","title":"@nestia/test","description":"Test program of Nestia","license":{"name":"MIT"}},"paths":{"/health":{"get":{"tags":[],"parameters":[],"responses":{"200":{"description":""}}}}},"components":{"schemas":{},"securitySchemes":{"bearer":{"type":"apiKey","in":"header","name":"Authorization"}}},"tags":[],"x-samchon-emended":true} \ No newline at end of file +{"openapi":"3.1.0","servers":[{"url":"https://github.com/samchon/nestia","description":"insert your server url"}],"info":{"version":"3.1.1-dev.20240430","title":"@nestia/test","description":"Test program of Nestia","license":{"name":"MIT"}},"paths":{"/health":{"get":{"tags":[],"parameters":[],"responses":{"200":{"description":""}}}}},"components":{"schemas":{},"securitySchemes":{"bearer":{"type":"apiKey","in":"header","name":"Authorization"}}},"tags":[],"x-samchon-emended":true} \ No newline at end of file diff --git a/test/package.json b/test/package.json index 20511802d..1ed379f14 100644 --- a/test/package.json +++ b/test/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@nestia/test", - "version": "3.1.0-dev.20240429", + "version": "3.1.1", "description": "Test program of Nestia", "main": "index.js", "scripts": { @@ -26,7 +26,7 @@ }, "homepage": "https://nestia.io", "devDependencies": { - "@nestia/sdk": "^3.1.0-dev.20240429", + "@nestia/sdk": "^3.1.1", "@nestjs/swagger": "^7.1.2", "@samchon/openapi": "^0.1.21", "@types/express": "^4.17.17", @@ -40,9 +40,9 @@ }, "dependencies": { "@fastify/multipart": "^8.1.0", - "@nestia/core": "^3.1.0-dev.20240429", + "@nestia/core": "^3.1.1", "@nestia/e2e": "^0.3.6", - "@nestia/fetcher": "^3.1.0-dev.20240429", + "@nestia/fetcher": "^3.1.1", "@nestjs/common": "^10.3.5", "@nestjs/core": "^10.3.5", "@nestjs/platform-express": "^10.3.5", diff --git a/website/package.json b/website/package.json index 8cb32a11a..b999fda50 100644 --- a/website/package.json +++ b/website/package.json @@ -23,7 +23,7 @@ "@mui/icons-material": "5.15.6", "@mui/material": "5.15.6", "@mui/system": "5.15.6", - "@nestia/migrate": "^0.13.9", + "@nestia/migrate": "^0.13.10", "@samchon/openapi": "^0.1.21", "@stackblitz/sdk": "^1.9.0", "js-yaml": "^4.1.0", diff --git a/website/pages/docs/core/WebSocketRoute.mdx b/website/pages/docs/core/WebSocketRoute.mdx new file mode 100644 index 000000000..80063ad9d --- /dev/null +++ b/website/pages/docs/core/WebSocketRoute.mdx @@ -0,0 +1,801 @@ +import { Callout, Tabs, Tab } from 'nextra-theme-docs' + +## Outline +@nestia/core, + "Server Application", + 'Client Application', + ]}> + +```typescript +export function WebSocketRoute(path?: string): MethodDecorator; +export namespace WebSocketRoute { + export function Acceptor(): ParameterDecorator; + export function Driver(): ParameterDecorator; + export function Header(): ParameterDecorator; + export function Param(field: string): ParameterDecorator; + export function Query(): ParameterDecorator; +} +``` + + +```typescript filename="src/CalculateController.ts" showLineNumbers +import { WebSocketRoute } from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import { Driver, WebAcceptor } from "tgrid"; + +import { ICalculator } from "./api/structures/ICalculator"; +import { IListener } from "./api/structures/IListener"; +import { Calculator } from "./providers/Calculator"; + +@Controller("calculate") +export class CalculateController { + /** + * Start simple calculator. + * + * Start simple calculator through WebSocket. + */ + @WebSocketRoute("start") + public async start( + @WebSocketRoute.Acceptor() + acceptor: WebAcceptor, + @WebSocketRoute.Driver() driver: Driver, + ): Promise { + await acceptor.accept(new Calculator(driver)); + } +} +``` + + +```typescript filename="test/features/test_api_calculate_start.ts" showLineNumbers +import { TestValidator } from "@nestia/e2e"; +import api from "@samchon/calculator-api/lib/index"; +import { IListener } from "@samchon/calculator-api/lib/structures/IListener"; + +export const test_api_calculate_start = async ( + connection: api.IConnection, +): Promise => { + const stack: IListener.IEvent[] = []; + const listener: IListener = { + on: (event) => stack.push(event), + }; + const { connector, driver } = await api.functional.calculate.start( + connection, + listener, + ); + try { + TestValidator.equals("plus")(await driver.plus(4, 2))(6); + TestValidator.equals("minus")(await driver.minus(4, 2))(2); + TestValidator.equals("multiply")(await driver.multiply(4, 2))(8); + TestValidator.equals("divide")(await driver.divide(4, 2))(2); + TestValidator.equals("events")(stack)([ + { type: "plus", x: 4, y: 2, z: 6 }, + { type: "minus", x: 4, y: 2, z: 2 }, + { type: "multiply", x: 4, y: 2, z: 8 }, + { type: "divide", x: 4, y: 2, z: 2 }, + ]); + } catch (exp) { + throw exp; + } finally { + await connector.close(); + } +}; +``` + + + +WebSocket route decorators. + +`@WebSocketRoute()` is a collection of decorators for WebSocket routes. + +Also, supports [SDK (Software Development Kit)](../generators/sdk), so that you can easily develop the WebSocket client. + + + + +## How to use +### Application Setup +```typescript filename="src/CalculateModule.ts" copy showLineNumbers {1,10} +import { WebSocketAdaptor } from "@nestia/core"; +import { INestApplication } from "@nestjs/common"; +import { NestFactory } from "@nestjs/core"; + +import { CalculateModule } from "./CalculateModule"; + +export namespace CalculateBackend { + export const start = async (): Promise => { + const app: INestApplication = await NestFactory.create(CalculateModule); + await WebSocketAdaptor.upgrade(app); + await app.listen(3_000, "0.0.0.0"); + return app; + }; +} +``` + +At first, you need to upgrade your NestJS application to support WebSocket protocol. + +Import `WebSocketAdaptor` class from `@nestia/core`, and call `WebSocketAdaptor.upgrade()` function with the NestJS application instance like above. + +If you don't upgrade it, `@WebSocketRoute()` decorated methods never work. + +### `@WebSocketRoute()` +CalculateController.ts, + Calculator.ts, + ICalculator.ts, + IListener.ts, + "Client Application", + ]}> + +```typescript filename="src/CalculateController.ts" copy showLineNumbers +import { WebSocketRoute } from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import { Driver, WebAcceptor } from "tgrid"; + +import { ICalculator } from "./api/structures/ICalculator"; +import { IListener } from "./api/structures/IListener"; +import { Calculator } from "./providers/Calculator"; + +@Controller("calculate") +export class CalculateController { + /** + * Start simple calculator. + * + * Start simple calculator through WebSocket. + */ + @WebSocketRoute("start") + public async start( + @WebSocketRoute.Acceptor() + acceptor: WebAcceptor, + @WebSocketRoute.Driver() driver: Driver, + ): Promise { + await acceptor.accept(new Calculator(driver)); + } +} +``` + + +```typescript filename="src/providers/Calculator.ts" copy showLineNumbers +import { Driver } from "tgrid"; + +import { ICalculator } from "../api/structures/ICalculator"; +import { IListener } from "../api/structures/IListener"; + +export class Calculator implements ICalculator { + public constructor(private readonly listener: Driver) {} + + public plus(x: number, y: number): number { + const z: number = x + y; + this.listener.on({ type: "plus", x, y, z }).catch(() => {}); + return z; + } + + public minus(x: number, y: number): number { + const z: number = x - y; + this.listener.on({ type: "minus", x, y, z }).catch(() => {}); + return z; + } + + public multiply(x: number, y: number): number { + const z: number = x * y; + this.listener.on({ type: "multiply", x, y, z }).catch(() => {}); + return z; + } + + public divide(x: number, y: number): number { + const z: number = x / y; + this.listener.on({ type: "divide", x, y, z }).catch(() => {}); + return z; + } +} +``` + + +```typescript filename="src/api/structures/ICalculator.ts" copy showLineNumbers +export interface ICalculator { + plus(a: number, b: number): number; + minus(a: number, b: number): number; + multiply(a: number, b: number): number; + divide(a: number, b: number): number; +} +``` + + +```typescript filename="src/api/structures/IListener.ts" copy showLineNumbers +export interface IListener { + on(event: IListener.IEvent): void; +} +export namespace IListener { + export interface IEvent { + type: string; + x: number; + y: number; + z: number; + } +} +``` + + +```typescript filename="test/features/test_api_calculate_start.ts" showLineNumbers +import { TestValidator } from "@nestia/e2e"; +import api from "@samchon/calculator-api/lib/index"; +import { IListener } from "@samchon/calculator-api/lib/structures/IListener"; + +export const test_api_calculate_start = async ( + connection: api.IConnection, +): Promise => { + const stack: IListener.IEvent[] = []; + const listener: IListener = { + on: (event) => stack.push(event), + }; + const { connector, driver } = await api.functional.calculate.start( + connection, + listener, + ); + try { + TestValidator.equals("plus")(await driver.plus(4, 2))(6); + TestValidator.equals("minus")(await driver.minus(4, 2))(2); + TestValidator.equals("multiply")(await driver.multiply(4, 2))(8); + TestValidator.equals("divide")(await driver.divide(4, 2))(2); + TestValidator.equals("events")(stack)([ + { type: "plus", x: 4, y: 2, z: 6 }, + { type: "minus", x: 4, y: 2, z: 2 }, + { type: "multiply", x: 4, y: 2, z: 8 }, + { type: "divide", x: 4, y: 2, z: 2 }, + ]); + } catch (exp) { + throw exp; + } finally { + await connector.close(); + } +}; +``` + + + +After that, attach `@WebSocketRoute()` decorator function onto target method like above. + +Note that, never forget to defining the `@WebSocketRoute.Acceptor()` decorated parameter. It is essential for both WebSocket route method and SDK library generation. Each generic arguments of `WebAcceptor` means like below: + + - `Header`: Header information received by client + - `Provider`: Service provider for client + - `Listener`: Remote service provider from client + +Also, the `Driver` is a type of the remote provider by client. If you call any function of the remote provider, your function call request will be sent to the remote client, and returned value would be recived from the client asynchronouly. + +Therefore, the `Driver` type converts every functions' return type to be `Promise`. In the client side, your `Provider` would be also wrapped into the `Driver`, so that client can call your functions asynchronously, too. + +### Nested Decorators +CalcController.ts, + AdvCalculator.ts, + IAvCalculator.ts, + IListener.ts, + "Client Application", + ]}> + +```typescript filename="src/CalculateController.ts" copy showLineNumbers +import { WebSocketRoute } from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import { Driver, WebAcceptor } from "tgrid"; +import { tags } from "typia"; + +import { IAdvancedCalculator } from "./api/structures/IAdvancedCalculator"; +import { IHeader } from "./api/structures/IHeader"; +import { IListener } from "./api/structures/IListener"; +import { IMemo } from "./api/structures/IMemo"; +import { AdvancedCalculator } from "./providers/AdvancedCalculator"; + +@Controller("calculate") +export class CalculateController { + /** + * Start advanced calculator. + * + * Start advanced calculator through WebSocket with additional informations. + * + * @param id ID to assign + * @param header Header information + * @param memo Memo to archive + */ + @WebSocketRoute(":id/advance") + public async advance( + @WebSocketRoute.Param("id") id: string & tags.Format<"uuid">, + @WebSocketRoute.Header() header: undefined | Partial, + @WebSocketRoute.Query() memo: IMemo, + @WebSocketRoute.Acceptor() + acceptor: WebAcceptor, + ): Promise { + if (header?.precision !== undefined && header.precision < 0) + await acceptor.reject(1008, "Invalid precision value"); + else + await acceptor.accept( + new AdvancedCalculator( + id, + { precision: header?.precision ?? 2 }, + memo, + acceptor.getDriver(), + ), + ); + } +} +``` + + +```typescript filename="src/providers/AdvancedCalculator.ts" copy showLineNumbers +import { Driver } from "tgrid"; + +import { IAdvancedCalculator } from "../api/structures/IAdvancedCalculator"; +import { IHeader } from "../api/structures/IHeader"; +import { IListener } from "../api/structures/IListener"; +import { IMemo } from "../api/structures/IMemo"; + +export class AdvancedCalculator implements IAdvancedCalculator { + private round: (value: number) => number; + + public constructor( + private readonly id: string, + private readonly header: IHeader, + private readonly memo: IMemo, + private readonly listener: Driver, + ) { + this.round = roundPrecision(header.precision); + } + + public getId(): string { + return this.id; + } + public getPrecision(): number { + return this.header.precision; + } + public getMemo(): IMemo { + return this.memo; + } + + public plus(x: number, y: number): number { + const z: number = this.round(x + y); + this.listener.on({ type: "plus", x, y, z }).catch(() => {}); + return z; + } + public minus(x: number, y: number): number { + const z: number = this.round(x - y); + this.listener.on({ type: "minus", x, y, z }).catch(() => {}); + return z; + } + public multiply(x: number, y: number): number { + const z: number = this.round(x * y); + this.listener.on({ type: "multiply", x, y, z }).catch(() => {}); + return z; + } + public divide(x: number, y: number): number { + const z: number = this.round(x / y); + this.listener.on({ type: "divide", x, y, z }).catch(() => {}); + return z; + } +} + +const roundPrecision = + (precision: number) => + (value: number): number => { + const factor: number = Math.pow(10, precision); + return Math.round(value * factor) / factor; + }; +``` + + +```typescript filename="src/api/structures/IAdvancedCalculator.ts" copy showLineNumbers +import { IMemo } from "./IMemo"; + +export interface IAdvancedCalculator { + plus(a: number, b: number): number; + minus(a: number, b: number): number; + multiply(a: number, b: number): number; + divide(a: number, b: number): number; + + getId(): string; + getPrecision(): number; + getMemo(): IMemo; +} +``` + + +```typescript filename="src/api/structures/IListener.ts" copy showLineNumbers +export interface IListener { + on(event: IListener.IEvent): void; +} +export namespace IListener { + export interface IEvent { + type: string; + x: number; + y: number; + z: number; + } +} +``` + + +```typescript filename="test/features/test_api_calculate_advavnce.ts" showLineNumbers +import { TestValidator } from "@nestia/e2e"; +import api from "@samchon/calculator-api/lib/index"; +import { IListener } from "@samchon/calculator-api/lib/structures/IListener"; +import { IMemo } from "@samchon/calculator-api/lib/structures/IMemo"; +import { v4 } from "uuid"; + +export const test_api_calculate_advance = async ( + connection: api.IConnection, +): Promise => { + const stack: IListener.IEvent[] = []; + const listener: IListener = { + on: (event) => stack.push(event), + }; + + const id: string = v4(); + const memo: IMemo = { + title: "test", + description: null, + time: Date.now(), + }; + const { connector, driver } = await api.functional.calculate.advance( + { + ...connection, + headers: { precision: 2 }, + }, + id, + memo, + listener, + ); + try { + TestValidator.equals("id")(await driver.getId())(id); + TestValidator.equals("memo")(await driver.getMemo())(memo); + TestValidator.equals("precision")(await driver.getPrecision())(2); + TestValidator.equals("plus")(await driver.plus(1, 2))(3); + TestValidator.equals("minus")(await driver.minus(1, 2))(-1); + TestValidator.equals("multiply")(await driver.multiply(0.01, 0.02))(0); + TestValidator.equals("divide")(await driver.divide(1, 3))(0.33); + TestValidator.equals("events")(stack)([ + { type: "plus", x: 1, y: 2, z: 3 }, + { type: "minus", x: 1, y: 2, z: -1 }, + { type: "multiply", x: 0.01, y: 0.02, z: 0 }, + { type: "divide", x: 1, y: 3, z: 0.33 }, + ]); + } catch (exp) { + throw exp; + } finally { + await connector.close(); + } +}; +``` + + + +If you need additional parameters, you can use nested decorators. + + - `@WebSocketRoute.Acceptor()`: Acceptor for the client connection + - `@WebSocketRoute.Driver()`: Driver for the remote provider by client + - `@WebSocketRoute.Header()`: Header information from the client + - `@WebSocketRoute.Param()`: URL path parameter + - `@WebSocketRoute.Query()`: URL query parameter + +For reference, those decorators are almost same with [`@TypedHeaders()`](./TypedHeaders), [`@TypedParam()`](./TypedParam) and [`@TypedQuery()`](./TypedQuery). However, they can't be used in `@WebSocketRoute()` decorated method. Only nested decorator functions under the `WebSocketRoute` module are allowed. + + + + +## Software Development Kit +Related Document: [Software Development Kit](../generators/sdk) + +When you configure a `nestia.config.ts` file and run `npx nestia sdk` command, `@nestia/sdk` will generate a SDK (Software Development Kit) library for the WebSocket route. With the SDK library, you can easily develop the WebSocket client application with TypeScript types. + +Also, as I've mentioned above, remote provider by WebSocket server is wrapped into the `Driver` type, so that the client application can call the remote provider's function asynchronously. For example, `ICalculator.plus()` function returned `number` value in the server side, but `Driver` returns `Promise` type. + +In the same reason, the `IListener` type would be wrapped into the `Driver` in the server side, and the `listener` provider would be called asynchronously in the server side through the WebSocket network communication. + +IAdvCalculator.ts, + IListener.ts, + ]}> + +```typescript filename="test/features/test_api_calculate_start.ts" showLineNumbers +import { TestValidator } from "@nestia/e2e"; +import api from "@samchon/calculator-api/lib/index"; +import { IListener } from "@samchon/calculator-api/lib/structures/IListener"; + +export const test_api_calculate_start = async ( + connection: api.IConnection, +): Promise => { + const stack: IListener.IEvent[] = []; + const listener: IListener = { + on: (event) => stack.push(event), + }; + const { connector, driver } = await api.functional.calculate.start( + connection, + listener, + ); + try { + TestValidator.equals("plus")(await driver.plus(4, 2))(6); + TestValidator.equals("minus")(await driver.minus(4, 2))(2); + TestValidator.equals("multiply")(await driver.multiply(4, 2))(8); + TestValidator.equals("divide")(await driver.divide(4, 2))(2); + TestValidator.equals("events")(stack)([ + { type: "plus", x: 4, y: 2, z: 6 }, + { type: "minus", x: 4, y: 2, z: 2 }, + { type: "multiply", x: 4, y: 2, z: 8 }, + { type: "divide", x: 4, y: 2, z: 2 }, + ]); + } catch (exp) { + throw exp; + } finally { + await connector.close(); + } +}; +``` + + +```typescript filename="src/api/functional/calculate/index.ts" showLineNumbers +/** + * @packageDocumentation + * @module api.functional.calculate + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection } from "@nestia/fetcher"; +import { WebConnector } from "tgrid"; +import type { Driver } from "tgrid"; +import type { Format } from "typia/lib/tags/Format"; + +import type { IAdvancedCalculator } from "../../structures/IAdvancedCalculator"; +import type { IHeader } from "../../structures/IHeader"; +import type { IListener } from "../../structures/IListener"; +import type { IMemo } from "../../structures/IMemo"; + +/** + * Start advanced calculator. + * + * Start advanced calculator through WebSocket with additional informations. + * + * @param id ID to assign + * @param memo Memo to archive + * + * @controller CalculateController.advance + * @path /calculate/:id/advance + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function advance( + connection: IConnection, + id: string & Format<"uuid">, + memo: advance.Query, + provider: advance.Provider, +): Promise { + const connector: WebConnector< + advance.Header, + advance.Provider, + advance.Listener + > = new WebConnector(connection.headers ?? ({} as any), provider); + await connector.connect( + `${connection.host}/${advance.path(id, memo)}` + .split("/") + .filter((str) => !!str) + .join("/"), + ); + const driver: Driver = connector.getDriver(); + return { + connector, + driver, + }; +} +export namespace advance { + export type Output = { + connector: WebConnector; + driver: Driver; + }; + export type Header = undefined | Partial; + export type Provider = IListener; + export type Listener = IAdvancedCalculator; + export type Query = IMemo; + + export const path = (id: string & Format<"uuid">, memo: advance.Query) => { + const variables: URLSearchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(memo as any)) + if (undefined === value) continue; + else if (Array.isArray(value)) + value.forEach((elem: any) => variables.append(key, String(elem))); + else variables.set(key, String(value)); + const location: string = `/calculate/${encodeURIComponent(id ?? "null")}/advance`; + return 0 === variables.size + ? location + : `${location}?${variables.toString()}`; + }; +} +``` + + +```typescript filename="src/CalculateController.ts" copy showLineNumbers +import { WebSocketRoute } from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import { Driver, WebAcceptor } from "tgrid"; +import { tags } from "typia"; + +import { IAdvancedCalculator } from "./api/structures/IAdvancedCalculator"; +import { IHeader } from "./api/structures/IHeader"; +import { IListener } from "./api/structures/IListener"; +import { IMemo } from "./api/structures/IMemo"; +import { AdvancedCalculator } from "./providers/AdvancedCalculator"; + +@Controller("calculate") +export class CalculateController { + /** + * Start advanced calculator. + * + * Start advanced calculator through WebSocket with additional informations. + * + * @param id ID to assign + * @param header Header information + * @param memo Memo to archive + */ + @WebSocketRoute(":id/advance") + public async advance( + @WebSocketRoute.Param("id") id: string & tags.Format<"uuid">, + @WebSocketRoute.Header() header: undefined | Partial, + @WebSocketRoute.Query() memo: IMemo, + @WebSocketRoute.Acceptor() + acceptor: WebAcceptor, + ): Promise { + if (header?.precision !== undefined && header.precision < 0) + await acceptor.reject(1008, "Invalid precision value"); + else + await acceptor.accept( + new AdvancedCalculator( + id, + { precision: header?.precision ?? 2 }, + memo, + acceptor.getDriver(), + ), + ); + } +} +``` + + +```typescript filename="src/api/structures/IAdvancedCalculator.ts" copy showLineNumbers +import { IMemo } from "./IMemo"; + +export interface IAdvancedCalculator { + plus(a: number, b: number): number; + minus(a: number, b: number): number; + multiply(a: number, b: number): number; + divide(a: number, b: number): number; + + getId(): string; + getPrecision(): number; + getMemo(): IMemo; +} +``` + + +```typescript filename="src/api/structures/IListener.ts" copy showLineNumbers +export interface IListener { + on(event: IListener.IEvent): void; +} +export namespace IListener { + export interface IEvent { + type: string; + x: number; + y: number; + z: number; + } +} +``` + + + + + + +## Restrictions +### `@WebSocketAcceptor()` +When defining `@WebSocketRoute()` decorated method, you must define the `@WebSocketRoute.Acceptor()` decorated parameter. It is essential for both WebSocket route method and SDK library generation, because its target type `WebAcceptor` has significant type definitions for WebSocket communication. + + - `Header`: Header information received by client + - `Provider`: Service provider for client + - `Listener`: Remote service provider from client + +```typescript filename="src/CalculateController.ts" copy showLineNumbers +import { WebSocketRoute } from "@nestia/core"; +import { Controller } from "@nestjs/common"; +import { Driver, WebAcceptor } from "tgrid"; + +import { ICalculator } from "./api/structures/ICalculator"; +import { IListener } from "./api/structures/IListener"; +import { Calculator } from "./providers/Calculator"; + +@Controller("calculate") +export class CalculateController { + /** + * Start simple calculator. + * + * Start simple calculator through WebSocket. + */ + @WebSocketRoute("start") + public async start( + @WebSocketRoute.Acceptor() + acceptor: WebAcceptor, + @WebSocketRoute.Driver() driver: Driver, + ): Promise { + await acceptor.accept(new Calculator(driver)); + } +} +``` + +### `@WebSocketRoute.Param()` +`@WebSocketRoute.Param()` allows only atomic type. + + - `boolean` + - `number` + - `string` + +Also, `@WebSocketRoute.Param()` allows nullable like `number | null`, but undefindable type is not. + + - `number | null` is allowed + - `string | undefined` is prohibited + +If you violate above condition, and try to declare object or union type, compilation error would be occured: + +```bash +Error on nestia.core.WebSocketRoute.Param(): only atomic type is allowed +``` + +### `@WebSocketRoute.Query()` +When using `@WebSocketRoute.Query()`, you've to follow such restrction. + +At first, type of `@WebSocketRoute.Query()` must be a pure **object type**. It does not allow union type. Also, nullable and undefindable types are not allowed, either. Note that, query parameter type must be a sole **object type** without any extra definition. + +At next, type of properties must be **atomic**, or array of atomic type. In the atomic type case, the atomic type allows both nullable and undefindable types. However, mixed union atomic type like `string | number` or `"1" | "2" | 3` are not allowed. Also, the array type does not allow both nullable and undefindable types, either. + + - `boolean` + - `number` + - `bigint` + - `string` + +```typescript filename="SomeQueryDto.ts" showLineNumbers +export interface SomeQueryDto { + //---- + // ATOMIC TYPES + //---- + // ALLOWED + boolean: boolean; + number: number; + string: string; + bigint: bigint; + optional_number?: number; + nullable_string: string | null; + literal_union: "A" | "B" | "C" | "D"; + + // NOT ALLOWED + mixed_union: string | number | boolean; + mixed_literal: "A" | "B" | 3; + + //---- + // ARRAY TYPES + //---- + // ALLOWED + nullable_element_array: (string | null)[]; + string_array: string[]; + number_array: number[]; + literal_union_array: ("A" | "B" | "C")[]; + literal_tuple: ["A", "B", "C"]; + + // NOT ALLOWED + optional_element_array: (string | undefined)[]; + optional_array: string[] | undefined; + nullable_array: string[] | null; + union_atomic_array: (string | number)[]; + mixed_literal_array: ("A", "B", 3)[]; + mixed_tuple: ["A", "B", 3]; +} +``` \ No newline at end of file diff --git a/website/pages/docs/core/_meta.json b/website/pages/docs/core/_meta.json index 3b34bdb51..f704df690 100644 --- a/website/pages/docs/core/_meta.json +++ b/website/pages/docs/core/_meta.json @@ -5,5 +5,6 @@ "TypedQuery": "TypedQuery", "TypedFormData": "TypedFormData", "TypedHeaders": "TypedHeaders", - "TypedException": "TypedException" + "TypedException": "TypedException", + "WebSocketRoute": "WebSocketRoute" } \ No newline at end of file