diff --git a/README.md b/README.md index 94861142..4e8e8964 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ Use class-based controllers to handle websocket events. Helps to organize your c ```typescript import 'reflect-metadata'; ``` + +3. Install a DI container, for example `typedi`; + + ``` + npm install typedi + ``` ## Example of usage @@ -39,8 +45,10 @@ Use class-based controllers to handle websocket events. Helps to organize your c MessageBody, OnMessage, } from 'socket-controllers'; + import {Service} from 'typedi'; // Only if you are using typedi @SocketController() + @Service() // Only if you are using typedi export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { @@ -67,10 +75,13 @@ Use class-based controllers to handle websocket events. Helps to organize your c ```typescript import 'es6-shim'; // this shim is optional if you are using old version of node import 'reflect-metadata'; // this shim is required - import { createSocketServer } from 'socket-controllers'; - import { MessageController } from './MessageController'; + import { SocketControllers } from 'socket-controllers'; + import { MessageController } from './MessageController'; + import {Container} from 'typedi'; // Only if you are using typedi - createSocketServer(3001, { + new SocketControllers({ + port: 3001, + container: Container, controllers: [MessageController], }); ``` @@ -135,7 +146,7 @@ export class MessageController { If you specify a class type to parameter that is decorated with `@MessageBody()`, socket-controllers will use [class-transformer][1] to create instance of the given class type with the data received in the message. -To disable this behaviour you need to specify a `{ useConstructorUtils: false }` in SocketControllerOptions when creating a server. +To disable this behaviour you need to specify a `{ transformOption: { transform: false ] }` in SocketControllerOptions when creating a server. #### `@SocketQueryParam()` decorator @@ -266,16 +277,18 @@ If promise returned by controller action, message will be emitted only after pro #### Using exist server instead of creating a new one If you need to create and configure socket.io server manually, -you can use `useSocketServer` instead of `createSocketServer` function. +you can pass it to the `SocketControllers` constructor. Here is example of creating socket.io server and configuring it with express: ```typescript import 'reflect-metadata'; // this shim is required -import { useSocketServer } from 'socket-controllers'; +import { SocketControllers } from 'socket-controllers'; +import { Server } from 'socket.io'; +import { Container } from 'typedi'; // Only if you are using typedi const app = require('express')(); const server = require('http').Server(app); -const io = require('socket.io')(server); +const io = new Server(server); server.listen(3001); @@ -287,7 +300,7 @@ io.use((socket: any, next: Function) => { console.log('Custom middleware'); next(); }); -useSocketServer(io); +new SocketControllers({io, container: Container}); ``` #### Load all controllers from the given directory @@ -297,9 +310,12 @@ You can load all controllers in once from specific directories, by specifying ar ```typescript import 'reflect-metadata'; // this shim is required -import { createSocketServer } from 'socket-controllers'; +import { SocketControllers } from 'socket-controllers'; +import { Container } from 'typedi'; // Only if you are using typedi -createSocketServer(3000, { +new SocketControllers({ + port: 3000, + container: Container, controllers: [__dirname + '/controllers/*.js'], }); // registers all given controllers ``` @@ -362,10 +378,13 @@ Controllers and middlewares should be loaded: ```typescript import 'reflect-metadata'; -import { createSocketServer } from 'socket-controllers'; +import { SocketControllers } from 'socket-controllers'; import { MessageController } from './MessageController'; import { MyMiddleware } from './MyMiddleware'; // here we import it -let io = createSocketServer(3000, { +import { Container } from 'typedi'; // Only if you are using typedi +const server = new SocketControllers({ + port: 3000, + container: Container, controllers: [MessageController], middlewares: [MyMiddleware], }); @@ -375,10 +394,13 @@ Also you can load them from directories. Also you can use glob patterns: ```typescript import 'reflect-metadata'; -import { createSocketServer } from 'socket-controllers'; -let io = createSocketServer(3000, { - controllers: [__dirname + '/controllers/**/*.js'], - middlewares: [__dirname + '/middlewares/**/*.js'], +import { SocketControllers } from 'socket-controllers'; +import { Container } from 'typedi'; // Only if you are using typedi +const server = new SocketControllers({ + port: 3000, + container: Container, + controllers: [__dirname + '/controllers/**/*.js'], + middlewares: [__dirname + '/middlewares/**/*.js'], }); ``` @@ -390,15 +412,13 @@ Here is example how to integrate socket-controllers with [typedi](https://github ```typescript import 'reflect-metadata'; -import { createSocketServer, useContainer } from 'socket-controllers'; +import { SocketControllers } from 'socket-controllers'; import { Container } from 'typedi'; -// its important to set container before any operation you do with socket-controllers, -// including importing controllers -useContainer(Container); - // create and run socket server -let io = createSocketServer(3000, { +const server = new SocketControllers({ + port: 3000, + container: Container, controllers: [__dirname + '/controllers/*.js'], middlewares: [__dirname + '/middlewares/*.js'], }); diff --git a/sample/sample1-simple-controller/MessageController.ts b/sample/sample1-simple-controller/MessageController.ts index ff3d771f..5374a7ac 100644 --- a/sample/sample1-simple-controller/MessageController.ts +++ b/sample/sample1-simple-controller/MessageController.ts @@ -1,12 +1,5 @@ -import { - OnConnect, - SocketController, - ConnectedSocket, - OnDisconnect, - MessageBody, - OnMessage, -} from '../../src/decorators'; import { Message } from './Message'; +import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController() export class MessageController { diff --git a/sample/sample1-simple-controller/app.ts b/sample/sample1-simple-controller/app.ts index 102196c2..a8e061f0 100644 --- a/sample/sample1-simple-controller/app.ts +++ b/sample/sample1-simple-controller/app.ts @@ -1,8 +1,11 @@ import 'reflect-metadata'; -import { createSocketServer } from '../../src/index'; +import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; +import { Container } from 'typedi'; -createSocketServer(3001, { +new SocketControllers({ + port: 3001, + container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there diff --git a/sample/sample2-use-created-socket-io/MessageController.ts b/sample/sample2-use-created-socket-io/MessageController.ts index ff3d771f..5374a7ac 100644 --- a/sample/sample2-use-created-socket-io/MessageController.ts +++ b/sample/sample2-use-created-socket-io/MessageController.ts @@ -1,12 +1,5 @@ -import { - OnConnect, - SocketController, - ConnectedSocket, - OnDisconnect, - MessageBody, - OnMessage, -} from '../../src/decorators'; import { Message } from './Message'; +import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController() export class MessageController { diff --git a/sample/sample2-use-created-socket-io/app.ts b/sample/sample2-use-created-socket-io/app.ts index 26cbd71f..5ad559a4 100644 --- a/sample/sample2-use-created-socket-io/app.ts +++ b/sample/sample2-use-created-socket-io/app.ts @@ -1,10 +1,12 @@ import 'reflect-metadata'; -import { useSocketServer } from '../../src/index'; +import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; +import { Server } from 'socket.io'; +import { Container } from 'typedi'; const app = require('express')(); const server = require('http').Server(app); -const io = require('socket.io')(server); +const io = new Server(server); server.listen(3001); @@ -16,7 +18,9 @@ io.use((socket: any, next: Function) => { console.log('Custom middleware'); next(); }); -useSocketServer(io, { +new SocketControllers({ + io, + container: Container, controllers: [MessageController], }); diff --git a/sample/sample3-namespaces/MessageController.ts b/sample/sample3-namespaces/MessageController.ts index aa3ab530..ae5dcae2 100644 --- a/sample/sample3-namespaces/MessageController.ts +++ b/sample/sample3-namespaces/MessageController.ts @@ -1,12 +1,5 @@ -import { - OnConnect, - SocketController, - ConnectedSocket, - OnDisconnect, - MessageBody, - OnMessage, -} from '../../src/decorators'; import { Message } from './Message'; +import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController('/messages') export class MessageController { diff --git a/sample/sample3-namespaces/app.ts b/sample/sample3-namespaces/app.ts index 102196c2..a8e061f0 100644 --- a/sample/sample3-namespaces/app.ts +++ b/sample/sample3-namespaces/app.ts @@ -1,8 +1,11 @@ import 'reflect-metadata'; -import { createSocketServer } from '../../src/index'; +import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; +import { Container } from 'typedi'; -createSocketServer(3001, { +new SocketControllers({ + port: 3001, + container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there diff --git a/sample/sample4-emitters/MessageController.ts b/sample/sample4-emitters/MessageController.ts index e1fa21dc..a74ed7e2 100644 --- a/sample/sample4-emitters/MessageController.ts +++ b/sample/sample4-emitters/MessageController.ts @@ -1,15 +1,15 @@ +import { Message } from './Message'; import { - OnConnect, - SocketController, ConnectedSocket, - OnDisconnect, + EmitOnFail, + EmitOnSuccess, MessageBody, + OnConnect, + OnDisconnect, OnMessage, - EmitOnSuccess, - EmitOnFail, SkipEmitOnEmptyResult, -} from '../../src/decorators'; -import { Message } from './Message'; + SocketController, +} from '../../src'; @SocketController() export class MessageController { diff --git a/sample/sample4-emitters/app.ts b/sample/sample4-emitters/app.ts index 102196c2..a8e061f0 100644 --- a/sample/sample4-emitters/app.ts +++ b/sample/sample4-emitters/app.ts @@ -1,8 +1,11 @@ import 'reflect-metadata'; -import { createSocketServer } from '../../src/index'; +import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; +import { Container } from 'typedi'; -createSocketServer(3001, { +new SocketControllers({ + port: 3001, + container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there diff --git a/sample/sample5-middlewares/AuthenticationMiddleware.ts b/sample/sample5-middlewares/AuthenticationMiddleware.ts index c2a60d3b..fbd5bb94 100644 --- a/sample/sample5-middlewares/AuthenticationMiddleware.ts +++ b/sample/sample5-middlewares/AuthenticationMiddleware.ts @@ -1,5 +1,4 @@ -import { Middleware } from '../../src/decorators'; -import { MiddlewareInterface } from '../../src/MiddlewareInterface'; +import { Middleware, MiddlewareInterface } from '../../src'; @Middleware() export class AuthenticationMiddleware implements MiddlewareInterface { diff --git a/sample/sample5-middlewares/MessageController.ts b/sample/sample5-middlewares/MessageController.ts index ff3d771f..5374a7ac 100644 --- a/sample/sample5-middlewares/MessageController.ts +++ b/sample/sample5-middlewares/MessageController.ts @@ -1,12 +1,5 @@ -import { - OnConnect, - SocketController, - ConnectedSocket, - OnDisconnect, - MessageBody, - OnMessage, -} from '../../src/decorators'; import { Message } from './Message'; +import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController() export class MessageController { diff --git a/sample/sample5-middlewares/app.ts b/sample/sample5-middlewares/app.ts index 9fcb98c1..8d9f3139 100644 --- a/sample/sample5-middlewares/app.ts +++ b/sample/sample5-middlewares/app.ts @@ -1,9 +1,12 @@ import 'reflect-metadata'; -import { createSocketServer } from '../../src/index'; +import { SocketControllers } from '../../src/index'; import { AuthenticationMiddleware } from './AuthenticationMiddleware'; import { MessageController } from './MessageController'; +import { Container } from 'typedi'; -createSocketServer(3001, { +new SocketControllers({ + port: 3001, + container: Container, controllers: [MessageController], middlewares: [AuthenticationMiddleware], }); // creates socket.io server and registers all controllers and middlewares there diff --git a/sample/sample6-dynamic-namespaces/MessageController.ts b/sample/sample6-dynamic-namespaces/MessageController.ts index 6b4c7aa1..12d98f3a 100644 --- a/sample/sample6-dynamic-namespaces/MessageController.ts +++ b/sample/sample6-dynamic-namespaces/MessageController.ts @@ -1,13 +1,13 @@ +import { Message } from './Message'; import { - OnConnect, - SocketController, ConnectedSocket, - OnDisconnect, MessageBody, - OnMessage, NspParams, -} from '../../src/decorators'; -import { Message } from './Message'; + OnConnect, + OnDisconnect, + OnMessage, + SocketController, +} from '../../src'; @SocketController('/messages/:id') export class MessageController { diff --git a/sample/sample6-dynamic-namespaces/app.ts b/sample/sample6-dynamic-namespaces/app.ts index 102196c2..a8e061f0 100644 --- a/sample/sample6-dynamic-namespaces/app.ts +++ b/sample/sample6-dynamic-namespaces/app.ts @@ -1,8 +1,11 @@ import 'reflect-metadata'; -import { createSocketServer } from '../../src/index'; +import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; +import { Container } from 'typedi'; -createSocketServer(3001, { +new SocketControllers({ + port: 3001, + container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there diff --git a/src/MiddlewareInterface.ts b/src/MiddlewareInterface.ts deleted file mode 100644 index a8b70baf..00000000 --- a/src/MiddlewareInterface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface MiddlewareInterface { - use(socket: any, next: (err?: any) => any): any; -} diff --git a/src/SocketControllerExecutor.ts b/src/SocketControllerExecutor.ts deleted file mode 100644 index e9f8c2ea..00000000 --- a/src/SocketControllerExecutor.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { MetadataBuilder } from './metadata-builder/MetadataBuilder'; -import { ActionMetadata } from './metadata/ActionMetadata'; -import { instanceToPlain, ClassTransformOptions, plainToInstance } from 'class-transformer'; -import { ActionTypes } from './metadata/types/ActionTypes'; -import { ParamMetadata } from './metadata/ParamMetadata'; -import { ParameterParseJsonError } from './error/ParameterParseJsonError'; -import { ParamTypes } from './metadata/types/ParamTypes'; -import { ControllerMetadata } from './metadata/ControllerMetadata'; -import { pathToRegexp } from 'path-to-regexp'; -import { Namespace } from 'socket.io'; -import { MiddlewareMetadata } from './metadata/MiddlewareMetadata'; - -/** - * Registers controllers and actions in the given server framework. - */ -export class SocketControllerExecutor { - // ------------------------------------------------------------------------- - // Public properties - // ------------------------------------------------------------------------- - - /** - * Indicates if class-transformer package should be used to perform message body serialization / deserialization. - * By default its enabled. - */ - useClassTransformer?: boolean; - - /** - * Global class transformer options passed to class-transformer during classToPlain operation. - * This operation is being executed when server returns response to user. - */ - classToPlainTransformOptions?: ClassTransformOptions; - - /** - * Global class transformer options passed to class-transformer during plainToInstance operation. - * This operation is being executed when parsing user parameters. - */ - plainToClassTransformOptions?: ClassTransformOptions; - - // ------------------------------------------------------------------------- - // Private properties - // ------------------------------------------------------------------------- - - private metadataBuilder: MetadataBuilder; - - // ------------------------------------------------------------------------- - // Constructor - // ------------------------------------------------------------------------- - - constructor(private io: any) { - this.metadataBuilder = new MetadataBuilder(); - } - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - execute(controllerClasses?: Function[], middlewareClasses?: Function[]) { - this.registerControllers(controllerClasses); - this.registerMiddlewares(middlewareClasses); - } - - // ------------------------------------------------------------------------- - // Private Methods - // ------------------------------------------------------------------------- - - /** - * Registers middlewares. - */ - private registerMiddlewares(classes?: Function[]): this { - const middlewares = this.metadataBuilder.buildMiddlewareMetadata(classes); - middlewares.sort((middleware1, middleware2) => (middleware1.priority || 0) - (middleware2.priority || 0)); - - const middlewaresWithoutNamespace = middlewares.filter(middleware => !middleware.namespace); - const middlewaresWithNamespace = middlewares.filter(middleware => !!middleware.namespace); - - for (const middleware of middlewaresWithoutNamespace) { - this.registerMiddleware(this.io as Namespace, middleware); - } - - this.io.on('new_namespace', (namespace: Namespace) => { - for (const middleware of middlewaresWithNamespace) { - const middlewareNamespaces = Array.isArray(middleware.namespace) - ? middleware.namespace - : [middleware.namespace]; - - const shouldApply = middlewareNamespaces.some(nsp => { - const nspRegexp = nsp instanceof RegExp ? nsp : pathToRegexp(nsp as string); - return nspRegexp.test(namespace.name); - }); - - if (shouldApply) { - this.registerMiddleware(namespace, middleware); - } - } - }); - - return this; - } - - /** - * Registers middleware. - */ - private registerMiddleware(namespace: Namespace, middleware: MiddlewareMetadata) { - namespace.use((socket: any, next: (err?: any) => any) => { - middleware.instance.use(socket, next); - }); - } - - /** - * Registers controllers. - */ - private registerControllers(classes?: Function[]): this { - const controllers = this.metadataBuilder.buildControllerMetadata(classes); - const controllersWithoutNamespaces = controllers.filter(ctrl => !ctrl.namespace); - const controllersWithNamespaces = controllers.filter(ctrl => !!ctrl.namespace); - - // register controllers without namespaces - this.io.on('connection', (socket: any) => this.handleConnection(controllersWithoutNamespaces, socket)); - - // register controllers with namespaces - controllersWithNamespaces.forEach(controller => { - let namespace: string | RegExp | undefined = controller.namespace; - if (namespace && !(namespace instanceof RegExp)) { - namespace = pathToRegexp(namespace); - } - this.io.of(namespace).on('connection', (socket: any) => this.handleConnection([controller], socket)); - }); - - return this; - } - - private handleConnection(controllers: ControllerMetadata[], socket: any) { - controllers.forEach(controller => { - (controller.actions || []).forEach(action => { - if (action.type === ActionTypes.CONNECT) { - this.handleAction(action, { socket: socket }) - .then(result => this.handleSuccessResult(result, action, socket)) - .catch(error => this.handleFailResult(error, action, socket)); - } else if (action.type === ActionTypes.DISCONNECT) { - socket.on('disconnect', () => { - this.handleAction(action, { socket: socket }) - .then(result => this.handleSuccessResult(result, action, socket)) - .catch(error => this.handleFailResult(error, action, socket)); - }); - } else if (action.type === ActionTypes.MESSAGE) { - socket.on(action.name, (data: any) => { - // todo get multiple args - this.handleAction(action, { socket: socket, data: data }) - .then(result => this.handleSuccessResult(result, action, socket)) - .catch(error => this.handleFailResult(error, action, socket)); - }); - } - }); - }); - } - - private handleAction(action: ActionMetadata, options: { socket?: any; data?: any }): Promise { - // compute all parameters - const paramsPromises = (action.params || []) - .sort((param1, param2) => param1.index - param2.index) - .map(param => { - if (param.type === ParamTypes.CONNECTED_SOCKET) { - return options.socket; - } else if (param.type === ParamTypes.SOCKET_IO) { - return this.io; - } else if (param.type === ParamTypes.SOCKET_QUERY_PARAM) { - return options.socket.handshake.query[param.value]; - } else if (param.type === ParamTypes.SOCKET_ID) { - return options.socket.id; - } else if (param.type === ParamTypes.SOCKET_REQUEST) { - return options.socket.request; - } else if (param.type === ParamTypes.SOCKET_ROOMS) { - return options.socket.rooms; - } else if (param.type === ParamTypes.NAMESPACE_PARAMS) { - return this.handleNamespaceParams(options.socket, action, param); - } else if (param.type === ParamTypes.NAMESPACE_PARAM) { - const params: any[] = this.handleNamespaceParams(options.socket, action, param); - return params[param.value]; - } else { - return this.handleParam(param, options); - } - }); - - // after all parameters are computed - const paramsPromise = Promise.all(paramsPromises).catch(error => { - console.log('Error during computation params of the socket controller: ', error); - throw error; - }); - return paramsPromise.then(params => { - return action.executeAction(params); - }); - } - - private handleParam(param: ParamMetadata, options: { socket?: any; data?: any }) { - let value = options.data; - if (value !== null && value !== undefined && value !== '') value = this.handleParamFormat(value, param); - - // if transform function is given for this param then apply it - if (param.transform) value = param.transform(value, options.socket); - - return value; - } - - private handleParamFormat(value: any, param: ParamMetadata): any { - const format = param.reflectedType; - const formatName = format instanceof Function && format.name ? format.name : format instanceof String ? format : ''; - switch (formatName.toLowerCase()) { - case 'number': - return +value; - - case 'string': - return value; - - case 'boolean': - if (value === 'true') { - return true; - } else if (value === 'false') { - return false; - } - return !!value; - - default: - const isObjectFormat = format instanceof Function || formatName.toLowerCase() === 'object'; - if (value && isObjectFormat) value = this.parseParamValue(value, param); - } - return value; - } - - private parseParamValue(value: any, paramMetadata: ParamMetadata) { - try { - const parseValue = typeof value === 'string' ? JSON.parse(value) : value; - if (paramMetadata.reflectedType !== Object && paramMetadata.reflectedType && this.useClassTransformer) { - const options = paramMetadata.classTransformOptions || this.plainToClassTransformOptions; - return plainToInstance(paramMetadata.reflectedType as never, parseValue as never, options); - } else { - return parseValue; - } - } catch (er) { - throw new ParameterParseJsonError(value); - } - } - - private handleSuccessResult(result: any, action: ActionMetadata, socket: any) { - if (result !== null && result !== undefined && action.emitOnSuccess) { - const transformOptions = action.emitOnSuccess.classTransformOptions || this.classToPlainTransformOptions; - const transformedResult = - this.useClassTransformer && result instanceof Object ? instanceToPlain(result, transformOptions) : result; - socket.emit(action.emitOnSuccess.value, transformedResult); - } else if ((result === null || result === undefined) && action.emitOnSuccess && !action.skipEmitOnEmptyResult) { - socket.emit(action.emitOnSuccess.value); - } - } - - private handleFailResult(result: any, action: ActionMetadata, socket: any) { - if (result !== null && result !== undefined && action.emitOnFail) { - const transformOptions = action.emitOnSuccess?.classTransformOptions || this.classToPlainTransformOptions; - let transformedResult = - this.useClassTransformer && result instanceof Object ? instanceToPlain(result, transformOptions) : result; - if (result instanceof Error && !Object.keys(transformedResult as never).length) { - transformedResult = result.toString(); - } - socket.emit(action.emitOnFail.value, transformedResult); - } else if ((result === null || result === undefined) && action.emitOnFail && !action.skipEmitOnEmptyResult) { - socket.emit(action.emitOnFail.value); - } - } - - private handleNamespaceParams(socket: any, action: ActionMetadata, param: ParamMetadata): any[] { - const keys: any[] = []; - const regexp = pathToRegexp(action.controllerMetadata.namespace || '/', keys); - const parts: any[] = regexp.exec(socket.nsp.name as string) || []; - const params: any[] = []; - keys.forEach((key: any, index: number) => { - params[key.name] = this.handleParamFormat(parts[index + 1], param); - }); - return params; - } -} diff --git a/src/SocketControllers.ts b/src/SocketControllers.ts new file mode 100644 index 00000000..305a3b9f --- /dev/null +++ b/src/SocketControllers.ts @@ -0,0 +1,312 @@ +import { Namespace, Server, Socket } from 'socket.io'; +import { sync } from 'glob'; +import { normalize } from 'path'; +import { SOCKET_CONTROLLER_META_KEY } from './types/SocketControllerMetaKey'; +import { pathToRegexp } from 'path-to-regexp'; +import { HandlerMetadata } from './types/HandlerMetadata'; +import { HandlerType } from './types/enums/HandlerType'; +import { SocketControllersOptions } from './types/SocketControllersOptions'; +import { ControllerMetadata } from './types/ControllerMetadata'; +import { MiddlewareMetadata } from './types/MiddlewareMetadata'; +import { ActionType } from './types/enums/ActionType'; +import { ActionMetadata } from './types/ActionMetadata'; +import { ParameterMetadata } from './types/ParameterMetadata'; +import { ParameterType } from './types/enums/ParameterType'; +import { ResultType } from './types/enums/ResultType'; +import { getMetadata } from './util/get-metadata'; +import { TransformOptions } from './types/TransformOptions'; +import { defaultTransformOptions } from './types/constants/defaultTransformOptions'; +import { ActionTransformOptions } from './types/ActionTransformOptions'; +import { instanceToPlain, plainToInstance } from 'class-transformer'; +import { MiddlewareInterface } from './types/MiddlewareInterface'; + +export class SocketControllers { + public container: { get(someClass: { new (...args: any[]): T } | Function): T }; + public controllers: HandlerMetadata[]; + public middlewares: HandlerMetadata[]; + public io: Server; + public transformOptions: TransformOptions; + + constructor(private options: SocketControllersOptions) { + this.container = options.container; + this.io = options.io || new Server(options.port); + this.transformOptions = { + ...defaultTransformOptions, + ...options.transformOption, + }; + this.controllers = this.loadHandlers( + options.controllers || [], + HandlerType.CONTROLLER + ); + this.middlewares = this.loadHandlers( + options.middlewares || [], + HandlerType.MIDDLEWARE + ); + + this.registerMiddlewares(); + this.registerControllers(); + } + + private loadHandlers( + handlers: Array, + type: HandlerType + ): HandlerMetadata[] { + const loadedHandlers: Function[] = []; + + for (const handler of handlers) { + if (typeof handler === 'string') { + loadedHandlers.push(...this.loadHandlersFromPath(handler, type)); + } else { + loadedHandlers.push(handler); + } + } + + return loadedHandlers.map(handler => { + return { + metadata: getMetadata(handler), + instance: this.container.get(handler), + }; + }); + } + + private loadHandlersFromPath(path: string, handlerType: HandlerType): Function[] { + const files = sync(normalize(path).replace(/\\/g, '/')); + + return files + .map(file => require(file)) + .reduce((loadedFiles: Function[], loadedFile: Record) => { + const handlersInFile = Object.values(loadedFile).filter(fileEntry => { + if (typeof fileEntry !== 'function') { + return false; + } + + if (!(Reflect as any).hasMetadata(SOCKET_CONTROLLER_META_KEY, fileEntry as Function)) { + return false; + } + + return (Reflect as any).getMetadata(SOCKET_CONTROLLER_META_KEY, fileEntry as Function).type === handlerType; + }); + loadedFiles.push(...(handlersInFile as Function[])); + + return loadedFiles; + }, []); + } + + private registerMiddlewares() { + const middlewares = this.middlewares.slice().sort((middleware1, middleware2) => { + return (middleware1.metadata.priority || 0) - (middleware2.metadata.priority || 0); + }); + + const middlewaresWithoutNamespace = middlewares.filter(middleware => !middleware.metadata.namespace); + const middlewaresWithNamespace = middlewares.filter(middleware => !!middleware.metadata.namespace); + + for (const middleware of middlewaresWithoutNamespace) { + this.registerMiddleware(this.io as unknown as Namespace, middleware.instance); + } + + this.io.on('new_namespace', (namespace: Namespace) => { + for (const middleware of middlewaresWithNamespace) { + const middlewareNamespaces = Array.isArray(middleware.metadata.namespace) + ? middleware.metadata.namespace + : [middleware.metadata.namespace]; + + const shouldApply = middlewareNamespaces.some(nsp => { + const nspRegexp = nsp instanceof RegExp ? nsp : pathToRegexp(nsp as string); + return nspRegexp.test(namespace.name); + }); + + if (shouldApply) { + this.registerMiddleware(namespace, middleware.instance); + } + } + }); + } + + private registerControllers() { + const controllersWithoutNamespace = this.controllers.filter(controller => !controller.metadata.namespace); + const controllersWithNamespace = this.controllers.filter(controller => !!controller.metadata.namespace); + + this.io.on('connection', (socket: Socket) => { + for (const controller of controllersWithoutNamespace) { + this.registerController(socket, controller); + } + }); + + for (const controller of controllersWithNamespace) { + this.io.of(pathToRegexp(controller.metadata.namespace as string)).on('connection', (socket: Socket) => { + this.registerController(socket, controller); + }); + } + } + + private registerController(socket: Socket, controller: HandlerMetadata) { + const connectedAction = Object.values(controller.metadata.actions || {}).find( + action => action.type === ActionType.CONNECT + ); + const disconnectedAction = Object.values(controller.metadata.actions || {}).find( + action => action.type === ActionType.DISCONNECT + ); + const messageActions = Object.values(controller.metadata.actions || {}).filter( + action => action.type === ActionType.MESSAGE + ); + + if (connectedAction) { + this.executeAction(socket, controller, connectedAction); + } + + if (disconnectedAction) { + socket.on('disconnect', () => { + this.executeAction(socket, controller, disconnectedAction); + }); + } + + for (const messageAction of messageActions) { + socket.on(messageAction.options.name, (message: any) => { + this.executeAction(socket, controller, messageAction, message); + }); + } + } + + private executeAction( + socket: Socket, + controller: HandlerMetadata, + action: ActionMetadata, + data?: any + ) { + const parameters = this.resolveParameters(socket, controller.metadata, action.parameters || [], data); + try { + const actionResult = controller.instance[action.methodName](...parameters); + Promise.resolve(actionResult) + .then(result => { + this.handleActionResult(socket, action, result, ResultType.EMIT_ON_SUCCESS); + }) + .catch(error => { + this.handleActionResult(socket, action, error, ResultType.EMIT_ON_FAIL); + }); + } catch (error: any) { + this.handleActionResult(socket, action, error, ResultType.EMIT_ON_FAIL); + } + } + + private handleActionResult(socket: Socket, action: ActionMetadata, result: any, resultType: ResultType) { + const onResultActions = action.results?.filter(result => result.type === resultType) || []; + const skipOnEmpty = action.results?.some(result => result.type === ResultType.SKIP_EMIT_ON_EMPTY_RESULT); + + if (result == null && skipOnEmpty) { + return; + } + + for (const onResultAction of onResultActions) { + const transformedValue = + result instanceof Error + ? result.message + : this.transformActionValue(result as never, null, onResultAction.options, 'result'); + socket.emit(onResultAction.options.messageName as never, transformedValue); + } + } + + private registerMiddleware(namespace: Namespace, middleware: MiddlewareInterface) { + namespace.use((socket: Socket, next: (err?: any) => void) => { + middleware.use(socket, next); + }); + } + + private resolveParameters( + socket: Socket, + controllerMetadata: ControllerMetadata, + parameterMetadatas: ParameterMetadata[], + data: any + ) { + const parameters = []; + + for (const metadata of parameterMetadatas) { + const parameterValue = this.resolveParameter(socket, controllerMetadata, metadata, data) as never; + parameters[metadata.index] = this.transformActionValue( + parameterValue, + metadata.reflectedType as never, + metadata.options, + 'parameter' + ); + } + + return parameters; + } + + private resolveParameter(socket: Socket, controller: ControllerMetadata, parameter: ParameterMetadata, data: any) { + switch (parameter.type) { + case ParameterType.CONNECTED_SOCKET: + return socket; + case ParameterType.SOCKET_ID: + return socket.id; + case ParameterType.SOCKET_IO: + return this.io; + case ParameterType.SOCKET_ROOMS: + return socket.rooms; + case ParameterType.MESSAGE_BODY: + return data; + case ParameterType.SOCKET_QUERY_PARAM: + return socket.handshake.query[parameter.options.name as string]; + case ParameterType.SOCKET_REQUEST: + return socket.request; + case ParameterType.NAMESPACE_PARAMS: + return this.extractNamespaceParameters(socket, controller.namespace, parameter); + case ParameterType.NAMESPACE_PARAM: + return this.extractNamespaceParameters(socket, controller.namespace, parameter)?.[ + parameter.options.name as string + ]; + } + } + + private transformActionValue( + value: never, + reflectedType: unknown, + options: ActionTransformOptions, + transformType: 'parameter' | 'result' + ) { + const transformOptions: TransformOptions = { + transform: options.transform ?? this.transformOptions.transform, + parameterTransformOptions: options.transformOptions ?? this.transformOptions.parameterTransformOptions, + resultTransformOptions: options.transformOptions ?? this.transformOptions.resultTransformOptions, + }; + + if (!transformOptions.transform) { + return value; + } + + if (typeof value !== 'object' || Array.isArray(value) || value == null) { + return value; + } + + if (transformType === 'parameter') { + return plainToInstance(reflectedType as never, value, transformOptions.parameterTransformOptions); + } + + if (transformType === 'result') { + return instanceToPlain(value, transformOptions.resultTransformOptions); + } + + return value; + } + + private extractNamespaceParameters( + socket: Socket, + namespace: string | RegExp | undefined, + parameterMetadata: ParameterMetadata + ) { + const keys: any[] = []; + const regexp = namespace instanceof RegExp ? namespace : pathToRegexp(namespace || '/', keys); + const parts: any[] = regexp.exec(socket.nsp.name) || []; + const params: Record = {}; + keys.forEach((key: any, index: number) => { + params[key.name as string] = parameterMetadata?.options?.transform + ? this.transformActionValue( + parts[index + 1] as never, + parameterMetadata.reflectedType, + parameterMetadata.options, + 'parameter' + ) + : parts[index + 1]; + }); + return params; + } +} diff --git a/src/SocketControllersOptions.ts b/src/SocketControllersOptions.ts deleted file mode 100644 index ad7a9a51..00000000 --- a/src/SocketControllersOptions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ClassTransformOptions } from 'class-transformer'; - -/** - * Socket controllers initialization options. - */ -export interface SocketControllersOptions { - /** - * List of directories from where to "require" all your controllers. - */ - controllers?: Function[] | string[]; - - /** - * List of directories from where to "require" all your middlewares. - */ - middlewares?: Function[] | string[]; - - /** - * Indicates if class-transformer package should be used to perform message body serialization / deserialization. - * By default its enabled. - */ - useClassTransformer?: boolean; - - /** - * Global class transformer options passed to class-transformer during classToPlain operation. - * This operation is being executed when server returns response to user. - */ - classToPlainTransformOptions?: ClassTransformOptions; - - /** - * Global class transformer options passed to class-transformer during plainToClass operation. - * This operation is being executed when parsing user parameters. - */ - plainToClassTransformOptions?: ClassTransformOptions; -} diff --git a/src/container.ts b/src/container.ts deleted file mode 100644 index 14e3ae97..00000000 --- a/src/container.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Container options. - */ -export interface UseContainerOptions { - /** - * If set to true, then default container will be used in the case if given container haven't returned anything. - */ - fallback?: boolean; - - /** - * If set to true, then default container will be used in the case if given container thrown an exception. - */ - fallbackOnErrors?: boolean; -} - -/** - * Container to be used by this library for inversion control. If container was not implicitly set then by default - * container simply creates a new instance of the given class. - */ -const defaultContainer: { get(someClass: { new (...args: any[]): T } | Function): T } = new (class { - private instances: { type: Function; object: any }[] = []; - get(someClass: { new (...args: any[]): T }): T { - let instance = this.instances.find(instance => instance.type === someClass); - if (!instance) { - instance = { type: someClass, object: new someClass() }; - this.instances.push(instance); - } - - return instance.object; - } -})(); - -let userContainer: { get(someClass: { new (...args: any[]): T } | Function): T }; -let userContainerOptions: UseContainerOptions | undefined; - -/** - * Sets container to be used by this library. - */ -export function useContainer(iocContainer: { get(someClass: any): any }, options?: UseContainerOptions) { - userContainer = iocContainer; - userContainerOptions = options; -} - -/** - * Gets the IOC container used by this library. - */ -export function getFromContainer(someClass: { new (...args: any[]): T } | Function): T { - if (userContainer) { - try { - const instance = userContainer.get(someClass); - if (instance) return instance; - - if (!userContainerOptions || !userContainerOptions.fallback) return instance; - } catch (error) { - if (!userContainerOptions || !userContainerOptions.fallbackOnErrors) throw error; - } - } - return defaultContainer.get(someClass); -} diff --git a/src/decorators.ts b/src/decorators.ts deleted file mode 100644 index e98decd7..00000000 --- a/src/decorators.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { defaultMetadataArgsStorage } from './index'; -import { SocketControllerMetadataArgs } from './metadata/args/SocketControllerMetadataArgs'; -import { ActionMetadataArgs } from './metadata/args/ActionMetadataArgs'; -import { ActionTypes } from './metadata/types/ActionTypes'; -import { ParamMetadataArgs } from './metadata/args/ParamMetadataArgs'; -import { ParamTypes } from './metadata/types/ParamTypes'; -import { ClassTransformOptions } from 'class-transformer'; -import { MiddlewareMetadataArgs } from './metadata/args/MiddlewareMetadataArgs'; -import { ResultMetadataArgs } from './metadata/args/ResultMetadataArgs'; -import { ResultTypes } from './metadata/types/ResultTypes'; - -/** - * Registers a class to be a socket controller that can listen to websocket events and respond to them. - * - * @param namespace Namespace in which this controller's events will be registered. - */ -export function SocketController(namespace?: string | RegExp) { - return function (object: Function) { - const metadata: SocketControllerMetadataArgs = { - namespace: namespace, - target: object, - }; - defaultMetadataArgsStorage().controllers.push(metadata); - }; -} - -/** - * Registers controller's action to be executed when socket receives message with given name. - */ -export function OnMessage(name?: string): Function { - return function (object: Object, methodName: string) { - const metadata: ActionMetadataArgs = { - name: name, - target: object.constructor, - method: methodName, - type: ActionTypes.MESSAGE, - }; - defaultMetadataArgsStorage().actions.push(metadata); - }; -} - -/** - * Registers controller's action to be executed when client connects to the socket. - */ -export function OnConnect(): Function { - return function (object: Object, methodName: string) { - const metadata: ActionMetadataArgs = { - target: object.constructor, - method: methodName, - type: ActionTypes.CONNECT, - }; - defaultMetadataArgsStorage().actions.push(metadata); - }; -} - -/** - * Registers controller's action to be executed when client disconnects from the socket. - */ -export function OnDisconnect(): Function { - return function (object: Object, methodName: string) { - const metadata: ActionMetadataArgs = { - target: object.constructor, - method: methodName, - type: ActionTypes.DISCONNECT, - }; - defaultMetadataArgsStorage().actions.push(metadata); - }; -} - -/** - * Injects connected client's socket object to the controller action. - */ -export function ConnectedSocket() { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.CONNECTED_SOCKET, - reflectedType: format, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects socket.io object that initialized a connection. - */ -export function SocketIO() { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.SOCKET_IO, - reflectedType: format, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects received message body. - */ -export function MessageBody(options?: { classTransformOptions?: ClassTransformOptions }) { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.SOCKET_BODY, - reflectedType: format, - classTransformOptions: options && options.classTransformOptions ? options.classTransformOptions : undefined, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects query parameter from the received socket request. - */ -export function SocketQueryParam(name?: string) { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.SOCKET_QUERY_PARAM, - reflectedType: format, - value: name, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects socket id from the received request. - */ -export function SocketId() { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.SOCKET_ID, - reflectedType: format, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects request object received by socket. - */ -export function SocketRequest() { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.SOCKET_REQUEST, - reflectedType: format, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects parameters of the connected socket namespace. - */ -export function NspParams() { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.NAMESPACE_PARAMS, - reflectedType: format, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects named param from the connected socket namespace. - */ -export function NspParam(name: string) { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.NAMESPACE_PARAM, - reflectedType: format, - value: name, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Injects rooms of the connected socket client. - */ -export function SocketRooms() { - return function (object: Object, methodName: string, index: number) { - const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; - const metadata: ParamMetadataArgs = { - target: object.constructor, - method: methodName, - index: index, - type: ParamTypes.SOCKET_ROOMS, - reflectedType: format, - }; - defaultMetadataArgsStorage().params.push(metadata); - }; -} - -/** - * Registers a new middleware to be registered in the socket.io. - */ -export function Middleware(options?: { - priority?: number; - namespace?: string | RegExp | Array; -}): Function { - return function (object: Function) { - const metadata: MiddlewareMetadataArgs = { - target: object, - priority: options?.priority, - namespace: options?.namespace, - }; - defaultMetadataArgsStorage().middlewares.push(metadata); - }; -} - -/** - * If this decorator is set then after controller action will emit message with the given name after action execution. - * It will emit message only if controller succeed without errors. - * If result is a Promise then it will wait until promise is resolved and emit a message. - */ -export function EmitOnSuccess( - messageName: string, - options?: { classTransformOptions?: ClassTransformOptions } -): Function { - return function (object: Object, methodName: string) { - const metadata: ResultMetadataArgs = { - target: object.constructor, - method: methodName, - type: ResultTypes.EMIT_ON_SUCCESS, - value: messageName, - classTransformOptions: options && options.classTransformOptions ? options.classTransformOptions : undefined, - }; - defaultMetadataArgsStorage().results.push(metadata); - }; -} - -/** - * If this decorator is set then after controller action will emit message with the given name after action execution. - * It will emit message only if controller throw an exception. - * If result is a Promise then it will wait until promise throw an error and emit a message. - */ -export function EmitOnFail(messageName: string, options?: { classTransformOptions?: ClassTransformOptions }): Function { - return function (object: Object, methodName: string) { - const metadata: ResultMetadataArgs = { - target: object.constructor, - method: methodName, - type: ResultTypes.EMIT_ON_FAIL, - value: messageName, - classTransformOptions: options && options.classTransformOptions ? options.classTransformOptions : undefined, - }; - defaultMetadataArgsStorage().results.push(metadata); - }; -} - -/** - * Used in conjunction with @EmitOnSuccess and @EmitOnFail decorators. - * If result returned by controller action is null or undefined then messages will not be emitted by @EmitOnSuccess - * or @EmitOnFail decorators. - */ -export function SkipEmitOnEmptyResult(): Function { - return function (object: Object, methodName: string) { - const metadata: ResultMetadataArgs = { - target: object.constructor, - method: methodName, - type: ResultTypes.SKIP_EMIT_ON_EMPTY_RESULT, - }; - defaultMetadataArgsStorage().results.push(metadata); - }; -} diff --git a/src/decorators/ConnectedSocket.ts b/src/decorators/ConnectedSocket.ts new file mode 100644 index 00000000..ef68ebfb --- /dev/null +++ b/src/decorators/ConnectedSocket.ts @@ -0,0 +1,17 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function ConnectedSocket() { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.CONNECTED_SOCKET, + options: { + transform: false, + }, + }); + }; +} diff --git a/src/decorators/EmitOnFail.ts b/src/decorators/EmitOnFail.ts new file mode 100644 index 00000000..47c796a2 --- /dev/null +++ b/src/decorators/EmitOnFail.ts @@ -0,0 +1,15 @@ +import { addResultToActionMetadata } from '../util/add-result-to-action-metadata'; +import { ResultType } from '../types/enums/ResultType'; +import { ActionTransformOptions } from '../types/ActionTransformOptions'; + +export function EmitOnFail(messageName: string, options?: ActionTransformOptions): Function { + return function (object: Object, methodName: string) { + addResultToActionMetadata(object.constructor, methodName, { + type: ResultType.EMIT_ON_FAIL, + options: { + messageName, + ...options, + }, + }); + }; +} diff --git a/src/decorators/EmitOnSuccess.ts b/src/decorators/EmitOnSuccess.ts new file mode 100644 index 00000000..a68d829b --- /dev/null +++ b/src/decorators/EmitOnSuccess.ts @@ -0,0 +1,15 @@ +import { addResultToActionMetadata } from '../util/add-result-to-action-metadata'; +import { ResultType } from '../types/enums/ResultType'; +import { ActionTransformOptions } from '../types/ActionTransformOptions'; + +export function EmitOnSuccess(messageName: string, options?: ActionTransformOptions): Function { + return function (object: Object, methodName: string) { + addResultToActionMetadata(object.constructor, methodName, { + type: ResultType.EMIT_ON_SUCCESS, + options: { + messageName, + ...options, + }, + }); + }; +} diff --git a/src/decorators/MessageBody.ts b/src/decorators/MessageBody.ts new file mode 100644 index 00000000..403939c0 --- /dev/null +++ b/src/decorators/MessageBody.ts @@ -0,0 +1,17 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; +import { ActionTransformOptions } from '../types/ActionTransformOptions'; + +export function MessageBody(options?: ActionTransformOptions) { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.MESSAGE_BODY, + options: { + ...options, + }, + }); + }; +} diff --git a/src/decorators/Middleware.ts b/src/decorators/Middleware.ts new file mode 100644 index 00000000..4b97d7a2 --- /dev/null +++ b/src/decorators/Middleware.ts @@ -0,0 +1,15 @@ +import { HandlerType } from '../types/enums/HandlerType'; +import { addMiddlewareMetadata } from '../util/add-middleware-metadata'; + +export function Middleware(options?: { + priority?: number; + namespace?: string | RegExp | Array; +}): Function { + return function (object: Function) { + addMiddlewareMetadata(object, { + type: HandlerType.MIDDLEWARE, + namespace: options?.namespace, + priority: options?.priority, + }); + }; +} diff --git a/src/decorators/NspParam.ts b/src/decorators/NspParam.ts new file mode 100644 index 00000000..6f58135a --- /dev/null +++ b/src/decorators/NspParam.ts @@ -0,0 +1,15 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function NspParam(name: string) { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.NAMESPACE_PARAM, + options: { name, transform: false }, + }); + }; +} diff --git a/src/decorators/NspParams.ts b/src/decorators/NspParams.ts new file mode 100644 index 00000000..20eaaea0 --- /dev/null +++ b/src/decorators/NspParams.ts @@ -0,0 +1,15 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function NspParams() { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.NAMESPACE_PARAMS, + options: { transform: false }, + }); + }; +} diff --git a/src/decorators/OnConnect.ts b/src/decorators/OnConnect.ts new file mode 100644 index 00000000..02e9ac51 --- /dev/null +++ b/src/decorators/OnConnect.ts @@ -0,0 +1,12 @@ +import { addActionToControllerMetadata } from '../util/add-action-to-controller-metadata'; +import { ActionType } from '../types/enums/ActionType'; + +export function OnConnect(): Function { + return function (object: Object, methodName: string) { + addActionToControllerMetadata(object.constructor, { + methodName, + type: ActionType.CONNECT, + options: {}, + }); + }; +} diff --git a/src/decorators/OnDisconnect.ts b/src/decorators/OnDisconnect.ts new file mode 100644 index 00000000..a1bb83a1 --- /dev/null +++ b/src/decorators/OnDisconnect.ts @@ -0,0 +1,12 @@ +import { addActionToControllerMetadata } from '../util/add-action-to-controller-metadata'; +import { ActionType } from '../types/enums/ActionType'; + +export function OnDisconnect(): Function { + return function (object: Object, methodName: string) { + addActionToControllerMetadata(object.constructor, { + methodName, + type: ActionType.DISCONNECT, + options: {}, + }); + }; +} diff --git a/src/decorators/OnMessage.ts b/src/decorators/OnMessage.ts new file mode 100644 index 00000000..0c29249e --- /dev/null +++ b/src/decorators/OnMessage.ts @@ -0,0 +1,12 @@ +import { addActionToControllerMetadata } from '../util/add-action-to-controller-metadata'; +import { ActionType } from '../types/enums/ActionType'; + +export function OnMessage(name?: string): Function { + return function (object: Object, methodName: string) { + addActionToControllerMetadata(object.constructor, { + methodName, + type: ActionType.MESSAGE, + options: { name }, + }); + }; +} diff --git a/src/decorators/SkipEmitOnEmptyResult.ts b/src/decorators/SkipEmitOnEmptyResult.ts new file mode 100644 index 00000000..7506a156 --- /dev/null +++ b/src/decorators/SkipEmitOnEmptyResult.ts @@ -0,0 +1,11 @@ +import { addResultToActionMetadata } from '../util/add-result-to-action-metadata'; +import { ResultType } from '../types/enums/ResultType'; + +export function SkipEmitOnEmptyResult(): Function { + return function (object: Object, methodName: string) { + addResultToActionMetadata(object.constructor, methodName, { + type: ResultType.SKIP_EMIT_ON_EMPTY_RESULT, + options: {}, + }); + }; +} diff --git a/src/decorators/SocketController.ts b/src/decorators/SocketController.ts new file mode 100644 index 00000000..a19e7eaa --- /dev/null +++ b/src/decorators/SocketController.ts @@ -0,0 +1,8 @@ +import { HandlerType } from '../types/enums/HandlerType'; +import { addControllerMetadata } from '../util/add-controller-metadata'; + +export function SocketController(namespace?: string | RegExp) { + return function (object: Function) { + addControllerMetadata(object, { namespace, type: HandlerType.CONTROLLER }); + }; +} diff --git a/src/decorators/SocketIO.ts b/src/decorators/SocketIO.ts new file mode 100644 index 00000000..94220e77 --- /dev/null +++ b/src/decorators/SocketIO.ts @@ -0,0 +1,15 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function SocketIO() { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.SOCKET_IO, + options: { transform: false }, + }); + }; +} diff --git a/src/decorators/SocketId.ts b/src/decorators/SocketId.ts new file mode 100644 index 00000000..799a5c50 --- /dev/null +++ b/src/decorators/SocketId.ts @@ -0,0 +1,15 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function SocketId() { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.SOCKET_ID, + options: { transform: false }, + }); + }; +} diff --git a/src/decorators/SocketQueryParam.ts b/src/decorators/SocketQueryParam.ts new file mode 100644 index 00000000..6251f5cb --- /dev/null +++ b/src/decorators/SocketQueryParam.ts @@ -0,0 +1,15 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function SocketQueryParam(name?: string) { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.SOCKET_QUERY_PARAM, + options: { name, transform: false }, + }); + }; +} diff --git a/src/decorators/SocketRequest.ts b/src/decorators/SocketRequest.ts new file mode 100644 index 00000000..46cb9bd7 --- /dev/null +++ b/src/decorators/SocketRequest.ts @@ -0,0 +1,15 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function SocketRequest() { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.SOCKET_REQUEST, + options: { transform: false }, + }); + }; +} diff --git a/src/decorators/SocketRooms.ts b/src/decorators/SocketRooms.ts new file mode 100644 index 00000000..39dbdaff --- /dev/null +++ b/src/decorators/SocketRooms.ts @@ -0,0 +1,15 @@ +import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; +import { ParameterType } from '../types/enums/ParameterType'; + +export function SocketRooms() { + return function (object: Object, methodName: string, index: number) { + const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; + + addParameterToActionMetadata(object.constructor, methodName, { + index, + reflectedType: format, + type: ParameterType.SOCKET_ROOMS, + options: { transform: false }, + }); + }; +} diff --git a/src/dummy.spec.ts b/src/dummy.spec.ts deleted file mode 100644 index fc4de7cc..00000000 --- a/src/dummy.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('Dummy', () => { - it('should pass', () => { - expect(true).toBe(true); - }); -}); diff --git a/src/error/ParameterParseJsonError.ts b/src/error/ParameterParseJsonError.ts deleted file mode 100644 index e84fd2d5..00000000 --- a/src/error/ParameterParseJsonError.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Caused when user parameter is given, but is invalid and cannot be parsed. - */ -export class ParameterParseJsonError extends Error { - name = 'ParameterParseJsonError'; - - constructor(value: any) { - super('Parameter is invalid. Value (' + JSON.stringify(value) + ') cannot be parsed to JSON'); - this.stack = new Error().stack; - } -} diff --git a/src/index.ts b/src/index.ts index 758fedaa..89828988 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,87 +1,23 @@ -import { MetadataArgsStorage } from './metadata-builder/MetadataArgsStorage'; -import { importClassesFromDirectories } from './util/DirectoryExportedClassesLoader'; -import { SocketControllerExecutor } from './SocketControllerExecutor'; -import { SocketControllersOptions } from './SocketControllersOptions'; - -// ------------------------------------------------------------------------- -// Main Functions -// ------------------------------------------------------------------------- - -/** - * Registers all loaded actions in your express application. - */ -export function useSocketServer(io: T, options?: SocketControllersOptions): T { - createExecutor(io, options || {}); - return io; -} - -/** - * Registers all loaded actions in your express application. - */ -export function createSocketServer(port: number, options?: SocketControllersOptions): any { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const io = require('socket.io')(port); - createExecutor(io, options || {}); - return io; -} - -/** - * Registers all loaded actions in your express application. - */ -function createExecutor(io: any, options: SocketControllersOptions): void { - const executor = new SocketControllerExecutor(io); - - // second import all controllers and middlewares and error handlers - let controllerClasses: Function[] = []; - if (options?.controllers?.length) { - controllerClasses = (options.controllers as any[]).filter(controller => controller instanceof Function); - const controllerDirs = (options.controllers as any[]).filter( - controller => typeof controller === 'string' - ) as string[]; - controllerClasses.push(...importClassesFromDirectories(controllerDirs)); - } - - let middlewareClasses: Function[] = []; - if (options?.middlewares?.length) { - middlewareClasses = (options.middlewares as any[]).filter(controller => controller instanceof Function); - const middlewareDirs = (options.middlewares as any[]).filter( - controller => typeof controller === 'string' - ) as string[]; - middlewareClasses.push(...importClassesFromDirectories(middlewareDirs)); - } - - if (options.useClassTransformer !== undefined) { - executor.useClassTransformer = options.useClassTransformer; - } else { - executor.useClassTransformer = true; - } - - executor.classToPlainTransformOptions = options.classToPlainTransformOptions; - executor.plainToClassTransformOptions = options.plainToClassTransformOptions; - - // run socket controller register and other operations - executor.execute(controllerClasses, middlewareClasses); -} - -// ------------------------------------------------------------------------- -// Global Metadata Storage -// ------------------------------------------------------------------------- - -/** - * Gets the metadata arguments storage. - */ -export function defaultMetadataArgsStorage(): MetadataArgsStorage { - if (!(global as any).socketControllersMetadataArgsStorage) - (global as any).socketControllersMetadataArgsStorage = new MetadataArgsStorage(); - - return (global as any).socketControllersMetadataArgsStorage; -} - -// ------------------------------------------------------------------------- -// Commonly Used exports -// ------------------------------------------------------------------------- - -export * from './container'; -export * from './decorators'; -export * from './SocketControllersOptions'; -export * from './MiddlewareInterface'; +export * from './decorators/ConnectedSocket'; +export * from './decorators/EmitOnFail'; +export * from './decorators/EmitOnSuccess'; +export * from './decorators/MessageBody'; +export * from './decorators/Middleware'; +export * from './decorators/NspParam'; +export * from './decorators/NspParams'; +export * from './decorators/OnConnect'; +export * from './decorators/OnDisconnect'; +export * from './decorators/OnMessage'; +export * from './decorators/SkipEmitOnEmptyResult'; +export * from './decorators/SocketController'; +export * from './decorators/SocketId'; +export * from './decorators/SocketIO'; +export * from './decorators/SocketQueryParam'; +export * from './decorators/SocketRequest'; +export * from './decorators/SocketRooms'; + +export * from './types/MiddlewareInterface'; +export * from './types/TransformOptions'; +export * from './types/SocketControllersOptions'; + +export * from './SocketControllers'; diff --git a/src/metadata-builder/MetadataArgsStorage.ts b/src/metadata-builder/MetadataArgsStorage.ts deleted file mode 100644 index 225fd030..00000000 --- a/src/metadata-builder/MetadataArgsStorage.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { SocketControllerMetadataArgs } from '../metadata/args/SocketControllerMetadataArgs'; -import { ActionMetadataArgs } from '../metadata/args/ActionMetadataArgs'; -import { ParamMetadataArgs } from '../metadata/args/ParamMetadataArgs'; -import { MiddlewareMetadataArgs } from '../metadata/args/MiddlewareMetadataArgs'; -import { ResultMetadataArgs } from '../metadata/args/ResultMetadataArgs'; - -/** - * Storage all metadatas read from decorators. - */ -export class MetadataArgsStorage { - // ------------------------------------------------------------------------- - // Properties - // ------------------------------------------------------------------------- - - controllers: SocketControllerMetadataArgs[] = []; - middlewares: MiddlewareMetadataArgs[] = []; - actions: ActionMetadataArgs[] = []; - results: ResultMetadataArgs[] = []; - params: ParamMetadataArgs[] = []; - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - findControllerMetadatasForClasses(classes: Function[]): SocketControllerMetadataArgs[] { - return this.controllers.filter(ctrl => { - return classes.filter(cls => ctrl.target === cls).length > 0; - }); - } - - findMiddlewareMetadatasForClasses(classes: Function[]): MiddlewareMetadataArgs[] { - return this.middlewares.filter(middleware => { - return classes.filter(cls => middleware.target === cls).length > 0; - }); - } - - findActionsWithTarget(target: Function): ActionMetadataArgs[] { - return this.actions.filter(action => action.target === target); - } - - findResutlsWithTargetAndMethod(target: Function, methodName: string): ResultMetadataArgs[] { - return this.results.filter(result => { - return result.target === target && result.method === methodName; - }); - } - - findParamsWithTargetAndMethod(target: Function, methodName: string): ParamMetadataArgs[] { - return this.params.filter(param => { - return param.target === target && param.method === methodName; - }); - } - - /** - * Removes all saved metadata. - */ - reset() { - this.controllers = []; - this.middlewares = []; - this.actions = []; - this.params = []; - } -} diff --git a/src/metadata-builder/MetadataBuilder.ts b/src/metadata-builder/MetadataBuilder.ts deleted file mode 100644 index cc61d018..00000000 --- a/src/metadata-builder/MetadataBuilder.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { defaultMetadataArgsStorage } from '../index'; -import { ControllerMetadata } from '../metadata/ControllerMetadata'; -import { ActionMetadata } from '../metadata/ActionMetadata'; -import { ParamMetadata } from '../metadata/ParamMetadata'; -import { MiddlewareMetadata } from '../metadata/MiddlewareMetadata'; -import { ResultMetadata } from '../metadata/ResultMetadata'; - -/** - * Builds metadata from the given metadata arguments. - */ -export class MetadataBuilder { - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - buildControllerMetadata(classes?: Function[]) { - return this.createControllers(classes); - } - - buildMiddlewareMetadata(classes?: Function[]) { - return this.createMiddlewares(classes); - } - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - private createMiddlewares(classes?: Function[]): MiddlewareMetadata[] { - const storage = defaultMetadataArgsStorage(); - const middlewares = !classes ? storage.middlewares : storage.findMiddlewareMetadatasForClasses(classes); - return middlewares.map(middlewareArgs => { - return new MiddlewareMetadata(middlewareArgs); - }); - } - - private createControllers(classes?: Function[]): ControllerMetadata[] { - const storage = defaultMetadataArgsStorage(); - const controllers = !classes ? storage.controllers : storage.findControllerMetadatasForClasses(classes); - return controllers.map(controllerArgs => { - const controller = new ControllerMetadata(controllerArgs); - controller.actions = this.createActions(controller); - return controller; - }); - } - - private createActions(controller: ControllerMetadata): ActionMetadata[] { - return defaultMetadataArgsStorage() - .findActionsWithTarget(controller.target) - .map(actionArgs => { - const action = new ActionMetadata(controller, actionArgs); - action.params = this.createParams(action); - action.results = this.createResults(action); - return action; - }); - } - - private createParams(action: ActionMetadata): ParamMetadata[] { - return defaultMetadataArgsStorage() - .findParamsWithTargetAndMethod(action.target, action.method) - .map(paramArgs => new ParamMetadata(action, paramArgs)); - } - - private createResults(action: ActionMetadata): ResultMetadata[] { - return defaultMetadataArgsStorage() - .findResutlsWithTargetAndMethod(action.target, action.method) - .map(resultArgs => new ResultMetadata(action, resultArgs)); - } -} diff --git a/src/metadata/ActionMetadata.ts b/src/metadata/ActionMetadata.ts deleted file mode 100644 index dac387f5..00000000 --- a/src/metadata/ActionMetadata.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ParamMetadata } from './ParamMetadata'; -import { ActionMetadataArgs } from './args/ActionMetadataArgs'; -import { ActionType } from './types/ActionTypes'; -import { ControllerMetadata } from './ControllerMetadata'; -import { ResultMetadata } from './ResultMetadata'; -import { ResultTypes } from './types/ResultTypes'; - -export class ActionMetadata { - // ------------------------------------------------------------------------- - // Properties - // ------------------------------------------------------------------------- - - /** - * Action's controller. - */ - controllerMetadata: ControllerMetadata; - - /** - * Action's parameters. - */ - params?: ParamMetadata[]; - - /** - * Action's result handlers. - */ - results?: ResultMetadata[]; - - /** - * Message name served by this action. - */ - name?: string; - - /** - * Class on which's method this action is attached. - */ - target: Function; - - /** - * Object's method that will be executed on this action. - */ - method: string; - - /** - * Action type represents http method used for the registered route. Can be one of the value defined in ActionTypes - * class. - */ - type: ActionType; - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - constructor(controllerMetadata: ControllerMetadata, args: ActionMetadataArgs) { - this.controllerMetadata = controllerMetadata; - this.name = args.name; - this.target = args.target; - this.method = args.method; - this.type = args.type; - } - - // ------------------------------------------------------------------------- - // Accessors - // ------------------------------------------------------------------------- - - get emitOnSuccess() { - return (this.results || []).find(resultHandler => resultHandler.type === ResultTypes.EMIT_ON_SUCCESS); - } - - get emitOnFail() { - return (this.results || []).find(resultHandler => resultHandler.type === ResultTypes.EMIT_ON_FAIL); - } - - get skipEmitOnEmptyResult() { - return (this.results || []).find(resultHandler => resultHandler.type === ResultTypes.SKIP_EMIT_ON_EMPTY_RESULT); - } - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - executeAction(params: any[]) { - // TODO: remove fix this eslint warning - // eslint-disable-next-line prefer-spread - return this.controllerMetadata.instance[this.method].apply(this.controllerMetadata.instance, params); - } -} diff --git a/src/metadata/ControllerMetadata.ts b/src/metadata/ControllerMetadata.ts deleted file mode 100644 index 55fbe17f..00000000 --- a/src/metadata/ControllerMetadata.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ActionMetadata } from './ActionMetadata'; -import { SocketControllerMetadataArgs } from './args/SocketControllerMetadataArgs'; -import { getFromContainer } from '../container'; - -export class ControllerMetadata { - // ------------------------------------------------------------------------- - // Properties - // ------------------------------------------------------------------------- - - /** - * Controller actions. - */ - actions?: ActionMetadata[]; - - /** - * Indicates object which is used by this controller. - */ - target: Function; - - /** - * Base route for all actions registered in this controller. - */ - namespace?: string | RegExp; - - // ------------------------------------------------------------------------- - // Constructor - // ------------------------------------------------------------------------- - - constructor(args: SocketControllerMetadataArgs) { - this.target = args.target; - this.namespace = args.namespace; - } - - // ------------------------------------------------------------------------- - // Accessors - // ------------------------------------------------------------------------- - - get instance(): any { - return getFromContainer(this.target); - } -} diff --git a/src/metadata/MiddlewareMetadata.ts b/src/metadata/MiddlewareMetadata.ts deleted file mode 100644 index 6db327f2..00000000 --- a/src/metadata/MiddlewareMetadata.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getFromContainer } from '../container'; -import { MiddlewareMetadataArgs } from './args/MiddlewareMetadataArgs'; -import { MiddlewareInterface } from '../MiddlewareInterface'; - -export class MiddlewareMetadata { - // ------------------------------------------------------------------------- - // Properties - // ------------------------------------------------------------------------- - - target: Function; - priority?: number; - namespace?: string | RegExp | Array; - - // ------------------------------------------------------------------------- - // Constructor - // ------------------------------------------------------------------------- - - constructor(args: MiddlewareMetadataArgs) { - this.target = args.target; - this.priority = args.priority; - this.namespace = args.namespace; - } - - // ------------------------------------------------------------------------- - // Accessors - // ------------------------------------------------------------------------- - - get instance(): MiddlewareInterface { - return getFromContainer(this.target); - } -} diff --git a/src/metadata/ParamMetadata.ts b/src/metadata/ParamMetadata.ts deleted file mode 100644 index 3798452b..00000000 --- a/src/metadata/ParamMetadata.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ActionMetadata } from './ActionMetadata'; -import { ParamMetadataArgs } from './args/ParamMetadataArgs'; -import { ParamTypes } from './types/ParamTypes'; -import { ClassTransformOptions } from 'class-transformer'; - -export class ParamMetadata { - // ------------------------------------------------------------------------- - // Properties - // ------------------------------------------------------------------------- - - /** - * Parameter's action. - */ - actionMetadata: ActionMetadata; - - /** - * Parameter target. - */ - target: Function; - - /** - * Method on which's parameter is attached. - */ - method: string; - - /** - * Index (# number) of the parameter in the method signature. - */ - index: number; - - /** - * Parameter type. - */ - type: ParamTypes; - - /** - * Extra parameter value. - */ - value?: any; - - /** - * Reflected type of the parameter. - */ - reflectedType: any; - - /** - * Transforms the value. - */ - transform?: (value: any, socket: any) => Promise | any; - - /** - * Class transform options used to perform plainToClass operation. - */ - classTransformOptions?: ClassTransformOptions; - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - constructor(actionMetadata: ActionMetadata, args: ParamMetadataArgs) { - this.actionMetadata = actionMetadata; - this.target = args.target; - this.method = args.method; - this.reflectedType = args.reflectedType; - this.index = args.index; - this.type = args.type; - this.transform = args.transform; - this.classTransformOptions = args.classTransformOptions; - this.value = args.value; - } -} diff --git a/src/metadata/ResultMetadata.ts b/src/metadata/ResultMetadata.ts deleted file mode 100644 index 6eabd2fc..00000000 --- a/src/metadata/ResultMetadata.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ActionMetadata } from './ActionMetadata'; -import { ResultType } from './types/ResultTypes'; -import { ResultMetadataArgs } from './args/ResultMetadataArgs'; -import { ClassTransformOptions } from 'class-transformer'; - -export class ResultMetadata { - // ------------------------------------------------------------------------- - // Properties - // ------------------------------------------------------------------------- - - /** - */ - actionMetadata: ActionMetadata; - - /** - */ - target: Function; - - /** - */ - method: string; - - /** - */ - type: ResultType; - - /** - */ - value?: any; - - classTransformOptions?: ClassTransformOptions; - - // ------------------------------------------------------------------------- - // Public Methods - // ------------------------------------------------------------------------- - - constructor(action: ActionMetadata, args: ResultMetadataArgs) { - this.actionMetadata = action; - this.target = args.target; - this.method = args.method; - this.type = args.type; - this.value = args.value; - this.classTransformOptions = args.classTransformOptions; - } -} diff --git a/src/metadata/args/ActionMetadataArgs.ts b/src/metadata/args/ActionMetadataArgs.ts deleted file mode 100644 index 49ec3e61..00000000 --- a/src/metadata/args/ActionMetadataArgs.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ActionType } from '../types/ActionTypes'; - -/** - * Action metadata used to storage information about registered action. - */ -export interface ActionMetadataArgs { - /** - * Message's name to listen to. - */ - name?: string; - - /** - * Class on which's method this action is attached. - */ - target: Function; - - /** - * Object's method that will be executed on this action. - */ - method: string; - - /** - * Action type. - */ - type: ActionType; -} diff --git a/src/metadata/args/MiddlewareMetadataArgs.ts b/src/metadata/args/MiddlewareMetadataArgs.ts deleted file mode 100644 index 09709204..00000000 --- a/src/metadata/args/MiddlewareMetadataArgs.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface MiddlewareMetadataArgs { - /** - * Indicates object which is used by this controller. - */ - target: Function; - - /** - * Middleware priority. - */ - priority?: number; - - /** - * Limits usage of the middleware to the given namespaces - */ - namespace?: string | RegExp | Array; -} diff --git a/src/metadata/args/ParamMetadataArgs.ts b/src/metadata/args/ParamMetadataArgs.ts deleted file mode 100644 index f6329dcf..00000000 --- a/src/metadata/args/ParamMetadataArgs.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ParamTypes } from '../types/ParamTypes'; -import { ClassTransformOptions } from 'class-transformer'; - -/** - * Controller metadata used to storage information about registered parameters. - */ -export interface ParamMetadataArgs { - /** - * Parameter target. - */ - target: any; - - /** - * Method on which's parameter is attached. - */ - method: string; - - /** - * Index (# number) of the parameter in the method signature. - */ - index: number; - - /** - * Parameter type. - */ - type: ParamTypes; - - /** - * Reflected type of the parameter. - */ - reflectedType: any; - - /** - * Transforms the value. - */ - transform?: (value: any, socket: any) => Promise | any; - - /** - * Class transform options used to perform plainToClass operation. - */ - classTransformOptions?: ClassTransformOptions; - - /** - * Extra parameter value. - */ - value?: any; -} diff --git a/src/metadata/args/ResultMetadataArgs.ts b/src/metadata/args/ResultMetadataArgs.ts deleted file mode 100644 index 40e23f2d..00000000 --- a/src/metadata/args/ResultMetadataArgs.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ResultType } from '../types/ResultTypes'; -import { ClassTransformOptions } from 'class-transformer'; - -/** - */ -export interface ResultMetadataArgs { - /** - */ - value?: string; - - /** - */ - target: Function; - - /** - */ - method: string; - - /** - * Result handler type. - */ - type: ResultType; - - classTransformOptions?: ClassTransformOptions; -} diff --git a/src/metadata/args/SocketControllerMetadataArgs.ts b/src/metadata/args/SocketControllerMetadataArgs.ts deleted file mode 100644 index a87e247e..00000000 --- a/src/metadata/args/SocketControllerMetadataArgs.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Controller metadata used to storage information about registered controller. - */ -export interface SocketControllerMetadataArgs { - /** - * Indicates object which is used by this controller. - */ - target: Function; - - /** - * Extra namespace in which this controller's events will be registered. - */ - namespace?: string | RegExp; -} diff --git a/src/metadata/types/ActionTypes.ts b/src/metadata/types/ActionTypes.ts deleted file mode 100644 index 35bb26c0..00000000 --- a/src/metadata/types/ActionTypes.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Controller action type. - */ -export type ActionType = 'message' | 'connection' | 'disconnection'; - -/** - * Static access to action types. - */ -export class ActionTypes { - static MESSAGE: ActionType = 'message'; - static CONNECT: ActionType = 'connection'; - static DISCONNECT: ActionType = 'disconnection'; -} diff --git a/src/metadata/types/ParamTypes.ts b/src/metadata/types/ParamTypes.ts deleted file mode 100644 index 2f57106f..00000000 --- a/src/metadata/types/ParamTypes.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Controller action's parameter type. - */ -export type ParamType = - | 'custom' - | 'connected-socket' - | 'socket-body' - | 'socket-query-param' - | 'socket-io' - | 'socket-id' - | 'socket-request' - | 'socket-rooms' - | 'namespace-params' - | 'namespace-param'; - -/** - * Controller action's parameter type. - */ -export class ParamTypes { - static CUSTOM: ParamType = 'custom'; - static CONNECTED_SOCKET: ParamType = 'connected-socket'; - static SOCKET_BODY: ParamType = 'socket-body'; - static SOCKET_QUERY_PARAM: ParamType = 'socket-query-param'; - static SOCKET_IO: ParamType = 'socket-io'; - static SOCKET_ID: ParamType = 'socket-id'; - static SOCKET_REQUEST: ParamType = 'socket-request'; - static SOCKET_ROOMS: ParamType = 'socket-rooms'; - static NAMESPACE_PARAMS: ParamType = 'namespace-params'; - static NAMESPACE_PARAM: ParamType = 'namespace-param'; -} diff --git a/src/metadata/types/ResultTypes.ts b/src/metadata/types/ResultTypes.ts deleted file mode 100644 index 15bdb4d0..00000000 --- a/src/metadata/types/ResultTypes.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Action result handler type. - */ -export type ResultType = 'emit-on-success' | 'emit-on-fail' | 'skip-emit-on-empty-result'; - -/** - * Static access to result handler types. - */ -export class ResultTypes { - static EMIT_ON_SUCCESS: ResultType = 'emit-on-success'; - static EMIT_ON_FAIL: ResultType = 'emit-on-fail'; - static SKIP_EMIT_ON_EMPTY_RESULT: ResultType = 'skip-emit-on-empty-result'; -} diff --git a/src/types/ActionMetadata.ts b/src/types/ActionMetadata.ts new file mode 100644 index 00000000..6ff75a7e --- /dev/null +++ b/src/types/ActionMetadata.ts @@ -0,0 +1,11 @@ +import { ParameterMetadata } from './ParameterMetadata'; +import { ResultMetadata } from './ResultMetadata'; +import { ActionType } from './enums/ActionType'; + +export interface ActionMetadata { + type: ActionType; + methodName: string; + options: any; + parameters: ParameterMetadata[]; + results: ResultMetadata[]; +} diff --git a/src/types/ActionTransformOptions.ts b/src/types/ActionTransformOptions.ts new file mode 100644 index 00000000..f113156c --- /dev/null +++ b/src/types/ActionTransformOptions.ts @@ -0,0 +1,6 @@ +import { ClassTransformOptions } from 'class-transformer'; + +export interface ActionTransformOptions { + transform?: boolean; + transformOptions?: ClassTransformOptions; +} diff --git a/src/types/ControllerMetadata.ts b/src/types/ControllerMetadata.ts new file mode 100644 index 00000000..32d6d2d1 --- /dev/null +++ b/src/types/ControllerMetadata.ts @@ -0,0 +1,10 @@ +import { HandlerType } from './enums/HandlerType'; +import { ActionMetadata } from './ActionMetadata'; + +export interface ControllerMetadata { + namespace?: string | RegExp; + type: HandlerType.CONTROLLER; + actions: { + [methodName: string]: ActionMetadata; + }; +} diff --git a/src/types/HandlerMetadata.ts b/src/types/HandlerMetadata.ts new file mode 100644 index 00000000..a53e3f64 --- /dev/null +++ b/src/types/HandlerMetadata.ts @@ -0,0 +1,4 @@ +export interface HandlerMetadata { + instance: T; + metadata: U; +} diff --git a/src/types/MiddlewareInterface.ts b/src/types/MiddlewareInterface.ts new file mode 100644 index 00000000..267d6c40 --- /dev/null +++ b/src/types/MiddlewareInterface.ts @@ -0,0 +1,5 @@ +import { Socket } from 'socket.io'; + +export interface MiddlewareInterface { + use(socket: Socket, next: (err?: any) => any): any; +} diff --git a/src/types/MiddlewareMetadata.ts b/src/types/MiddlewareMetadata.ts new file mode 100644 index 00000000..cdc27d8d --- /dev/null +++ b/src/types/MiddlewareMetadata.ts @@ -0,0 +1,7 @@ +import { HandlerType } from './enums/HandlerType'; + +export interface MiddlewareMetadata { + namespace?: string | RegExp | Array; + priority?: number; + type: HandlerType.MIDDLEWARE; +} diff --git a/src/types/ParameterMetadata.ts b/src/types/ParameterMetadata.ts new file mode 100644 index 00000000..5df34d1b --- /dev/null +++ b/src/types/ParameterMetadata.ts @@ -0,0 +1,9 @@ +import { ParameterType } from './enums/ParameterType'; +import { ActionTransformOptions } from './ActionTransformOptions'; + +export interface ParameterMetadata { + type: ParameterType; + index: number; + reflectedType: any; + options: ActionTransformOptions & Record; +} diff --git a/src/types/ResultMetadata.ts b/src/types/ResultMetadata.ts new file mode 100644 index 00000000..d03b85f1 --- /dev/null +++ b/src/types/ResultMetadata.ts @@ -0,0 +1,7 @@ +import { ResultType } from './enums/ResultType'; +import { ActionTransformOptions } from './ActionTransformOptions'; + +export interface ResultMetadata { + type: ResultType; + options: ActionTransformOptions & Record; +} diff --git a/src/types/SocketControllerMetaKey.ts b/src/types/SocketControllerMetaKey.ts new file mode 100644 index 00000000..4350af57 --- /dev/null +++ b/src/types/SocketControllerMetaKey.ts @@ -0,0 +1 @@ +export const SOCKET_CONTROLLER_META_KEY = Symbol('SocketControllerMetaKey'); diff --git a/src/types/SocketControllersOptions.ts b/src/types/SocketControllersOptions.ts new file mode 100644 index 00000000..8a2d5f6c --- /dev/null +++ b/src/types/SocketControllersOptions.ts @@ -0,0 +1,18 @@ +import { Server } from 'socket.io'; +import { TransformOptions } from './TransformOptions'; + +export interface SocketControllersOptions { + container: { get(someClass: { new (...args: any[]): T } | Function): T }; + + io?: Server; + + port?: number; + + ioHttpServer?: any; + + controllers?: Function[] | string[]; + + middlewares?: Function[] | string[]; + + transformOption?: Partial; +} diff --git a/src/types/TransformOptions.ts b/src/types/TransformOptions.ts new file mode 100644 index 00000000..d7aae830 --- /dev/null +++ b/src/types/TransformOptions.ts @@ -0,0 +1,7 @@ +import { ClassTransformOptions } from 'class-transformer'; + +export interface TransformOptions { + transform?: boolean; + parameterTransformOptions?: ClassTransformOptions; + resultTransformOptions?: ClassTransformOptions; +} diff --git a/src/types/constants/defaultTransformOptions.ts b/src/types/constants/defaultTransformOptions.ts new file mode 100644 index 00000000..434a0728 --- /dev/null +++ b/src/types/constants/defaultTransformOptions.ts @@ -0,0 +1,5 @@ +import { TransformOptions } from '../TransformOptions'; + +export const defaultTransformOptions: TransformOptions = { + transform: true, +}; diff --git a/src/types/enums/ActionType.ts b/src/types/enums/ActionType.ts new file mode 100644 index 00000000..c9e37bb5 --- /dev/null +++ b/src/types/enums/ActionType.ts @@ -0,0 +1,5 @@ +export enum ActionType { + MESSAGE, + CONNECT, + DISCONNECT, +} diff --git a/src/types/enums/HandlerType.ts b/src/types/enums/HandlerType.ts new file mode 100644 index 00000000..4efdc26c --- /dev/null +++ b/src/types/enums/HandlerType.ts @@ -0,0 +1,4 @@ +export enum HandlerType { + CONTROLLER, + MIDDLEWARE, +} diff --git a/src/types/enums/ParameterType.ts b/src/types/enums/ParameterType.ts new file mode 100644 index 00000000..79981d8c --- /dev/null +++ b/src/types/enums/ParameterType.ts @@ -0,0 +1,12 @@ +export enum ParameterType { + CUSTOM, + CONNECTED_SOCKET, + MESSAGE_BODY, + SOCKET_QUERY_PARAM, + SOCKET_IO, + SOCKET_ID, + SOCKET_REQUEST, + SOCKET_ROOMS, + NAMESPACE_PARAMS, + NAMESPACE_PARAM, +} diff --git a/src/types/enums/ResultType.ts b/src/types/enums/ResultType.ts new file mode 100644 index 00000000..668aed55 --- /dev/null +++ b/src/types/enums/ResultType.ts @@ -0,0 +1,5 @@ +export enum ResultType { + EMIT_ON_SUCCESS, + EMIT_ON_FAIL, + SKIP_EMIT_ON_EMPTY_RESULT, +} diff --git a/src/util/DirectoryExportedClassesLoader.ts b/src/util/DirectoryExportedClassesLoader.ts deleted file mode 100644 index dde53ce9..00000000 --- a/src/util/DirectoryExportedClassesLoader.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as path from 'path'; -import * as glob from 'glob'; - -/** - * Loads all exported classes from the given directory. - */ -export function importClassesFromDirectories(directories: string[], formats = ['.js', '.ts']): Function[] { - const loadFileClasses = function (exported: Function | Record | Function[], allLoaded: Function[]) { - if (exported instanceof Function) { - allLoaded.push(exported); - } else if (typeof exported === 'object' && !Array.isArray(exported)) { - Object.keys(exported).forEach(key => loadFileClasses(exported[key], allLoaded)); - } else if (Array.isArray(exported)) { - exported.forEach((i: Function | Record | Function[]) => loadFileClasses(i, allLoaded)); - } - - return allLoaded; - }; - - const allFiles = directories.reduce((allDirs, dir) => { - return allDirs.concat(glob.sync(path.normalize(dir))); - }, [] as string[]); - - const dirs = allFiles - .filter(file => { - const dtsExtension = file.substring(file.length - 5, file.length); - return formats.indexOf(path.extname(file)) !== -1 && dtsExtension !== '.d.ts'; - }) - .map(file => { - return require(file); - }); - - return loadFileClasses(dirs, []); -} diff --git a/src/util/add-action-to-controller-metadata.ts b/src/util/add-action-to-controller-metadata.ts new file mode 100644 index 00000000..70f9f330 --- /dev/null +++ b/src/util/add-action-to-controller-metadata.ts @@ -0,0 +1,25 @@ +import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; +import { ActionMetadata } from '../types/ActionMetadata'; +import { getMetadata } from './get-metadata'; +import { ControllerMetadata } from '../types/ControllerMetadata'; + +export const addActionToControllerMetadata = ( + target: Function, + actionMetadata: Pick +) => { + const existingMetadata = getMetadata(target); + (Reflect as any).defineMetadata( + SOCKET_CONTROLLER_META_KEY, + { + ...existingMetadata, + actions: { + ...existingMetadata?.actions, + [actionMetadata.methodName]: { + ...existingMetadata?.actions?.[actionMetadata.methodName], + ...actionMetadata, + }, + }, + }, + target + ); +}; diff --git a/src/util/add-controller-metadata.ts b/src/util/add-controller-metadata.ts new file mode 100644 index 00000000..a14b24ad --- /dev/null +++ b/src/util/add-controller-metadata.ts @@ -0,0 +1,15 @@ +import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; +import { ControllerMetadata } from '../types/ControllerMetadata'; +import { getMetadata } from './get-metadata'; + +export const addControllerMetadata = (target: Function, metadata: Pick) => { + const existingMetadata = getMetadata(target); + (Reflect as any).defineMetadata( + SOCKET_CONTROLLER_META_KEY, + { + ...existingMetadata, + ...metadata, + }, + target + ); +}; diff --git a/src/util/add-middleware-metadata.ts b/src/util/add-middleware-metadata.ts new file mode 100644 index 00000000..eb508579 --- /dev/null +++ b/src/util/add-middleware-metadata.ts @@ -0,0 +1,6 @@ +import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; +import { MiddlewareMetadata } from '../types/MiddlewareMetadata'; + +export const addMiddlewareMetadata = (target: Function, metadata: MiddlewareMetadata) => { + (Reflect as any).defineMetadata(SOCKET_CONTROLLER_META_KEY, metadata, target); +}; diff --git a/src/util/add-parameter-to-action-metadata.ts b/src/util/add-parameter-to-action-metadata.ts new file mode 100644 index 00000000..c2f7096f --- /dev/null +++ b/src/util/add-parameter-to-action-metadata.ts @@ -0,0 +1,22 @@ +import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; +import { ParameterMetadata } from '../types/ParameterMetadata'; +import { getMetadata } from './get-metadata'; +import { ControllerMetadata } from '../types/ControllerMetadata'; + +export const addParameterToActionMetadata = (target: Function, methodName: string, args: ParameterMetadata) => { + const existingMetadata = getMetadata(target); + (Reflect as any).defineMetadata( + SOCKET_CONTROLLER_META_KEY, + { + ...existingMetadata, + actions: { + ...existingMetadata?.actions, + [methodName]: { + ...existingMetadata?.actions?.[methodName], + parameters: [args, ...(existingMetadata?.actions?.[methodName]?.parameters || [])], + }, + }, + }, + target + ); +}; diff --git a/src/util/add-result-to-action-metadata.ts b/src/util/add-result-to-action-metadata.ts new file mode 100644 index 00000000..703c5b94 --- /dev/null +++ b/src/util/add-result-to-action-metadata.ts @@ -0,0 +1,22 @@ +import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; +import { ResultMetadata } from '../types/ResultMetadata'; +import { getMetadata } from './get-metadata'; +import { ControllerMetadata } from '../types/ControllerMetadata'; + +export const addResultToActionMetadata = (target: Function, methodName: string, args: ResultMetadata) => { + const existingMetadata = getMetadata(target); + (Reflect as any).defineMetadata( + SOCKET_CONTROLLER_META_KEY, + { + ...existingMetadata, + actions: { + ...existingMetadata?.actions, + [methodName]: { + ...existingMetadata?.actions?.[methodName], + results: [args, ...(existingMetadata?.actions?.[methodName]?.results || [])], + }, + }, + }, + target + ); +}; diff --git a/src/util/get-metadata.ts b/src/util/get-metadata.ts new file mode 100644 index 00000000..c1a85537 --- /dev/null +++ b/src/util/get-metadata.ts @@ -0,0 +1,5 @@ +import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; + +export const getMetadata = (target: T): U => { + return (Reflect as any).getMetadata(SOCKET_CONTROLLER_META_KEY, target); +}; diff --git a/test/functional/connected-socket.spec.ts b/test/functional/connected-socket.spec.ts new file mode 100644 index 00000000..d48e3ab8 --- /dev/null +++ b/test/functional/connected-socket.spec.ts @@ -0,0 +1,70 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; + +describe('ConnectedSocket', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Connected socket is retrieved correctly', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket) { + testResult = socket.id; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect(wsClient.id).toEqual(testResult); + }); +}); diff --git a/test/functional/controllers/test.controller.ts b/test/functional/controllers/test.controller.ts new file mode 100644 index 00000000..151d78ae --- /dev/null +++ b/test/functional/controllers/test.controller.ts @@ -0,0 +1,11 @@ +import { ConnectedSocket, OnConnect, SocketController } from '../../../src'; +import { Service } from 'typedi'; + +@SocketController() +@Service() +export class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: any) { + socket.emit('connected'); + } +} diff --git a/test/functional/controllers/test2.controller.ts b/test/functional/controllers/test2.controller.ts new file mode 100644 index 00000000..30afde00 --- /dev/null +++ b/test/functional/controllers/test2.controller.ts @@ -0,0 +1,11 @@ +import { ConnectedSocket, OnMessage, SocketController } from '../../../src'; +import { Service } from 'typedi'; + +@SocketController() +@Service() +export class Test2Controller { + @OnMessage('test') + connected(@ConnectedSocket() socket: any) { + socket.emit('response'); + } +} diff --git a/test/unit/createSocketServer.spec.ts b/test/functional/create-socket-server.spec.ts similarity index 63% rename from test/unit/createSocketServer.spec.ts rename to test/functional/create-socket-server.spec.ts index f44a28c1..dec915be 100644 --- a/test/unit/createSocketServer.spec.ts +++ b/test/functional/create-socket-server.spec.ts @@ -1,15 +1,16 @@ -import { createSocketServer, SocketController } from '../../src'; +import { SocketController, SocketControllers } from '../../src'; import { testConnection } from '../utilities/testSocketConnection'; +import { Container, Service } from 'typedi'; -describe('createSocketServer', () => { - let wsApp: any; +describe('Create socket server', () => { + let wsApp: SocketControllers; const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; afterEach(() => { return new Promise(resolve => { if (wsApp) - return wsApp.close(() => { + return wsApp.io.close(() => { resolve(null); }); resolve(null); @@ -18,13 +19,13 @@ describe('createSocketServer', () => { it('should create socket server without options', async () => { expect.assertions(1); - wsApp = await createSocketServer(PORT); + wsApp = new SocketControllers({ port: PORT, container: Container }); expect(await testConnection(PATH_FOR_CLIENT)).toEqual(0); }); it('should create socket server with empty controllers array in options', async () => { expect.assertions(1); - wsApp = await createSocketServer(PORT, { controllers: [] }); + wsApp = new SocketControllers({ port: PORT, controllers: [], container: Container }); expect(await testConnection(PATH_FOR_CLIENT)).toEqual(0); }); @@ -32,9 +33,10 @@ describe('createSocketServer', () => { expect.assertions(1); @SocketController() + @Service() class TestController {} - wsApp = await createSocketServer(PORT, { controllers: [TestController] }); + wsApp = new SocketControllers({ port: PORT, container: Container, controllers: [TestController] }); expect(await testConnection(PATH_FOR_CLIENT)).toEqual(0); }); }); diff --git a/test/functional/emit-on-fail.spec.ts b/test/functional/emit-on-fail.spec.ts new file mode 100644 index 00000000..a4752fad --- /dev/null +++ b/test/functional/emit-on-fail.spec.ts @@ -0,0 +1,127 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { EmitOnFail, OnMessage } from '../../src'; + +describe('EmitOnFail', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Emit defined event on failing sync execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + @EmitOnFail('fail') + connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnFail('fail') + testEvent() { + throw new Error('error string'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const errors = []; + + wsClient.on('fail', data => { + errors.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(errors.length).toEqual(0); + + wsClient.emit('request'); + await waitForEvent(wsClient, 'fail'); + expect(errors[0]).toEqual('error string'); + expect(errors.length).toEqual(1); + }); + + it('Emit defined event on failing async execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + @EmitOnFail('fail') + connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnFail('fail') + async testEvent() { + throw new Error('error string'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const errors = []; + + wsClient.on('fail', data => { + errors.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(errors.length).toEqual(0); + + wsClient.emit('request'); + await waitForEvent(wsClient, 'fail'); + expect(errors[0]).toEqual('error string'); + expect(errors.length).toEqual(1); + }); +}); diff --git a/test/functional/emit-on-success.spec.ts b/test/functional/emit-on-success.spec.ts new file mode 100644 index 00000000..d9aab241 --- /dev/null +++ b/test/functional/emit-on-success.spec.ts @@ -0,0 +1,139 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { EmitOnSuccess, OnMessage } from '../../src'; + +describe('EmitOnSuccess', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Emit defined event on successful sync execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnSuccess('response') + testEvent() { + throw new Error('error string'); + } + + @OnMessage('request2') + @EmitOnSuccess('response') + testEvent2() { + return 'data'; + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const responses = []; + + wsClient.on('response', data => { + responses.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(responses.length).toEqual(0); + + wsClient.emit('request'); + wsClient.emit('request2'); + await waitForEvent(wsClient, 'response'); + expect(responses[0]).toEqual('data'); + expect(responses.length).toEqual(1); + }); + + it('Emit defined event on successful async execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnSuccess('response') + async testEvent() { + throw new Error('error string'); + } + + @OnMessage('request2') + @EmitOnSuccess('response') + async testEvent2() { + return 'data'; + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const responses = []; + + wsClient.on('response', data => { + responses.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(responses.length).toEqual(0); + + wsClient.emit('request'); + wsClient.emit('request2'); + await waitForEvent(wsClient, 'response'); + expect(responses[0]).toEqual('data'); + expect(responses.length).toEqual(1); + }); +}); diff --git a/test/functional/load-controllers-from-directory.spec.ts b/test/functional/load-controllers-from-directory.spec.ts new file mode 100644 index 00000000..40a3fd11 --- /dev/null +++ b/test/functional/load-controllers-from-directory.spec.ts @@ -0,0 +1,60 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container } from 'typedi'; +import { waitForEvent } from '../utilities/waitForEvent'; +import path from 'path'; + +describe('Load controllers from directory', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Load controllers from directory', async () => { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [path.join(__dirname, './controllers/**.*')], + }); + wsClient = io(PATH_FOR_CLIENT, { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + wsClient.emit('test'); + await waitForEvent(wsClient, 'response'); + expect(true).toEqual(true); + }); +}); diff --git a/test/functional/middlewares.spec.ts b/test/functional/middlewares.spec.ts index 6ddeb0b8..37093cca 100644 --- a/test/functional/middlewares.spec.ts +++ b/test/functional/middlewares.spec.ts @@ -1,18 +1,14 @@ import { Server } from 'socket.io'; import { Socket, io } from 'socket.io-client'; -import { - ConnectedSocket, - defaultMetadataArgsStorage, - Middleware, - MiddlewareInterface, - OnConnect, - SocketController, - useContainer, - useSocketServer, -} from '../../src'; import { Container, Service } from 'typedi'; import { waitForEvent } from '../utilities/waitForEvent'; import { createServer, Server as HttpServer } from 'http'; +import { Middleware } from '../../src/decorators/Middleware'; +import { MiddlewareInterface } from '../../src/types/MiddlewareInterface'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { SocketControllers } from '../../src/SocketControllers'; describe('Middlewares', () => { const PORT = 8080; @@ -22,6 +18,7 @@ describe('Middlewares', () => { let wsApp: Server; let wsClient: Socket; let testResult; + let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); @@ -33,7 +30,6 @@ describe('Middlewares', () => { httpServer.listen(PORT, () => { done(); }); - useContainer(Container); }); afterEach(() => { @@ -42,7 +38,7 @@ describe('Middlewares', () => { Container.reset(); wsClient.close(); wsClient = null; - defaultMetadataArgsStorage().reset(); + socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { @@ -71,7 +67,9 @@ describe('Middlewares', () => { } } - useSocketServer(wsApp, { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, middlewares: [GlobalMiddleware], controllers: [Controller], }); @@ -101,7 +99,9 @@ describe('Middlewares', () => { } } - useSocketServer(wsApp, { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, middlewares: [StringNamespaceMiddleware], controllers: [StringNamespaceController], }); @@ -130,7 +130,9 @@ describe('Middlewares', () => { } } - useSocketServer(wsApp, { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, middlewares: [StringNamespaceMiddleware], controllers: [String2NamespaceController], }); @@ -161,7 +163,9 @@ describe('Middlewares', () => { } } - useSocketServer(wsApp, { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, middlewares: [RegexpNamespaceMiddleware], controllers: [RegexpNamespaceController], }); @@ -190,7 +194,9 @@ describe('Middlewares', () => { } } - useSocketServer(wsApp, { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, middlewares: [RegexpNamespaceMiddleware], controllers: [RegexpNamespaceController], }); @@ -221,7 +227,9 @@ describe('Middlewares', () => { } } - useSocketServer(wsApp, { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, middlewares: [RegexpArrayNamespaceMiddleware], controllers: [RegexpNamespaceController], }); @@ -250,7 +258,9 @@ describe('Middlewares', () => { } } - useSocketServer(wsApp, { + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, middlewares: [RegexpArrayNamespaceMiddleware], controllers: [RegexpNamespaceController], }); diff --git a/test/functional/nsp-param.spec.ts b/test/functional/nsp-param.spec.ts new file mode 100644 index 00000000..b5f826ed --- /dev/null +++ b/test/functional/nsp-param.spec.ts @@ -0,0 +1,71 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { NspParam } from '../../src'; + +describe('NspParam', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Namespace param is retrieved correctly', async () => { + @SocketController('/:first/:second') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @NspParam('second') parameter: string) { + testResult = parameter; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/test1/test2', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect(testResult).toEqual('test2'); + }); +}); diff --git a/test/functional/nsp-params.spec.ts b/test/functional/nsp-params.spec.ts new file mode 100644 index 00000000..701d2fa2 --- /dev/null +++ b/test/functional/nsp-params.spec.ts @@ -0,0 +1,71 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { NspParams } from '../../src'; + +describe('NspParams', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Namespace params are retrieved correctly', async () => { + @SocketController('/:first/:second') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @NspParams() parameters: { first: string; second: string }) { + testResult = parameters; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/test1/test2', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect(testResult).toEqual({ first: 'test1', second: 'test2' }); + }); +}); diff --git a/test/functional/on-disconnect.spec.ts b/test/functional/on-disconnect.spec.ts new file mode 100644 index 00000000..1516d142 --- /dev/null +++ b/test/functional/on-disconnect.spec.ts @@ -0,0 +1,78 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { OnDisconnect } from '../../src'; +import { waitForTime } from '../utilities/waitForTime'; + +describe('OnDisconnect', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('OnDisconnect is called correctly', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnDisconnect() + disconnected() { + testResult = 'disconnected'; + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + wsClient.disconnect(); + await waitForTime(1000); + expect(testResult).toEqual('disconnected'); + }); +}); diff --git a/test/functional/parameter-transformation.spec.ts b/test/functional/parameter-transformation.spec.ts new file mode 100644 index 00000000..02d5c321 --- /dev/null +++ b/test/functional/parameter-transformation.spec.ts @@ -0,0 +1,96 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { MessageBody, OnMessage } from '../../src'; +import { Expose } from 'class-transformer'; + +describe('Parameter transformation', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Parameters are converted correctly with the given options', async () => { + class Body { + @Expose() prop1: string; + @Expose() prop2: number; + } + + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket) { + testResult = socket.id; + socket.emit('connected'); + } + + @OnMessage('test') + test( + @ConnectedSocket() socket: Socket, + @MessageBody({ + transform: true, + transformOptions: { excludeExtraneousValues: true, enableImplicitConversion: true }, + }) + body: Body + ) { + testResult = body; + socket.emit('result'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + + wsClient.emit('test', { prop1: 'test', prop2: '2', prop3: 10 }); + await waitForEvent(wsClient, 'result'); + expect(testResult).toBeInstanceOf(Body); + expect(testResult.prop1).toEqual('test'); + expect(testResult.prop2).toEqual(2); + expect(testResult.prop3).toEqual(undefined); + }); +}); diff --git a/test/functional/skip-emit-on-empty-result.spec.ts b/test/functional/skip-emit-on-empty-result.spec.ts new file mode 100644 index 00000000..ee268888 --- /dev/null +++ b/test/functional/skip-emit-on-empty-result.spec.ts @@ -0,0 +1,241 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { EmitOnFail, EmitOnSuccess, OnMessage, SkipEmitOnEmptyResult } from '../../src'; + +describe('SkipEmitOnEmptyResult', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Skip emit of defined event on successful sync execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnSuccess('response') + @SkipEmitOnEmptyResult() + testEvent() { + return { data: true }; + } + + @OnMessage('request2') + @EmitOnSuccess('response') + @SkipEmitOnEmptyResult() + testEvent2() { + return null; + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const responses = []; + + wsClient.on('response', data => { + responses.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(responses.length).toEqual(0); + + wsClient.emit('request2'); + wsClient.emit('request'); + + await waitForEvent(wsClient, 'response'); + expect(responses.length).toEqual(1); + expect(responses[0]).toEqual({ data: true }); + }); + + it('Skip emit of defined event on successful async execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + async connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnSuccess('response') + @SkipEmitOnEmptyResult() + async testEvent() { + return { data: true }; + } + + @OnMessage('request2') + @EmitOnSuccess('response') + @SkipEmitOnEmptyResult() + async testEvent2() { + return null; + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const responses = []; + + wsClient.on('response', data => { + responses.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(responses.length).toEqual(0); + + wsClient.emit('request2'); + wsClient.emit('request'); + + await waitForEvent(wsClient, 'response'); + expect(responses.length).toEqual(1); + expect(responses[0]).toEqual({ data: true }); + }); + + it('Skip emit of defined event on failing sync execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnFail('response') + @SkipEmitOnEmptyResult() + testEvent() { + throw new Error('error string'); + } + + @OnMessage('request2') + @EmitOnFail('response') + @SkipEmitOnEmptyResult() + testEvent2() { + return Promise.reject(null); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const responses = []; + + wsClient.on('response', data => { + responses.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(responses.length).toEqual(0); + + wsClient.emit('request2'); + wsClient.emit('request'); + + await waitForEvent(wsClient, 'response'); + expect(responses.length).toEqual(1); + expect(responses[0]).toEqual('error string'); + }); + + it('Skip emit of defined event on failing async execution', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + async connected(@ConnectedSocket() socket: Socket) { + socket.emit('connected'); + } + + @OnMessage('request') + @EmitOnFail('response') + @SkipEmitOnEmptyResult() + async testEvent() { + throw new Error('error string'); + } + + @OnMessage('request2') + @EmitOnFail('response2') + @SkipEmitOnEmptyResult() + async testEvent2(@ConnectedSocket() socket: Socket) { + return Promise.reject(null); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + const responses = []; + + wsClient.on('response', data => { + responses.push(data); + }); + + await waitForEvent(wsClient, 'connected'); + expect(responses.length).toEqual(0); + + wsClient.emit('request2'); + wsClient.emit('request'); + + await waitForEvent(wsClient, 'response'); + expect(responses.length).toEqual(1); + expect(responses[0]).toEqual('error string'); + }); +}); diff --git a/test/functional/socket-id.spec.ts b/test/functional/socket-id.spec.ts new file mode 100644 index 00000000..808955f7 --- /dev/null +++ b/test/functional/socket-id.spec.ts @@ -0,0 +1,71 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { SocketId } from '../../src'; + +describe('SocketId', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Connected socket id is retrieved correctly', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @SocketId() socketId: string) { + testResult = socketId; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect(wsClient.id).toEqual(testResult); + }); +}); diff --git a/test/functional/socket-io.spec.ts b/test/functional/socket-io.spec.ts new file mode 100644 index 00000000..fa61696d --- /dev/null +++ b/test/functional/socket-io.spec.ts @@ -0,0 +1,71 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { SocketIO } from '../../src'; + +describe('SocketIo', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Socket.io instance is retrieved correctly', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @SocketIO() socketIO: Server) { + testResult = socketIO; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect(socketControllers.io).toEqual(testResult); + }); +}); diff --git a/test/functional/socket-message-body.spec.ts b/test/functional/socket-message-body.spec.ts new file mode 100644 index 00000000..b51c0e7a --- /dev/null +++ b/test/functional/socket-message-body.spec.ts @@ -0,0 +1,81 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { MessageBody, OnMessage, SocketId } from '../../src'; + +describe('MessageBody', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Event body is retrieved correctly', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @SocketId() socketId: string) { + testResult = socketId; + socket.emit('connected'); + } + + @OnMessage('test') + test(@MessageBody() data: any, @ConnectedSocket() socket: Socket) { + testResult = data; + socket.emit('return'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + + wsClient.emit('test', 'test data'); + + await waitForEvent(wsClient, 'return'); + expect(testResult).toEqual('test data'); + }); +}); diff --git a/test/functional/socket-query-param.spec.ts b/test/functional/socket-query-param.spec.ts new file mode 100644 index 00000000..dafb01d3 --- /dev/null +++ b/test/functional/socket-query-param.spec.ts @@ -0,0 +1,67 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { ConnectedSocket, OnConnect, SocketController, SocketControllers, SocketQueryParam } from '../../src'; +import { Container, Service } from 'typedi'; +import { waitForEvent } from '../utilities/waitForEvent'; + +describe('SocketQueryParam', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Socket query param is retrieved correctly', async () => { + @SocketController() + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @SocketQueryParam('testParam') parameter: string) { + testResult = parameter; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '?testParam=testValue', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect(testResult).toEqual('testValue'); + }); +}); diff --git a/test/functional/socket-request.spec.ts b/test/functional/socket-request.spec.ts new file mode 100644 index 00000000..3c3588dd --- /dev/null +++ b/test/functional/socket-request.spec.ts @@ -0,0 +1,74 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { + ConnectedSocket, + OnConnect, + SocketController, + SocketControllers, + SocketQueryParam, + SocketRequest, +} from '../../src'; +import { Container, Service } from 'typedi'; +import { waitForEvent } from '../utilities/waitForEvent'; + +describe('SocketRequest', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Socket request is retrieved correctly', async () => { + @SocketController() + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @SocketRequest() request: any) { + testResult = request; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '?testParam=testValue', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect(testResult.url).toContain('/socket.io/?testParam=testValue&EIO='); + }); +}); diff --git a/test/functional/socket-rooms.spec.ts b/test/functional/socket-rooms.spec.ts new file mode 100644 index 00000000..e2a07b5b --- /dev/null +++ b/test/functional/socket-rooms.spec.ts @@ -0,0 +1,71 @@ +import { createServer, Server as HttpServer } from 'http'; +import { Server } from 'socket.io'; +import { io, Socket } from 'socket.io-client'; +import { SocketControllers } from '../../src/SocketControllers'; +import { Container, Service } from 'typedi'; +import { SocketController } from '../../src/decorators/SocketController'; +import { OnConnect } from '../../src/decorators/OnConnect'; +import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; +import { waitForEvent } from '../utilities/waitForEvent'; +import { SocketRooms } from '../../src'; + +describe('SocketRooms', () => { + const PORT = 8080; + const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; + + let httpServer: HttpServer; + let wsApp: Server; + let wsClient: Socket; + let testResult; + let socketControllers: SocketControllers; + + beforeEach(done => { + httpServer = createServer(); + wsApp = new Server(httpServer, { + cors: { + origin: '*', + }, + }); + httpServer.listen(PORT, () => { + done(); + }); + }); + + afterEach(() => { + testResult = undefined; + + Container.reset(); + wsClient.close(); + wsClient = null; + socketControllers = null; + return new Promise(resolve => { + if (wsApp) + return wsApp.close(() => { + resolve(null); + }); + resolve(null); + }); + }); + + it('Socket rooms set is retrieved correctly', async () => { + @SocketController('/string') + @Service() + class TestController { + @OnConnect() + connected(@ConnectedSocket() socket: Socket, @SocketRooms() rooms: any) { + testResult = rooms; + socket.emit('connected'); + } + } + + socketControllers = new SocketControllers({ + io: wsApp, + container: Container, + controllers: [TestController], + }); + wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); + + await waitForEvent(wsClient, 'connected'); + expect([...(testResult as Set).keys()][0]).toEqual(wsClient.id); + }); +}); diff --git a/test/utilities/waitForTime.ts b/test/utilities/waitForTime.ts new file mode 100644 index 00000000..88765947 --- /dev/null +++ b/test/utilities/waitForTime.ts @@ -0,0 +1,7 @@ +export const waitForTime = (time: number): Promise => { + return new Promise(resolve => { + setTimeout(() => { + resolve(null); + }, time); + }); +};