├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── image1.png ├── image2.png ├── image3.png ├── package-lock.json ├── package.json ├── src └── index.ts ├── test ├── moleculer.service.spec.ts ├── services │ ├── sample1.service.ts │ └── sample1.service.types.ts └── tsconfig.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | jest: true 6 | }, 7 | extends: ['airbnb-base', 'prettier'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly' 11 | }, 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | sourceType: 'module' 16 | }, 17 | plugins: ['@typescript-eslint', 'prettier'], 18 | rules: { 19 | 'prettier/prettier': ['error'], 20 | '@typescript-eslint/no-unused-vars': 'error', 21 | 'import/prefer-default-export': 'off' 22 | }, 23 | settings: { 24 | 'import/resolver': { 25 | node: { 26 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 27 | moduleDirectory: ['node_modules', 'src/'] 28 | } 29 | } 30 | } 31 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Test coverage output 107 | test/coverage 108 | 109 | # OS filesystem uff 110 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 70 6 | } 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ByteTechnology 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moleculer-service-ts 2 | 3 | Typescript support for moleculer service actions and events 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install moleculer-service-ts 9 | ``` 10 | 11 | `moleculer` is a peer dependency, so it will need to be installed separately. 12 | 13 | ## Use 14 | 15 | Define actions you handle and events you emit in your service in a `.service.types.ts` file: 16 | 17 | Example sample1.service.types.ts: 18 | 19 | ```ts 20 | import { 21 | GenericActionWithParameters, 22 | GenericActionWithoutParameters, 23 | GenericEventWithoutPayload, 24 | GenericEventWithPayload 25 | } from 'moleculer-service-ts'; 26 | 27 | export type ServiceName = 'sample1'; 28 | 29 | export type ServiceAction = 30 | | GenericActionWithoutParameters<'sample1.hello', string> 31 | | GenericActionWithParameters< 32 | 'sample1.boo', 33 | { foo: string; bar?: string }, 34 | string 35 | > 36 | | GenericActionWithParameters< 37 | 'sample1.welcome', 38 | { name: string }, 39 | string 40 | >; 41 | 42 | export type ServiceEvent = 43 | | GenericEventWithoutPayload<'sample1.event1'> 44 | | GenericEventWithPayload<'sample1.event2', { id: string }>; 45 | ``` 46 | 47 | Example sample2.service.types.ts: 48 | 49 | ```ts 50 | import { 51 | GenericActionWithParameters, 52 | GenericActionWithoutParameters, 53 | GenericEventWithoutPayload, 54 | GenericEventWithPayload 55 | } from 'moleculer-service-ts'; 56 | 57 | export type ServiceName = 'sample2'; 58 | 59 | export type ServiceAction = 60 | | GenericActionWithoutParameters<'sample2.hello', string> 61 | | GenericActionWithParameters< 62 | 'sample2.boo', 63 | { foo: string; bar?: string }, 64 | string 65 | > 66 | | GenericActionWithParameters< 67 | 'sample2.welcome', 68 | { name: string }, 69 | string 70 | >; 71 | 72 | export type ServiceEvent = 73 | | GenericEventWithoutPayload<'sample2.event1'> 74 | | GenericEventWithPayload<'sample2.event2', { id: string }>; 75 | ``` 76 | 77 | Then, when you want to call actions and emit events, you import the type definitions and feed them to a typed moleculer broker from this package: 78 | 79 | main.ts: 80 | 81 | ```ts 82 | import { TypedServiceBroker } from 'moleculer-service-ts'; 83 | 84 | // import the service types from sample1 service 85 | import { 86 | ServiceAction as Sample1Action, 87 | ServiceEvent as Sample1Event, 88 | ServiceName as Sample1Name 89 | } from './sample1.service.types'; // eslint-disable-line import/extensions 90 | 91 | // import the actual service schema of the sample1 service 92 | import sample1 from './sample1.service'; // eslint-disable-line import/extensions 93 | 94 | // import the service types from sample2 service 95 | import { 96 | ServiceAction as Sample2Action, 97 | ServiceEvent as Sample2Event, 98 | ServiceName as Sample2Name 99 | } from './sample2.service.types'; // eslint-disable-line import/extensions 100 | 101 | // import the actual service schema of the sample2 service 102 | import sample2 from './sample2.service'; // eslint-disable-line import/extensions 103 | 104 | // build union of types 105 | type ServiceAction = Sample1Action | Sample2Action; 106 | type ServiceEvent = Sample1Event | Sample2Event; 107 | type ServiceName = Sample1Name | Sample2Name; 108 | 109 | // create the typed broker 110 | const broker: TypedServiceBroker< 111 | ServiceAction, 112 | ServiceEvent, 113 | ServiceName 114 | > = new TypedServiceBroker({ 115 | logLevel: 'info' 116 | }); 117 | 118 | // create the services and start the broker 119 | broker.createService(sample1); 120 | broker.createService(sample2); 121 | broker.start(); 122 | 123 | // now the broker call/emit methods are typescript aware to your specific services 124 | broker.emit('sample1.event2', { id: '1234' }); // no typescript error 125 | 126 | broker.emit('sample1.event2'); // typescript error since arguments are expected 127 | 128 | broker.emit('sample1.event2', { id: 1234 }); // typescript error since arguments are of wrong type 129 | 130 | broker.call('sample1.hello'); // no typescript error 131 | 132 | broker.call('sample1.hello', {}); // typescript error since this action does not take an argument 133 | 134 | broker.call('sample1.welcome', { 135 | name: 'John' 136 | }); // no typescript error 137 | 138 | broker.call('sample1.welcome'); // typescript error since arguments are expected 139 | 140 | broker.call('sample1.welcome', { 141 | id: 1234 142 | }); // typescript error since wrong type of arguments are supplied 143 | 144 | const result: PromiseLike = broker.call('sample1.welcome', { 145 | name: 'John' 146 | }); // typescript error since return type is different 147 | ``` 148 | 149 | On VS Code and other typescript aware IDEs, code intellisense should work: 150 | 151 |

152 | 153 |

154 | 155 |

156 | 157 |

158 | 159 |

160 | 161 |

162 | -------------------------------------------------------------------------------- /image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytetechnology/moleculer-service-ts/cff1c09a0086fdf07f393ce0a805438fe91ffafd/image1.png -------------------------------------------------------------------------------- /image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytetechnology/moleculer-service-ts/cff1c09a0086fdf07f393ce0a805438fe91ffafd/image2.png -------------------------------------------------------------------------------- /image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytetechnology/moleculer-service-ts/cff1c09a0086fdf07f393ce0a805438fe91ffafd/image3.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleculer-service-ts", 3 | "version": "1.0.14", 4 | "description": "Typescript support for moleculer service actions and events", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist/**/*" 9 | ], 10 | "scripts": { 11 | "build": "tsc", 12 | "clean": "rm -rf dist", 13 | "format": "prettier --write \"{src,{tests,mocks}}/**/*.{js,ts}\"", 14 | "lint": "eslint --fix \"{src,{tests,mocks}}/**/*.{js,ts}\"", 15 | "prepare": "npm run build", 16 | "prepublishOnly": "npm test && npm run lint", 17 | "preversion": "npm run lint", 18 | "test": "jest --coverage --forceExit --detectOpenHandles --runInBand --no-cache", 19 | "version": "npm run format && git add -A src", 20 | "postversion": "git push && git push --tags" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/bytetechnology/moleculer-service-ts.git" 25 | }, 26 | "keywords": [ 27 | "moleculer", 28 | "service", 29 | "typescript" 30 | ], 31 | "author": "", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/bytetechnology/moleculer-service-ts/issues" 35 | }, 36 | "homepage": "https://github.com/bytetechnology/moleculer-service-ts#readme", 37 | "devDependencies": { 38 | "@types/jest": "^26.0.24", 39 | "@types/node": "^16.3.3", 40 | "@typescript-eslint/eslint-plugin": "^4.28.3", 41 | "@typescript-eslint/parser": "^4.28.3", 42 | "eslint": "^7.30.0", 43 | "eslint-config-airbnb-base": "^14.2.1", 44 | "eslint-config-prettier": "^8.3.0", 45 | "eslint-plugin-import": "^2.23.4", 46 | "eslint-plugin-prettier": "^3.4.0", 47 | "jest": "^27.0.6", 48 | "moleculer-decorators-extra": "^1.1.2", 49 | "prettier": "^2.3.2", 50 | "ts-jest": "^27.0.3", 51 | "typescript": "^4.3.5" 52 | }, 53 | "peerDependencies": { 54 | "moleculer": "^0.14.15" 55 | }, 56 | "jest": { 57 | "testEnvironment": "node", 58 | "rootDir": "./test", 59 | "coverageDirectory": "/coverage", 60 | "roots": [ 61 | "." 62 | ], 63 | "transform": { 64 | "^.+\\.tsx?$": "ts-jest" 65 | }, 66 | "testRegex": ".*\\.(test|spec).(ts|js)$", 67 | "globals": { 68 | "ts-jest": { 69 | "tsconfig": "./tsconfig.json" 70 | } 71 | }, 72 | "setupFiles": [], 73 | "moduleFileExtensions": [ 74 | "ts", 75 | "tsx", 76 | "js", 77 | "jsx", 78 | "json", 79 | "node" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | // Moleculer micro-services framework 3 | import moleculer from 'moleculer'; 4 | 5 | // This is so that we are forcing an actual key/value object type 6 | type GenericObject = { [name: string]: any }; 7 | 8 | // Action interfaces 9 | 10 | // All actions should have this interface 11 | interface ActionInterface { 12 | name: string; 13 | returns: unknown; 14 | } 15 | 16 | // Actions with parameters should have this interface 17 | interface ActionWithParametersInterface extends ActionInterface { 18 | parameters: GenericObject; 19 | } 20 | 21 | // Event interfaces 22 | 23 | interface EventInterface { 24 | name: string; 25 | } 26 | 27 | interface EventWithPayloadInterface extends EventInterface { 28 | payload: GenericObject; 29 | } 30 | 31 | // Action type utilities 32 | 33 | // Get simple action type from list of action type 34 | type ActionWithoutParameters = 35 | A extends ActionWithParametersInterface 36 | ? never 37 | : A extends EventInterface 38 | ? Exclude extends never 39 | ? A 40 | : never 41 | : never; 42 | 43 | // action with params 44 | type ActionWithParameters = 45 | A extends ActionWithParametersInterface ? A : never; 46 | 47 | // Get the parameters type for an ActionWithParameters type 48 | type ActionParameters = A extends ActionWithParametersInterface 49 | ? Extract['parameters'] 50 | : never; 51 | 52 | // Get the return type for an ActionWithParameters type 53 | type ActionReturns = A extends ActionInterface 54 | ? Extract['returns'] 55 | : never; 56 | 57 | // Get action name type from a list of event types 58 | type ActionName = A['name']; 59 | 60 | // Get action name types for event types without payload 61 | type ActionNameWithoutParameters = 62 | ActionWithoutParameters['name']; 63 | 64 | // Get action name types for event types with payload 65 | type ActionNameWithParameters = 66 | ActionWithParameters['name']; 67 | 68 | // Event type utilities 69 | 70 | // Get simple event type from list of event type 71 | type EventWithoutPayload = 72 | E extends EventWithPayloadInterface 73 | ? never 74 | : E extends EventInterface 75 | ? Exclude extends never 76 | ? E 77 | : never 78 | : never; 79 | 80 | // Get payload event type from list of event type 81 | type EventWithPayload = 82 | E extends EventWithPayloadInterface ? E : never; 83 | 84 | // Get the payload type for an EventWithPayload type 85 | type EventPayload = E extends EventWithPayloadInterface 86 | ? Extract['payload'] 87 | : never; 88 | 89 | // Get event name types for event types without payload 90 | type EventNameWithoutPayload = 91 | EventWithoutPayload['name']; 92 | 93 | // Get event name types for event types with payload 94 | type EventNameWithPayload = 95 | EventWithPayload['name']; 96 | 97 | // Our exports 98 | 99 | // Our main action type generics 100 | export type GenericActionWithoutParameters< 101 | N extends string, 102 | R extends any 103 | > = { 104 | name: N; 105 | returns: R; 106 | }; 107 | 108 | export type GenericActionWithParameters< 109 | N extends string, 110 | P extends GenericObject, 111 | R extends any 112 | > = { 113 | name: N; 114 | parameters: P; 115 | returns: R; 116 | }; 117 | 118 | // Our main event type generics 119 | export type GenericEventWithoutPayload = { 120 | name: N; 121 | }; 122 | 123 | export type GenericEventWithPayload< 124 | N extends string, 125 | P extends GenericObject 126 | > = { 127 | name: N; 128 | payload: P; 129 | }; 130 | 131 | // Our typed generic moleculer broker 132 | export class TypedServiceBroker< 133 | A extends ActionInterface, 134 | E extends EventInterface, 135 | // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars 136 | S extends string, 137 | M extends GenericObject = GenericObject 138 | > extends moleculer.ServiceBroker { 139 | // Overload our call functions to type them 140 | public call>( 141 | name: T, 142 | params?: undefined, 143 | opts?: moleculer.CallingOptions & { 144 | meta?: M; 145 | } 146 | ): Promise>; 147 | 148 | // eslint-disable-next-line no-dupe-class-members 149 | public call>( 150 | name: T, 151 | params: ActionParameters, 152 | opts?: moleculer.CallingOptions & { 153 | meta?: M; 154 | } 155 | ): Promise>; 156 | 157 | // eslint-disable-next-line no-dupe-class-members 158 | public call>( 159 | name: T, 160 | params?: ActionParameters, 161 | opts?: moleculer.CallingOptions & { 162 | meta?: M; 163 | } 164 | ): Promise> { 165 | return super.call(name, params, opts); 166 | } 167 | 168 | // Overload our emit functions to type them 169 | public emit>( 170 | name: T, 171 | payload?: undefined, 172 | groups?: string | Array 173 | ): Promise; 174 | 175 | // eslint-disable-next-line no-dupe-class-members 176 | public emit>( 177 | name: T, 178 | payload?: undefined, 179 | opts?: GenericObject 180 | ): Promise; 181 | 182 | // eslint-disable-next-line no-dupe-class-members 183 | public emit>( 184 | name: T, 185 | payload: EventPayload, 186 | groups?: string | Array 187 | ): Promise; 188 | 189 | // eslint-disable-next-line no-dupe-class-members 190 | public emit>( 191 | name: T, 192 | payload: EventPayload, 193 | opts?: moleculer.GenericObject 194 | ): Promise; 195 | 196 | // eslint-disable-next-line no-dupe-class-members 197 | public emit(name: any, payload?: any, opts?: any): Promise { 198 | return super.emit(name, payload, opts); 199 | } 200 | 201 | // Overload our broadcast functions to type them 202 | public broadcast>( 203 | name: T, 204 | payload?: undefined, 205 | groups?: string | Array 206 | ): Promise; 207 | 208 | // eslint-disable-next-line no-dupe-class-members 209 | public broadcast>( 210 | name: T, 211 | payload?: undefined, 212 | opts?: GenericObject 213 | ): Promise; 214 | 215 | // eslint-disable-next-line no-dupe-class-members 216 | public broadcast>( 217 | name: T, 218 | payload: EventPayload, 219 | groups?: string | Array 220 | ): Promise; 221 | 222 | // eslint-disable-next-line no-dupe-class-members 223 | public broadcast>( 224 | name: T, 225 | payload: EventPayload, 226 | opts?: moleculer.GenericObject 227 | ): Promise; 228 | 229 | // eslint-disable-next-line no-dupe-class-members 230 | public broadcast( 231 | name: any, 232 | payload?: any, 233 | opts?: any 234 | ): Promise { 235 | return super.broadcast(name, payload, opts); 236 | } 237 | 238 | // Overload our broadcastLocal functions to type them 239 | public broadcastLocal>( 240 | name: T, 241 | payload?: undefined, 242 | groups?: string | Array 243 | ): Promise; 244 | 245 | // eslint-disable-next-line no-dupe-class-members 246 | public broadcastLocal>( 247 | name: T, 248 | payload?: undefined, 249 | opts?: GenericObject 250 | ): Promise; 251 | 252 | // eslint-disable-next-line no-dupe-class-members 253 | public broadcastLocal>( 254 | name: T, 255 | payload: EventPayload, 256 | groups?: string | Array 257 | ): Promise; 258 | 259 | // eslint-disable-next-line no-dupe-class-members 260 | public broadcastLocal>( 261 | name: T, 262 | payload: EventPayload, 263 | opts?: moleculer.GenericObject 264 | ): Promise; 265 | 266 | // eslint-disable-next-line no-dupe-class-members 267 | public broadcastLocal( 268 | name: any, 269 | payload?: any, 270 | opts?: any 271 | ): Promise { 272 | return super.broadcastLocal(name, payload, opts); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /test/moleculer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TypedServiceBroker } from '../src/index'; // eslint-disable-line import/extensions 2 | import { 3 | ServiceAction as Sample1Action, 4 | ServiceEvent as Sample1Event, 5 | ServiceName as Sample1Name 6 | } from './services/sample1.service.types'; // eslint-disable-line import/extensions 7 | import sample1 from './services/sample1.service'; // eslint-disable-line import/extensions 8 | 9 | type ServiceAction = Sample1Action; 10 | type ServiceEvent = Sample1Event; 11 | type ServiceName = Sample1Name; 12 | 13 | describe('moleculer-service-ts', () => { 14 | const broker: TypedServiceBroker< 15 | ServiceAction, 16 | ServiceEvent, 17 | ServiceName, 18 | { 19 | auth: { 20 | userId: string; 21 | clientId: string; 22 | roles: string[]; 23 | }; 24 | } 25 | > = new TypedServiceBroker({ logLevel: 'fatal' }); 26 | const sampleService = broker.createService(sample1); 27 | 28 | beforeAll(async () => { 29 | await broker.start(); 30 | }); 31 | 32 | afterAll(async () => { 33 | broker.destroyService(sampleService); 34 | await broker.stop(); 35 | }); 36 | 37 | // test actions 38 | describe('Testing actions', () => { 39 | it('Action without parameter', async () => { 40 | const response: string = await broker.call('sample1.hello'); 41 | expect(response).toBe('Hello World null!'); 42 | }); 43 | 44 | it('Action without parameter, but with calling options', async () => { 45 | const response: string = await broker.call( 46 | 'sample1.hello', 47 | undefined, 48 | { caller: 'test' } 49 | ); 50 | expect(response).toBe('Hello World test!'); 51 | }); 52 | 53 | it('Action with required parameter', async () => { 54 | const response: string = await broker.call( 55 | 'sample1.welcome', 56 | { 57 | name: 'John Doe' 58 | }, 59 | { 60 | meta: { 61 | auth: { 62 | userId: 'abcd', 63 | clientId: 'efgh', 64 | roles: ['admin'] 65 | } 66 | } 67 | } 68 | ); 69 | expect(response).toBe('Welcome John Doe!'); 70 | }); 71 | 72 | it('Action with optional parameter missing', async () => { 73 | const response: string = await broker.call('sample1.boo', { 74 | foo: 'Foo' 75 | }); 76 | expect(response).toBe('Welcome Foo!'); 77 | }); 78 | 79 | it('Action with optional parameter included', async () => { 80 | const response: string = await broker.call('sample1.boo', { 81 | foo: 'Foo', 82 | bar: 'Bar' 83 | }); 84 | expect(response).toBe('Welcome Foo Bar!'); 85 | }); 86 | }); 87 | 88 | // test events 89 | describe('Testing events', () => { 90 | beforeAll(() => { 91 | sampleService.event1TestReturn = jest.fn(); 92 | sampleService.event2TestReturn = jest.fn(); 93 | }); 94 | afterAll(() => { 95 | sampleService.event1TestReturn.mockRestore(); 96 | sampleService.event2TestReturn.mockRestore(); 97 | }); 98 | 99 | it('Event1 without payload', () => { 100 | broker.emit('sample1.event1', undefined, 'sample1'); 101 | expect(sampleService.event1TestReturn).toBeCalledTimes(1); 102 | }); 103 | 104 | it('Event1 with payload', () => { 105 | // We use emitlocalEventHandler because our typed broker won't allow us to send bad payloads :-) 106 | sampleService.emitLocalEventHandler( 107 | 'sample1.event1', 108 | { foo: 'bar' }, 109 | 'sample1' 110 | ); 111 | expect(sampleService.event1TestReturn).toBeCalledTimes(1); 112 | }); 113 | 114 | it('Event2 with good payload', () => { 115 | broker.emit('sample1.event2', { id: '1234' }, 'sample1'); 116 | expect(sampleService.event2TestReturn).toBeCalledTimes(1); 117 | }); 118 | 119 | it('Event2 with bad payload', () => { 120 | // We use emitlocalEventHandler because our typed broker won't allow us to send bad payloads :-) 121 | sampleService.emitLocalEventHandler( 122 | 'sample1.event2', 123 | { id: 1234 }, 124 | 'sample1' 125 | ); 126 | expect(sampleService.event2TestReturn).toBeCalledTimes(1); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/services/sample1.service.ts: -------------------------------------------------------------------------------- 1 | // Moleculer micro-services framework 2 | import moleculer, { Errors } from 'moleculer'; 3 | import { 4 | Action, 5 | Event, 6 | Method, 7 | Service 8 | } from 'moleculer-decorators-extra'; 9 | 10 | const eventSchema = { id: 'string' }; 11 | 12 | // Define our service 13 | @Service({ 14 | // Our service name 15 | name: 'sample1' 16 | }) 17 | class Sample1 extends moleculer.Service { 18 | // Our actions 19 | @Action() 20 | hello(ctx: moleculer.Context) { 21 | this.logger.info( 22 | `hello got called from ${ctx.nodeID}; caller: ${ctx.caller}` 23 | ); 24 | return `Hello World ${ctx.caller}!`; 25 | } 26 | 27 | @Action({ 28 | cache: false, 29 | params: { 30 | name: 'string' 31 | } 32 | }) 33 | welcome(ctx: moleculer.Context<{ name: string }>) { 34 | this.logger.info(`welcome got called from ${ctx.nodeID}`); 35 | return `Welcome ${ctx.params.name}!`; 36 | } 37 | 38 | @Action({ 39 | cache: false, 40 | params: { 41 | foo: 'string', 42 | bar: { 43 | type: 'string', 44 | optional: true 45 | } 46 | } 47 | }) 48 | boo(ctx: moleculer.Context<{ foo: string; bar?: string }>) { 49 | this.logger.info(`boo got called from ${ctx.nodeID}`); 50 | if (ctx.params.bar) 51 | return `Welcome ${ctx.params.foo} ${ctx.params.bar}!`; 52 | return `Welcome ${ctx.params.foo}!`; 53 | } 54 | 55 | /* istanbul ignore next */ 56 | @Method 57 | event1TestReturn() {} // eslint-disable-line class-methods-use-this 58 | 59 | @Event() 'sample1.event1'( 60 | payload: never, 61 | sender: string, 62 | eventName: string 63 | ) { 64 | if (payload) { 65 | this.logger.error( 66 | `Validation check failed! event ${eventName} does not take any payload!` 67 | ); 68 | throw new Errors.ValidationError( 69 | 'Event parameter check failed', 70 | 'ERR_VALIDATION', 71 | payload 72 | ); 73 | } 74 | 75 | this.logger.info(`Got event ${eventName} from sender ${sender}`); 76 | this.event1TestReturn(); 77 | } 78 | 79 | /* istanbul ignore next */ 80 | @Method 81 | event2TestReturn() {} // eslint-disable-line class-methods-use-this 82 | 83 | @Event({ 84 | params: { 85 | id: 'string' 86 | } 87 | }) 88 | 'sample1.event2'( 89 | payload: typeof eventSchema, 90 | sender: string, 91 | eventName: string 92 | ) { 93 | this.logger.info( 94 | `Got event ${eventName} from sender ${sender}; id: ${payload.id}` 95 | ); 96 | this.event2TestReturn(); 97 | } 98 | } 99 | 100 | export default Sample1; 101 | -------------------------------------------------------------------------------- /test/services/sample1.service.types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GenericActionWithParameters, 3 | GenericActionWithoutParameters, 4 | GenericEventWithoutPayload, 5 | GenericEventWithPayload 6 | } from '../../src/index'; // eslint-disable-line import/extensions 7 | 8 | export type ServiceName = 'sample1'; 9 | 10 | export type ServiceAction = 11 | | GenericActionWithoutParameters<'sample1.hello', string> 12 | | GenericActionWithParameters< 13 | 'sample1.boo', 14 | { foo: string; bar?: string }, 15 | string 16 | > 17 | | GenericActionWithParameters< 18 | 'sample1.welcome', 19 | { name: string }, 20 | string 21 | >; 22 | 23 | export type ServiceEvent = 24 | | GenericEventWithoutPayload<'sample1.event1'> 25 | | GenericEventWithPayload<'sample1.event2', { id: string }>; 26 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "sourceMap": false, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "isolatedModules": false, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "strict": true, 12 | "declaration": true, 13 | "noImplicitAny": false, 14 | "removeComments": true, 15 | "noLib": false, 16 | "preserveConstEnums": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "outDir": "dist", 19 | "baseUrl": ".", 20 | "paths": { 21 | "*": [ 22 | "*" 23 | ] 24 | } 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "typings/main", 29 | "typings/main.d.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "sourceMap": false, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "isolatedModules": true, 8 | "strict": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "esModuleInterop": true, 12 | "declaration": true, 13 | "noImplicitAny": true, 14 | "removeComments": true, 15 | "noLib": false, 16 | "preserveConstEnums": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "outDir": "dist", 19 | "baseUrl": "./src/", 20 | "paths": { 21 | "*": [ 22 | "src/*" 23 | ] 24 | } 25 | }, 26 | "include": [ 27 | "src/**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "typings/main", 32 | "typings/main.d.ts" 33 | ] 34 | } --------------------------------------------------------------------------------