From 7f5c06894296e81b7f314dc6dcc21fc6d626cce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C5=A0vanda?= <46406259+Papooch@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:39:01 +0200 Subject: [PATCH] fix(core): support primitive values in websocket payload (#172) * fix(core): support primitive values in websocket payload * test(core): add tests for ws * docs: add example code to websockets section --- .../05_considerations/02_compatibility.md | 10 ++ docs/docusaurus.config.js | 9 ++ packages/core/package.json | 7 +- .../utils/context-cls-store-map.ts | 2 +- .../test/websockets/expect-ids-websockets.ts | 39 +++++ .../core/test/websockets/test-ws.filter.ts | 18 +++ .../core/test/websockets/websockets.app.ts | 55 +++++++ packages/core/test/websockets/ws.spec.ts | 76 +++++++++ yarn.lock | 151 +++++++++++++++++- 9 files changed, 358 insertions(+), 9 deletions(-) create mode 100644 packages/core/test/websockets/expect-ids-websockets.ts create mode 100644 packages/core/test/websockets/test-ws.filter.ts create mode 100644 packages/core/test/websockets/websockets.app.ts create mode 100644 packages/core/test/websockets/ws.spec.ts diff --git a/docs/docs/05_considerations/02_compatibility.md b/docs/docs/05_considerations/02_compatibility.md index 8e7149aa..a9823109 100644 --- a/docs/docs/05_considerations/02_compatibility.md +++ b/docs/docs/05_considerations/02_compatibility.md @@ -56,3 +56,13 @@ Below are listed transports with which it is confirmed to work: ### Websockets _Websocket Gateways_ don't respect globally bound enhancers, therefore it is required to bind the `ClsGuard` or `ClsInterceptor` manually on the `WebsocketGateway`. Special care is also needed for the `handleConnection` method (See [#8](https://github.com/Papooch/nestjs-cls/issues/8)) + +```ts +@WebSocketGateway() +// highlight-start +@UseInterceptors(ClsInterceptor) +// highlight-end +export class Gateway { + // ... +} +``` diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 76ca7188..8f823324 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -100,6 +100,15 @@ const config = { footer: { style: 'dark', links: [ + { + title: 'Docs', + items: [ + { + label: 'NestJS Documentation', + href: 'https://docs.nestjs.com/', + }, + ], + }, { title: 'Community', items: [ diff --git a/packages/core/package.json b/packages/core/package.json index 2a3864cf..6086107b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,12 +56,15 @@ "@nestjs/mercurius": "^12.1.1", "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-fastify": "^10.3.7", + "@nestjs/platform-ws": "^10.3.10", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.3.7", + "@nestjs/websockets": "^10.3.10", "@types/express": "^4.17.13", "@types/jest": "^28.1.2", "@types/node": "^18.0.0", "@types/supertest": "^2.0.12", + "@types/ws": "^8", "graphql": "^16.5.0", "jest": "^29.7.0", "mercurius": "^13.0.0", @@ -69,10 +72,12 @@ "rimraf": "^3.0.2", "rxjs": "^7.5.5", "supertest": "^6.2.3", + "superwstest": "^2.0.4", "ts-jest": "^29.1.2", "ts-loader": "^9.3.0", "ts-node": "^10.8.1", "tsconfig-paths": "^4.0.0", - "typescript": "5.0" + "typescript": "5.0", + "ws": "^8.18.0" } } diff --git a/packages/core/src/lib/cls-initializers/utils/context-cls-store-map.ts b/packages/core/src/lib/cls-initializers/utils/context-cls-store-map.ts index 1c1d2a77..0846518e 100644 --- a/packages/core/src/lib/cls-initializers/utils/context-cls-store-map.ts +++ b/packages/core/src/lib/cls-initializers/utils/context-cls-store-map.ts @@ -39,7 +39,7 @@ export class ContextClsStoreMap { case 'http': return context.switchToHttp().getRequest(); case 'ws': - return context.switchToWs().getData(); + return context.switchToWs(); case 'rpc': return context.switchToRpc().getContext(); case 'graphql': diff --git a/packages/core/test/websockets/expect-ids-websockets.ts b/packages/core/test/websockets/expect-ids-websockets.ts new file mode 100644 index 00000000..d600bedc --- /dev/null +++ b/packages/core/test/websockets/expect-ids-websockets.ts @@ -0,0 +1,39 @@ +import { INestApplication } from '@nestjs/common'; +import request from 'superwstest'; + +export const expectOkIdsWs = + (path = '', event = 'hello', data = {}) => + async (app: INestApplication) => + request(await app.getUrl()) + .ws(path) + .sendJson({ + event, + data, + }) + .expectJson((body) => { + const id = body.fromGuard ?? body.fromInterceptor; + expect(body.fromInterceptor).toEqual(id); + expect(body.fromInterceptorAfter).toEqual(id); + expect(body.fromGateway).toEqual(id); + expect(body.fromService).toEqual(id); + expect(body.data).toEqual(data); + }) + .close(); + +export const expectErrorIdsWs = + (path = '', event = 'error', data = {}) => + (app: INestApplication) => + request(app.getHttpServer()) + .ws(path) + .sendJson({ + event, + data, + }) + .expectJson((body) => { + const id = body.fromGuard ?? body.fromInterceptor; + expect(body.fromInterceptor).toEqual(id); + expect(body.fromGateway).toEqual(id); + expect(body.fromService).toEqual(id); + expect(body.fromFilter).toEqual(id); + }) + .close(); diff --git a/packages/core/test/websockets/test-ws.filter.ts b/packages/core/test/websockets/test-ws.filter.ts new file mode 100644 index 00000000..8d779bc1 --- /dev/null +++ b/packages/core/test/websockets/test-ws.filter.ts @@ -0,0 +1,18 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; +import { WebSocket } from 'ws'; +import { ClsService } from '../../src'; +import { TestException } from '../common/test.exception'; + +@Catch(TestException) +export class TestWsExceptionFilter implements ExceptionFilter { + constructor(private readonly cls: ClsService) {} + + catch(exception: TestException, host: ArgumentsHost) { + const client = host.switchToWs().getClient(); + const response = { + ...exception.response, + fromFilter: this.cls.getId(), + }; + client.send(JSON.stringify(response)); + } +} diff --git a/packages/core/test/websockets/websockets.app.ts b/packages/core/test/websockets/websockets.app.ts new file mode 100644 index 00000000..2359d5e8 --- /dev/null +++ b/packages/core/test/websockets/websockets.app.ts @@ -0,0 +1,55 @@ +import { + Injectable, + UseFilters, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { MessageBody, SubscribeMessage } from '@nestjs/websockets'; + +import { ClsService } from '../../src'; +import { TestException } from '../common/test.exception'; +import { TestGuard } from '../common/test.guard'; +import { TestInterceptor } from '../common/test.interceptor'; +import { TestWsExceptionFilter } from './test-ws.filter'; + +@Injectable() +export class TestWebsocketService { + constructor(private readonly cls: ClsService) {} + + async hello(data?: unknown) { + return { + fromGuard: this.cls.get('FROM_GUARD'), + fromInterceptor: this.cls.get('FROM_INTERCEPTOR'), + fromInterceptorAfter: this.cls.get('FROM_INTERCEPTOR'), + fromGateway: this.cls.get('FROM_GATEWAY'), + fromService: this.cls.getId(), + data, + }; + } +} + +@Injectable() +@UseFilters(TestWsExceptionFilter) +export class TestWebsocketGateway { + constructor( + private readonly service: TestWebsocketService, + private readonly cls: ClsService, + ) {} + + @SubscribeMessage('hello') + @UseGuards(TestGuard) + @UseInterceptors(TestInterceptor) + hello(@MessageBody() data: unknown) { + this.cls.set('FROM_GATEWAY', this.cls.getId()); + return this.service.hello(data); + } + + @UseInterceptors(TestInterceptor) + @UseGuards(TestGuard) + @SubscribeMessage('error') + async error(@MessageBody() data: unknown) { + this.cls.set('FROM_GATEWAY', this.cls.getId()); + const response = await this.service.hello(data); + throw new TestException(response); + } +} diff --git a/packages/core/test/websockets/ws.spec.ts b/packages/core/test/websockets/ws.spec.ts new file mode 100644 index 00000000..22e1c43b --- /dev/null +++ b/packages/core/test/websockets/ws.spec.ts @@ -0,0 +1,76 @@ +import { + INestApplication, + Module, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { WsAdapter } from '@nestjs/platform-ws'; +import { Test } from '@nestjs/testing'; +import { WebSocketGateway } from '@nestjs/websockets'; +import { ClsGuard, ClsInterceptor, ClsModule } from '../../src'; +import { expectErrorIdsWs, expectOkIdsWs } from './expect-ids-websockets'; +import { TestWebsocketGateway, TestWebsocketService } from './websockets.app'; + +describe('Websockets - WS', () => { + let app: INestApplication; + + @WebSocketGateway({ path: 'interceptor' }) + @UseInterceptors(ClsInterceptor) + class WebsocketGatewayWithClsInterceptor extends TestWebsocketGateway {} + + @WebSocketGateway({ path: 'guard' }) + @UseGuards(ClsGuard) + class WebsocketGatewayWithClsGuard extends TestWebsocketGateway {} + + @Module({ + imports: [ + ClsModule.forRoot({ + interceptor: { mount: false, generateId: true }, + guard: { mount: false, generateId: true }, + }), + ], + providers: [ + TestWebsocketService, + WebsocketGatewayWithClsInterceptor, + WebsocketGatewayWithClsGuard, + ], + }) + class TestWebsocketModule {} + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [TestWebsocketModule], + }).compile(); + app = moduleFixture.createNestApplication(); + app.useWebSocketAdapter(new WsAdapter(app)); + await app.listen(3125); + }); + + afterAll(async () => { + await app?.close(); + }); + + describe.each(['guard', 'interceptor'])( + 'when using an %s to initialize the context', + (name) => { + const path = '/' + name; + + it.each([ + ['ok', 'object', expectOkIdsWs(path, 'hello', { value: 12 })], + ['ok', 'primitive', expectOkIdsWs(path, 'hello', 'primitive')], + [ + 'error', + 'object', + expectErrorIdsWs(path, 'error', { value: 12 }), + ], + [ + 'error', + 'primitive', + expectErrorIdsWs(path, 'error', 'primitive'), + ], + ])('works with %s response and %s payload', async (_, __, func) => { + await func(app); + }); + }, + ); +}); diff --git a/yarn.lock b/yarn.lock index dcf22ce0..103a366d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5633,6 +5633,20 @@ __metadata: languageName: node linkType: hard +"@nestjs/platform-ws@npm:^10.3.10": + version: 10.3.10 + resolution: "@nestjs/platform-ws@npm:10.3.10" + dependencies: + tslib: "npm:2.6.3" + ws: "npm:8.17.1" + peerDependencies: + "@nestjs/common": ^10.0.0 + "@nestjs/websockets": ^10.0.0 + rxjs: ^7.1.0 + checksum: ad36db73162aeaeff42c1e789041efcf2cd16bc74b29fd9e214c5daafb3fdada701462a0bea2778455d876f9637c875504ac87ee6aa4b3c2855944ca649dd57e + languageName: node + linkType: hard + "@nestjs/schematics@npm:^10.0.1": version: 10.0.1 resolution: "@nestjs/schematics@npm:10.0.1" @@ -5667,6 +5681,26 @@ __metadata: languageName: node linkType: hard +"@nestjs/websockets@npm:^10.3.10": + version: 10.3.10 + resolution: "@nestjs/websockets@npm:10.3.10" + dependencies: + iterare: "npm:1.2.1" + object-hash: "npm:3.0.0" + tslib: "npm:2.6.3" + peerDependencies: + "@nestjs/common": ^10.0.0 + "@nestjs/core": ^10.0.0 + "@nestjs/platform-socket.io": ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + "@nestjs/platform-socket.io": + optional: true + checksum: eaad3ef3a58c03ab8004aae70ad3d5f3ba4d51e0baabe336a6cb6ac0bf82fc0e34495981317664f25a772c4481cf31a6ed4bfb2c0f02c697b248919ed1ebe4c2 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -6482,6 +6516,13 @@ __metadata: languageName: node linkType: hard +"@types/cookiejar@npm:^2.1.5": + version: 2.1.5 + resolution: "@types/cookiejar@npm:2.1.5" + checksum: af38c3d84aebb3ccc6e46fb6afeeaac80fb26e63a487dd4db5a8b87e6ad3d4b845ba1116b2ae90d6f886290a36200fa433d8b1f6fe19c47da6b81872ce9a2764 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -6710,6 +6751,13 @@ __metadata: languageName: node linkType: hard +"@types/methods@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/methods@npm:1.1.4" + checksum: a78534d79c300718298bfff92facd07bf38429c66191f640c1db4c9cff1e36f819304298a96f7536b6512bfc398e5c3e6b831405e138cd774b88ad7be78d682a + languageName: node + linkType: hard + "@types/mime@npm:*": version: 3.0.1 resolution: "@types/mime@npm:3.0.1" @@ -6987,6 +7035,28 @@ __metadata: languageName: node linkType: hard +"@types/superagent@npm:^8.1.0": + version: 8.1.8 + resolution: "@types/superagent@npm:8.1.8" + dependencies: + "@types/cookiejar": "npm:^2.1.5" + "@types/methods": "npm:^1.1.4" + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: c5fa8fe48e63445317d2e056c93c373a14cd916ac7b6e5a084f8cdecc70419683c89e3245ad47ff3d1f33406cfdc23117e3877651b184257adcd3063b7037feb + languageName: node + linkType: hard + +"@types/supertest@npm:*": + version: 6.0.2 + resolution: "@types/supertest@npm:6.0.2" + dependencies: + "@types/methods": "npm:^1.1.4" + "@types/superagent": "npm:^8.1.0" + checksum: 44a28f9b35b65800f4c7bcc23748e71c925098aa74ea504d14c98385c36d00de2a4f5aca15d7dc4514bc80533e0af21f985a4ab9f5f317c7266e9e75836aef39 + languageName: node + linkType: hard + "@types/supertest@npm:^2.0.12": version: 2.0.12 resolution: "@types/supertest@npm:2.0.12" @@ -7043,6 +7113,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:7.x || 8.x, @types/ws@npm:^8": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "npm:*" + checksum: 3fd77c9e4e05c24ce42bfc7647f7506b08c40a40fe2aea236ef6d4e96fc7cb4006a81ed1b28ec9c457e177a74a72924f4768b7b4652680b42dfd52bc380e15f9 + languageName: node + linkType: hard + "@types/ws@npm:^8.5.5": version: 8.5.5 resolution: "@types/ws@npm:8.5.5" @@ -17142,12 +17221,15 @@ __metadata: "@nestjs/mercurius": "npm:^12.1.1" "@nestjs/platform-express": "npm:^10.3.7" "@nestjs/platform-fastify": "npm:^10.3.7" + "@nestjs/platform-ws": "npm:^10.3.10" "@nestjs/schematics": "npm:^10.0.1" "@nestjs/testing": "npm:^10.3.7" + "@nestjs/websockets": "npm:^10.3.10" "@types/express": "npm:^4.17.13" "@types/jest": "npm:^28.1.2" "@types/node": "npm:^18.0.0" "@types/supertest": "npm:^2.0.12" + "@types/ws": "npm:^8" graphql: "npm:^16.5.0" jest: "npm:^29.7.0" mercurius: "npm:^13.0.0" @@ -17155,11 +17237,13 @@ __metadata: rimraf: "npm:^3.0.2" rxjs: "npm:^7.5.5" supertest: "npm:^6.2.3" + superwstest: "npm:^2.0.4" ts-jest: "npm:^29.1.2" ts-loader: "npm:^9.3.0" ts-node: "npm:^10.8.1" tsconfig-paths: "npm:^4.0.0" typescript: "npm:5.0" + ws: "npm:^8.18.0" peerDependencies: "@nestjs/common": "> 7.0.0 < 11" "@nestjs/core": "> 7.0.0 < 11" @@ -17459,6 +17543,13 @@ __metadata: languageName: node linkType: hard +"object-hash@npm:3.0.0": + version: 3.0.0 + resolution: "object-hash@npm:3.0.0" + checksum: a06844537107b960c1c8b96cd2ac8592a265186bfa0f6ccafe0d34eabdb526f6fa81da1f37c43df7ed13b12a4ae3457a16071603bcd39d8beddb5f08c37b0f47 + languageName: node + linkType: hard + "object-inspect@npm:^1.9.0": version: 1.12.3 resolution: "object-inspect@npm:1.12.3" @@ -21090,6 +21181,22 @@ __metadata: languageName: node linkType: hard +"superwstest@npm:^2.0.4": + version: 2.0.4 + resolution: "superwstest@npm:2.0.4" + dependencies: + "@types/supertest": "npm:*" + "@types/ws": "npm:7.x || 8.x" + ws: "npm:7.x || 8.x" + peerDependencies: + supertest: "*" + peerDependenciesMeta: + supertest: + optional: true + checksum: da553e5fb803a7a92e083d4691cca5a809d94174ac9d7b6c88b08e2d467ea24703ead5bdf3474182c8fae79f289fb97580d98b7d11d46c176103f0f3cacbb0a3 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -21730,6 +21837,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.6.3, tslib@npm:^2.6.3": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a + languageName: node + linkType: hard + "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -21744,13 +21858,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.6.3": - version: 2.6.3 - resolution: "tslib@npm:2.6.3" - checksum: 2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -23007,6 +23114,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:7.x || 8.x, ws@npm:^8.18.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 25eb33aff17edcb90721ed6b0eb250976328533ad3cd1a28a274bd263682e7296a6591ff1436d6cbc50fa67463158b062f9d1122013b361cec99a05f84680e06 + languageName: node + linkType: hard + "ws@npm:8.16.0": version: 8.16.0 resolution: "ws@npm:8.16.0" @@ -23022,6 +23144,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: f4a49064afae4500be772abdc2211c8518f39e1c959640457dcee15d4488628620625c783902a52af2dd02f68558da2868fd06e6fd0e67ebcd09e6881b1b5bfe + languageName: node + linkType: hard + "ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0, ws@npm:^7, ws@npm:^7.3.1, ws@npm:^7.5.5": version: 7.5.9 resolution: "ws@npm:7.5.9"