diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3c4103..2f78f19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,9 @@ jobs: key: ${{ runner.os }}-nx-${{ steps.branch-names.outputs.current_branch }} - run: git branch --track main origin/master - name: Test and build - run: npx nx affected -t test build --parallel=3 --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,database,@nestjs-json-api/source' + env: + NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 + run: npx nx affected -t test build --parallel=3 --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,database,@nestjs-json-api/source,type-for-rpc' - name: Save cached .nx id: cache-dependencies-save uses: actions/cache/save@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ea608c3..ffbf673 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,7 +34,6 @@ jobs: .nx key: ${{ runner.os }}-nx-master - run: npx nx affected -t build --parallel=3 --exclude='json-api-front,json-api-server,json-api-server-e2e,shared-utils,json-shared-type,database' - - run: ls -l ./dist/libs/json-api/json-api-nestjs-sdk - name: Publish packages run: npx nx release publish shell: bash diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml deleted file mode 100644 index a007fe8..0000000 --- a/.verdaccio/config.yml +++ /dev/null @@ -1,28 +0,0 @@ -# path to a directory with all packages -storage: ../tmp/local-registry/storage - -# a list of other known repositories we can talk to -uplinks: - npmjs: - url: https://registry.npmjs.org/ - maxage: 60m - -packages: - '**': - # give all users (including non-authenticated users) full access - # because it is a local registry - access: $all - publish: $all - unpublish: $all - - # if package is not available locally, proxy requests to npm registry - proxy: npmjs - -# log settings -logs: - type: stdout - format: pretty - level: warn - -publish: - allow_offline: true # set offline to true to allow publish offline diff --git a/README.md b/README.md index aeb5370..9bdbaba 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ - *[json-api-nestjs](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs)* - plugin for create CRUD overs JSON API - *[json-api-nestjs-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs-sdk)* - tool for client, call api over *json-api-nestjs* +- *[nestjs-json-rpc](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc)* - plugin for create RPC server using [JSON-RPC](https://www.jsonrpc.org/) +- *[nestjs-json-rpc-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc-sdk)* - tool for client, call RPC server *nestjs-json-rpc* - *json-api-nestjs-acl* - tool for acl over *json-api-nestjs*(coming soon...) ## Installation diff --git a/apps/json-api-front/proxy.conf.json b/apps/json-api-front/proxy.conf.json index 63dd627..6974fc0 100644 --- a/apps/json-api-front/proxy.conf.json +++ b/apps/json-api-front/proxy.conf.json @@ -2,5 +2,10 @@ "/api": { "target": "http://localhost:3000", "secure": false + }, + "/rpc": { + "target": "http://localhost:3000", + "secure": false, + "ws": true } } diff --git a/apps/json-api-front/src/app/app.component.ts b/apps/json-api-front/src/app/app.component.ts index 4f91abb..43a3508 100644 --- a/apps/json-api-front/src/app/app.component.ts +++ b/apps/json-api-front/src/app/app.component.ts @@ -2,8 +2,19 @@ import { Component, inject, OnInit } from '@angular/core'; import { NxWelcomeComponent } from './nx-welcome.component'; import { JsonApiSdkService } from 'json-api-nestjs-sdk'; import { AtomicFactory } from 'json-api-nestjs-sdk/json-api-nestjs-sdk.module'; +import { + JSON_RPC, + RPC_BATCH, + Rpc, +} from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module'; + +import { RpcService as IRpcService } from '@nestjs-json-api/type-for-rpc'; import { switchMap } from 'rxjs'; +type RpcMap = { + RpcService: IRpcService; +}; + @Component({ standalone: true, imports: [NxWelcomeComponent], @@ -14,14 +25,25 @@ import { switchMap } from 'rxjs'; export class AppComponent implements OnInit { private JsonApiSdkService = inject(JsonApiSdkService); private atomicFactory = inject(AtomicFactory); + private rpc = inject>(JSON_RPC); + private rpcBatch = inject(RPC_BATCH); ngOnInit(): void { - // this.JsonApiSdkService.getAll(class Users {}, { - // page: { - // size: 2, - // number: 1, - // }, - // }).subscribe((r) => console.log(r)); + const rpc1 = this.rpc.RpcService.someMethode(1); + + const rpc2 = this.rpc.RpcService.methodeWithObjectParams({ + a: 1, + b: 1, + }); + + this.rpcBatch(rpc2, rpc1).subscribe(([r2, r1]) => console.log(r1, r2)); + + this.JsonApiSdkService.getAll(class Users {}, { + page: { + size: 2, + number: 1, + }, + }).subscribe((r) => console.log(r)); class Addresses { id = 1; @@ -54,9 +76,9 @@ export class AppComponent implements OnInit { const tmpUsers = new Users(); tmpUsers.id = 1; - // this.JsonApiSdkService.getRelationships(tmpUsers, 'addresses').subscribe( - // (r) => console.log(r) - // ); + this.JsonApiSdkService.getRelationships(tmpUsers, 'addresses').subscribe( + (r) => console.log(r) + ); const roles = new Roles(); roles.id = 10000; diff --git a/apps/json-api-front/src/app/app.config.ts b/apps/json-api-front/src/app/app.config.ts index fe16eb7..e6d0f35 100644 --- a/apps/json-api-front/src/app/app.config.ts +++ b/apps/json-api-front/src/app/app.config.ts @@ -1,5 +1,60 @@ -import { ApplicationConfig, importProvidersFrom } from '@angular/core'; +import { + ApplicationConfig, + importProvidersFrom, + InjectionToken, +} from '@angular/core'; import { JsonApiAngular } from 'json-api-nestjs-sdk/json-api-nestjs-sdk.module'; +import { + JsonRpcAngular, + JsonRpcAngularConfig, + TransportType, +} from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module'; +import { Subject } from 'rxjs'; +import { webSocket } from 'rxjs/webSocket'; +import { io } from 'socket.io-client'; + +const destroySubject = new Subject(); +setTimeout(() => { + console.log('Disconnect'); + destroySubject.next(true); + destroySubject.complete(); +}, 5000); +const destroySubjectToken = new InjectionToken('destroySubjectToken', { + factory: () => destroySubject, +}); +destroySubject.subscribe((r) => console.log(r)); +const tokenSocketInst = new InjectionToken('tokenSocketInst', { + factory: () => webSocket('ws://localhost:4200/rpc'), +}); + +const tokenIoSocketInst = new InjectionToken('tokenIoSocketInst', { + factory: () => io('http://localhost:3000', { path: '/rpc' }), +}); + +const httpConfig: JsonRpcAngularConfig = { + transport: TransportType.HTTP, + rpcPath: '/api/rpc', + rpcHost: 'http://localhost:4200', +}; +const wsConfig: JsonRpcAngularConfig = { + transport: TransportType.WS, + useWsNativeSocket: true, + rpcPath: 'rpc', + rpcHost: 'ws://localhost:4200', + destroySubjectToken, +}; +const wsConfigWithToken: JsonRpcAngularConfig = { + transport: TransportType.WS, + useWsNativeSocket: true, + tokenSocketInst, + destroySubjectToken, +}; +const ioConfig: JsonRpcAngularConfig = { + transport: TransportType.WS, + useWsNativeSocket: false, + destroySubjectToken, + tokenSocketInst: tokenIoSocketInst, +}; export const appConfig: ApplicationConfig = { providers: [ @@ -11,5 +66,13 @@ export const appConfig: ApplicationConfig = { operationUrl: 'operation', }) ), + importProvidersFrom( + JsonRpcAngular.forRoot( + // httpConfig + // wsConfig + // wsConfigWithToken, + ioConfig + ) + ), ], }; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts index 2bbf853..7b7f3c2 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts @@ -3,7 +3,7 @@ import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; import { faker } from '@faker-js/faker'; import { getUser } from '../utils/data-utils'; -import { run, creatSdk } from '../utils/run-ppplication'; +import { run, creatSdk } from '../utils/run-application'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts index 91751fb..47b18d9 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts @@ -3,7 +3,7 @@ import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; import { AxiosError } from 'axios'; import { Users } from 'database'; -import { run, creatSdk } from '../utils/run-ppplication'; +import { run, creatSdk } from '../utils/run-application'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts index 3a44773..076dc27 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts @@ -4,7 +4,7 @@ import { BookList, Users } from 'database'; import { AxiosError } from 'axios'; import { faker } from '@faker-js/faker'; import { lastValueFrom } from 'rxjs'; -import { creatSdk, run, axiosAdapter } from '../utils/run-ppplication'; +import { creatSdk, run, axiosAdapter } from '../utils/run-application'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts index 8dec9b0..03e470c 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts @@ -4,7 +4,7 @@ import { faker } from '@faker-js/faker'; import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; import { getUser } from '../utils/data-utils'; -import { creatSdk, run } from '../utils/run-ppplication'; +import { creatSdk, run } from '../utils/run-application'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts index ffdd50f..b2bf226 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts @@ -3,7 +3,7 @@ import { Addresses, CommentKind, Comments, Users } from 'database'; import { faker } from '@faker-js/faker'; import { JsonSdkPromise } from 'json-api-nestjs-sdk'; -import { creatSdk, run } from '../utils/run-ppplication'; +import { creatSdk, run } from '../utils/run-application'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts index 640eb99..ebc9b48 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts @@ -2,7 +2,7 @@ import { Addresses, BookList, CommentKind, Comments, Users } from 'database'; import { faker } from '@faker-js/faker'; import { JsonSdkPromise } from 'json-api-nestjs-sdk'; -import { creatSdk, run } from '../utils/run-ppplication'; +import { creatSdk, run } from '../utils/run-application'; import { INestApplication } from '@nestjs/common'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-rpc/run-json-rpc.spec.ts b/apps/json-api-server-e2e/src/json-api/json-rpc/run-json-rpc.spec.ts new file mode 100644 index 0000000..3c15ada --- /dev/null +++ b/apps/json-api-server-e2e/src/json-api/json-rpc/run-json-rpc.spec.ts @@ -0,0 +1,102 @@ +import { INestApplication } from '@nestjs/common'; +import { + ResultRpcFactoryPromise, + ErrorCodeType, + RpcError, +} from '@klerick/nestjs-json-rpc-sdk'; + +import { creatRpcSdk, MapperRpc, run } from '../utils/run-application'; + +let app: INestApplication; + +beforeAll(async () => { + app = await run(); +}); + +afterAll(async () => { + await app.close(); +}); + +describe('Run json rpc:', () => { + let rpc: ResultRpcFactoryPromise['rpc']; + let rpcBatch: ResultRpcFactoryPromise['rpcBatch']; + let rpcForBatch: ResultRpcFactoryPromise['rpcForBatch']; + beforeEach(() => { + ({ rpc, rpcBatch, rpcForBatch } = creatRpcSdk()); + }); + + describe('Should be correct response', () => { + it('Should be call one method', async () => { + const input = 1; + const result = await rpc.RpcService.someMethode(input); + expect(result).toBe(input); + }); + + it('Should be correct response batch', async () => { + const input = 1; + const input2 = { + a: 1, + b: 2, + }; + const call1 = rpcForBatch.RpcService.someMethode(input); + const call2 = rpcForBatch.RpcService.methodeWithObjectParams(input2); + + const [result1, result2] = await rpcBatch(call1, call2); + expect(result1).toBe(input); + if ('error' in result2) { + throw Error('Return error'); + } + expect(result2.d).toEqual(`${input2.a}`); + expect(result2.c).toEqual(`${input2.b}`); + }); + }); + + describe('Check error', () => { + it('Should throw an error ' + ErrorCodeType.MethodNotFound, async () => { + const input = 1; + expect.assertions(6); + try { + // @ts-ignore + await rpc.IncorrectService.incorrectMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32601); + expect((e as RpcError).message).toBe(ErrorCodeType.MethodNotFound); + } + try { + // @ts-ignore + await rpc.RpcService.incorrectMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32601); + expect((e as RpcError).message).toBe(ErrorCodeType.MethodNotFound); + } + }); + + it('Should throw an error ' + ErrorCodeType.InvalidParams, async () => { + const input = 'llll'; + expect.assertions(3); + try { + // @ts-ignore + await rpc.RpcService.someMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32602); + expect((e as RpcError).message).toBe(ErrorCodeType.InvalidParams); + } + }); + + it('Should throw an error ' + ErrorCodeType.ServerError, async () => { + const input = 5; + expect.assertions(4); + try { + await rpc.RpcService.someMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32099); + expect((e as RpcError).message).toBe(ErrorCodeType.ServerError); + expect((e as RpcError).data.title).toBe('Custom Error'); + } + }); + }); +}); diff --git a/apps/json-api-server-e2e/src/json-api/json-rpc/run-ws-json-rpc.spec.ts b/apps/json-api-server-e2e/src/json-api/json-rpc/run-ws-json-rpc.spec.ts new file mode 100644 index 0000000..09803e1 --- /dev/null +++ b/apps/json-api-server-e2e/src/json-api/json-rpc/run-ws-json-rpc.spec.ts @@ -0,0 +1,109 @@ +import { INestApplication } from '@nestjs/common'; +import { + ResultRpcFactoryPromise, + ErrorCodeType, + RpcError, +} from '@klerick/nestjs-json-rpc-sdk'; + +import { + creatWsRpcSdk, + MapperRpc, + run, + destroySubject, +} from '../utils/run-application'; + +let app: INestApplication; + +beforeAll(async () => { + app = await run(); +}); + +afterAll(async () => { + destroySubject.next(true); + destroySubject.complete(); + await app.close(); +}); + +describe('Run ws json rpc:', () => { + let rpc: ResultRpcFactoryPromise['rpc']; + let rpcBatch: ResultRpcFactoryPromise['rpcBatch']; + let rpcForBatch: ResultRpcFactoryPromise['rpcForBatch']; + beforeEach(() => { + ({ rpc, rpcBatch, rpcForBatch } = creatWsRpcSdk()); + }); + + describe('Should be correct response', () => { + it('Should be call one method', async () => { + const input = 1; + const result = await rpc.RpcService.someMethode(input); + expect(result).toBe(input); + }); + + it('Should be correct response batch', async () => { + const input = 1; + const input2 = { + a: 1, + b: 2, + }; + const call1 = rpcForBatch.RpcService.someMethode(input); + const call2 = rpcForBatch.RpcService.methodeWithObjectParams(input2); + + const [result1, result2] = await rpcBatch(call1, call2); + expect(result1).toBe(input); + if ('error' in result2) { + throw Error('Return error'); + } + expect(result2.d).toEqual(`${input2.a}`); + expect(result2.c).toEqual(`${input2.b}`); + }); + }); + + describe('Check error', () => { + it('Should throw an error ' + ErrorCodeType.MethodNotFound, async () => { + const input = 1; + expect.assertions(6); + try { + // @ts-ignore + await rpc.IncorrectService.incorrectMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32601); + expect((e as RpcError).message).toBe(ErrorCodeType.MethodNotFound); + } + try { + // @ts-ignore + await rpc.RpcService.incorrectMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32601); + expect((e as RpcError).message).toBe(ErrorCodeType.MethodNotFound); + } + }); + + it('Should throw an error ' + ErrorCodeType.InvalidParams, async () => { + const input = 'llll'; + expect.assertions(3); + try { + // @ts-ignore + await rpc.RpcService.someMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32602); + expect((e as RpcError).message).toBe(ErrorCodeType.InvalidParams); + } + }); + + it('Should throw an error ' + ErrorCodeType.ServerError, async () => { + const input = 5; + expect.assertions(4); + try { + await rpc.RpcService.someMethode(input); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32099); + expect((e as RpcError).message).toBe(ErrorCodeType.ServerError); + expect((e as RpcError).data.title).toBe('Custom Error'); + } + }); + }); +}); diff --git a/apps/json-api-server-e2e/src/json-api/utils/run-ppplication.ts b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts similarity index 54% rename from apps/json-api-server-e2e/src/json-api/utils/run-ppplication.ts rename to apps/json-api-server-e2e/src/json-api/utils/run-application.ts index 922bb7a..b4e57ac 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/run-ppplication.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts @@ -1,13 +1,22 @@ import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import { NestFactory } from '@nestjs/core'; import { adapterForAxios, JsonApiJs } from 'json-api-nestjs-sdk'; +import { + RpcFactory, + axiosTransportFactory, + RpcConfig, +} from '@klerick/nestjs-json-rpc-sdk'; +import { RpcService } from '@nestjs-json-api/type-for-rpc'; +import { TransportType } from '@klerick/nestjs-json-rpc-sdk'; import axios from 'axios'; import { Logger } from 'nestjs-pino'; +import { WebSocket } from 'ws'; import { AppModule } from '../../../../json-api-server/src/app/app.module'; import { JsonConfig } from '../../../../../libs/json-api/json-api-nestjs-sdk/src/lib/types'; +import { WsAdapter } from '@nestjs/platform-ws'; +import { Subject } from 'rxjs'; export const axiosAdapter = adapterForAxios(axios); let saveApp: INestApplication; @@ -26,6 +35,7 @@ export const run = async () => { app.useLogger(app.get(Logger)); // const app = await NestFactory.create(AppModule); app.setGlobalPrefix(globalPrefix); + app.useWebSocketAdapter(new WsAdapter(app)); await app.init(); await app.listen(port); @@ -45,3 +55,32 @@ export const creatSdk = (config: Partial = {}) => }, true ); + +export type MapperRpc = { + RpcService: RpcService; +}; + +export const creatRpcSdk = (config: Partial = {}) => + RpcFactory( + { + ...config, + rpcHost: `http://localhost:${port}`, + rpcPath: `/api/rpc`, + transport: TransportType.HTTP, + httpAgentFactory: axiosTransportFactory(axios), + }, + true + ); +export const destroySubject = new Subject(); +export const creatWsRpcSdk = (config: Partial = {}) => + RpcFactory( + { + transport: TransportType.WS, + useWsNativeSocket: true, + nativeSocketImplementation: WebSocket, + rpcHost: `http://localhost:${port}`, + rpcPath: `/rpc`, + destroySubject, + }, + true + ); diff --git a/apps/json-api-server/src/app/app.module.ts b/apps/json-api-server/src/app/app.module.ts index 3d7ea99..4540324 100644 --- a/apps/json-api-server/src/app/app.module.ts +++ b/apps/json-api-server/src/app/app.module.ts @@ -3,12 +3,14 @@ import { LoggerModule } from 'nestjs-pino'; import { DatabaseModule } from 'database'; import { ResourcesModule } from './resources/resources.module'; +import { RpcModule } from './rpc/rpc.module'; import * as process from 'process'; @Module({ imports: [ DatabaseModule, ResourcesModule, + RpcModule, LoggerModule.forRoot({ pinoHttp: { level: process.env['NODE_ENV'] === 'test' ? 'silent' : 'debug', diff --git a/apps/json-api-server/src/app/rpc/rpc.module.ts b/apps/json-api-server/src/app/rpc/rpc.module.ts new file mode 100644 index 0000000..f3a8a99 --- /dev/null +++ b/apps/json-api-server/src/app/rpc/rpc.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; +import { RpcService } from './service/rpc.service'; + +@Module({ + imports: [ + NestjsJsonRpcModule.forRoot({ + path: 'rpc', + transport: TransportType.HTTP, + }), + NestjsJsonRpcModule.forRoot({ + transport: TransportType.WS, + wsConfig: { + path: '/rpc', + cors: { + origin: '*', + }, + }, + }), + ], + providers: [RpcService], +}) +export class RpcModule {} diff --git a/apps/json-api-server/src/app/rpc/service/rpc.service.ts b/apps/json-api-server/src/app/rpc/service/rpc.service.ts new file mode 100644 index 0000000..0e44c56 --- /dev/null +++ b/apps/json-api-server/src/app/rpc/service/rpc.service.ts @@ -0,0 +1,31 @@ +import { + InputType, + OutputType, + RpcService as IRpcService, +} from '@nestjs-json-api/type-for-rpc'; + +import { + createErrorCustomError, + RpcHandler, + RpcParamsPipe, +} from '@klerick/nestjs-json-rpc'; +import { ParseIntPipe } from '@nestjs/common'; + +@RpcHandler() +export class RpcService implements IRpcService { + methodeWithObjectParams(a: InputType): Promise { + return Promise.resolve({ + d: `${a.a}`, + c: `${a.b}`, + }); + } + + someMethode(@RpcParamsPipe(ParseIntPipe) firstArg: number): Promise { + if (firstArg === 5) throw createErrorCustomError(-32099, 'Custom Error'); + return Promise.resolve(firstArg); + } + + someOtherMethode(firstArg: number, secondArgument: number): Promise { + return Promise.resolve(''); + } +} diff --git a/apps/json-api-server/src/main.ts b/apps/json-api-server/src/main.ts index 9fbec19..4f76509 100644 --- a/apps/json-api-server/src/main.ts +++ b/apps/json-api-server/src/main.ts @@ -5,12 +5,14 @@ import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { WsAdapter } from '@nestjs/platform-ws'; import { AppModule } from './app/app.module'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useWebSocketAdapter(new WsAdapter(app)); const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); diff --git a/libs/json-api/json-api-nestjs-sdk/README.md b/libs/json-api/json-api-nestjs-sdk/README.md index 8941763..55ee0cb 100644 --- a/libs/json-api/json-api-nestjs-sdk/README.md +++ b/libs/json-api/json-api-nestjs-sdk/README.md @@ -8,7 +8,7 @@ # json-api-nestjs-sdk -The plugin of Angular for help work with JSON API over [json-api-nestjs](https://www.npmjs.com/package/json-api-nestjs) +The plugin of client for help work with JSON API over [json-api-nestjs](https://www.npmjs.com/package/json-api-nestjs) ## Installation diff --git a/libs/json-api/json-api-nestjs-sdk/project.json b/libs/json-api/json-api-nestjs-sdk/project.json index 6b3576a..6d8102a 100644 --- a/libs/json-api/json-api-nestjs-sdk/project.json +++ b/libs/json-api/json-api-nestjs-sdk/project.json @@ -30,7 +30,7 @@ "commands": [ "node tools/scripts/preparation-hybrid-npm-package.mjs json-api-nestjs-sdk", "node tools/scripts/preparation-npm-package.mjs json-api-nestjs-sdk", - "node tools/scripts/prepare-for-angular.mjs" + "node tools/scripts/prepare-for-angular.mjs json-api-nestjs-sdk json-api-angular" ], "cwd": "./", "parallel": false diff --git a/libs/json-api/json-api-nestjs/CHANGELOG.md b/libs/json-api/json-api-nestjs/CHANGELOG.md index 9017d97..a9b78b3 100644 --- a/libs/json-api/json-api-nestjs/CHANGELOG.md +++ b/libs/json-api/json-api-nestjs/CHANGELOG.md @@ -1,3 +1,15 @@ +## 7.0.1 (2024-04-06) + + +### 🩹 Fixes + +- **json-api-nestjs:** Fix validation ([1d048a8](https://github.com/klerick/nestjs-json-api/commit/1d048a8)) + + +### ❤️ Thank You + +- Alex H + # 7.0.0 (2024-03-08) diff --git a/libs/json-api/json-api-nestjs/package.json b/libs/json-api/json-api-nestjs/package.json index 57ec8e7..f788582 100644 --- a/libs/json-api/json-api-nestjs/package.json +++ b/libs/json-api/json-api-nestjs/package.json @@ -1,6 +1,6 @@ { "name": "json-api-nestjs", - "version": "7.0.0", + "version": "7.0.1", "engines": { "node": ">= 16.0.0" }, diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts index 69db0fc..0a04e1c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts @@ -220,6 +220,14 @@ describe('Check "filter" zod schema', () => { }, relation: null, }; + const check9: FilterQuerySchema = { + target: { + createdAt: { + eq: 'null', + }, + }, + relation: null, + }; const checkArray = [ check1, @@ -235,6 +243,10 @@ describe('Check "filter" zod schema', () => { const result = filterQuerySchema.parse(check); expect(result).toEqual(check); } + const result = filterQuerySchema.parse(check9); + expect(result.target!.createdAt!.eq).toEqual(null); + result.target!.createdAt!.eq = 'null'; + expect(result).toEqual(check9); }); it('Invalid schema', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts index fec4b03..99f9c7c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts +++ b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts @@ -2,10 +2,12 @@ import { z, ZodArray, ZodEffects, + ZodLiteral, ZodNullable, ZodObject, ZodOptional, ZodString, + ZodUnion, } from 'zod'; import { arrayItemStringLongerThan, @@ -41,15 +43,21 @@ import { ZodFilterRelationSchema, } from '../zod-input-query-schema/filter'; -type ZodForString = ZodEffects; -const zodForString: ZodForString = z.string().refine(stringLongerThan(), { - message: 'String should be not empty', -}); +type ZodForString = ZodUnion< + [ZodEffects, null, 'null'>, ZodEffects] +>; + +const zodForString: ZodForString = z.union([ + z.literal('null').transform(() => null), + z.string().refine(stringLongerThan(), { + message: 'String should be not empty', + }), +]); type ZodForStringArray = ZodEffects< ZodArray, - string[], - string[] + [string | null, ...(string | null)[]], + [string, ...string[]] >; const zodForStringArray: ZodForStringArray = zodForString .array() diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts index eec27e4..f265e25 100644 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts @@ -14,14 +14,15 @@ export const stringLongerThan = export const arrayItemStringLongerThan = (length = 0) => - (array: [string, ...string[]]) => { + (array: [string | null, ...(string | null)[]]) => { const checkFunction = stringLongerThan(length); - return !array.some((i) => !checkFunction(i)); + return !array.some((i) => i !== null && !checkFunction(i)); }; export const stringMustBe = (type: TypeField = TypeField.string) => - (inputString: string) => { + (inputString: string | null) => { + if (inputString === null) return true; switch (type) { case TypeField.boolean: return inputString === 'true' || inputString === 'false'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts index 2777d32..96aa3b5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts @@ -26,7 +26,6 @@ import { } from '../../constants'; import { TypeormUtilsService } from './typeorm-utils.service'; import { - ObjectTyped, PostData, PostRelationshipData, Query, @@ -253,7 +252,7 @@ describe('TypeormUtilsService', () => { `test for ${filterOperand}`; const valueTestArray = ( filterOperand: FilterOperand.nin | FilterOperand.in - ) => [valueTest(filterOperand)]; + ): [string, ...string[]] => [valueTest(filterOperand)]; const query = getDefaultQuery(); query.filter.target = { @@ -429,9 +428,10 @@ describe('TypeormUtilsService', () => { createdAt: { [FilterOperand.eq]: 'test1', [FilterOperand.ne]: 'test2', - [FilterOperand.nin]: ['test3'], + [FilterOperand.nin]: ['test3'] as [string, ...string[]], }, }; + query.filter.relation = { roles: conditional, }; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/.eslintrc.json b/libs/json-rpc/nestjs-json-rpc-sdk/.eslintrc.json new file mode 100644 index 0000000..c9748d2 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/README.md b/libs/json-rpc/nestjs-json-rpc-sdk/README.md new file mode 100644 index 0000000..7df1511 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/README.md @@ -0,0 +1,286 @@ +

+ NPM Version + Package License + NPM Downloads + Commitizen friendly + Coverage Badge +

+ +# nestjs-json-rpc-sdk + +The plugin of client for help work with JSON-ROC over [nestjs-json-rpc](https://www.npmjs.com/package/@klerick/nestjs-json-rpc) +Work with RPC look like call function + +## Installation + +```bash $ +npm install @klerick/nestjs-json-rpc-sdk +``` + +## Example + +Once the installation process is complete, we can import the **RpcFactory**. +For example, we have RPC server which have service which implement this interface: + +```typescript +export type InputType = { + a: number; + b: number; +}; + +export type OutputType = { + c: string; + d: string; +}; + +export interface RpcService { + someMethod(firstArg: number): Promise; + someOtherMethod(firstArg: number, secondArgument: number): Promise; + methodWithObjectParams(a: InputType): Promise; +} +``` + +```typescript +import { + RpcFactory, +} from '@klerick/nestjs-json-rpc-sdk'; +const { rpc, rpcBatch } = RpcFactory( + { + rpcHost: `http://localhost:${port}`, + rpcPath: `/api/rpc`, + transport: TransportType.HTTP, + }, + false +); + +rpc.RpcService.someMethod(1).sibcribe(r => console.log(r)) + +const call1 = rpcForBatch.RpcService.someMethod(1); +const call2 = rpcForBatch.RpcService.methodWithObjectParams({ + a: 1, + b: 2, +}); + +rpcBatch(call1, call2).sibcribe(([result1, result2]) => console.log(result1, result2)) + +``` +That's all:) + +You can use typescript for type checking: +```typescript +import { + RpcFactory, +} from '@klerick/nestjs-json-rpc-sdk'; + + + +type MapperRpc = { + RpcService: RpcService; +}; + +const { rpc, rpcBatch } = RpcFactory( + { + rpcHost: `http://localhost:${port}`, + rpcPath: `/api/rpc`, + transport: TransportType.HTTP, + }, + false +); +//TS2345: Argument of type string is not assignable to parameter of type number +const call = rpc.RpcService.someMethod('inccorectParam'); +//TS2339: Property IncorrectService does not exist on type MapperRpc +const call2 = rpc.IncorrectService.someMethod(1); +//TS2339: Property incorrectMethod does not exist on type RpcService +const call3 = rpc.RpcService.incorrectMethod(1); + +``` + + +By default, HTTP transport using fetch, but you can set other: + +```typescript +import axios from 'axios'; +import { + RpcFactory, + axiosTransportFactory, +} from '@klerick/nestjs-json-rpc-sdk'; + +const { rpc, rpcBatch } = RpcFactory( + { + rpcHost: `http://localhost:4200`, + rpcPath: `/api/rpc`, + transport: TransportType.HTTP, + httpAgentFactory: axiosTransportFactory(axios), + }, + false +); +``` +Or you can implement your personal factory. + +You should implement **HttpAgentFactory** type + +```typescript + +type Transport = ( + body: PayloadRpc +) => Observable>; + +type HttpAgentFactory = ( + url: string +) => Transport; +``` + + + +if you want to use **Promise** instead of **Observer** + +***!!!!***: - you need to use another object for prepare rpc batch call +```typescript +import axios from 'axios'; +import { + RpcFactory, + axiosTransportFactory, +} from '@klerick/nestjs-json-rpc-sdk'; + +const { rpc, rpcBatch, rpcForBatch } = RpcFactory( + { + rpcHost: `http://localhost:4200`, + rpcPath: `/api/rpc`, + transport: TransportType.HTTP, + httpAgentFactory: axiosTransportFactory(axios), + }, + true // need true for use promise as result +); +const result = await rpc.RpcService.someMethod(1) + +const call1 = rpcForBatch.RpcService.someMethod(1); +const call2 = rpcForBatch.RpcService.methodWithObjectParams({ + a: 1, + b: 2, +}); + +const [result1, result2] = await rpcBatch(call1, call2); +``` + +For use **WebSocket** +```typescript +import { + RpcFactory, +} from '@klerick/nestjs-json-rpc-sdk'; +import { WebSocket as ws } from 'ws'; +import { webSocket } from 'rxjs/webSocket'; + +const someUrl = 'ws://localhost:4200/rpc' +const destroySubject = new Subject(); +const nativeSocketInstance = webSocket(destroySubject); + +const { rpc, rpcBatch } = RpcFactory( + { + transport: TransportType.WS, + useWsNativeSocket: true, // - Will be use native WebSocket + //nativeSocketImplementation: ws, - if you use NodeJS you can use other implementation + rpcHost: `http://localhost:4200`, + rpcPath: `/rpc`, + destroySubject, // - If you need close connection you need call destroySubject.next(true), + //nativeSocketInstance - you can use your owner socket instance + }, + false +); +``` +You can use **socket.io** +```typescript +import { + RpcFactory, +} from '@klerick/nestjs-json-rpc-sdk'; + +import { io } from 'socket.io-client'; + +const someUrl = 'ws://localhost:4200' +const destroySubject = new Subject(); +const ioSocketInstance = io(someUrl, { path: '/rpc' }) +const { rpc, rpcBatch } = RpcFactory( + { + transport: TransportType.WS, + useWsNativeSocket: false, // - Will be use socket.io + destroySubject, // - If you need close connection you need call destroySubject.next(true), + ioSocketInstance + }, + false +); +``` + +You can use Angular module: + +```typescript + +import { + JsonRpcAngular, + JsonRpcAngularConfig, + TransportType, +} from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module' +import { Subject } from 'rxjs'; +import { io } from 'socket.io-client'; +import { + JSON_RPC, + RPC_BATCH, + Rpc, +} from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module'; + +@Component({ + standalone: true, + selector: 'nestjs-json-api-root', + templateUrl: './app.component.html', + styleUrl: './app.component.css', +}) +export class AppComponent { + private rpc = inject>(JSON_RPC); + private rpcBatch = inject(RPC_BATCH); +} + +const destroySubjectToken = new InjectionToken('destroySubjectToken', { + factory: () => new Subject(), +}); + +const tokenSocketInst = new InjectionToken('tokenSocketInst', { + factory: () => webSocket('ws://localhost:4200/rpc'), +}); +const tokenIoSocketInst = new InjectionToken('tokenIoSocketInst', { + factory: () => io('http://localhost:4200', { path: '/rpc' }), +}); + +const httpConfig: JsonRpcAngularConfig = { + transport: TransportType.HTTP, + rpcPath: '/api/rpc', + rpcHost: 'http://localhost:4200', +}; +const wsConfig: JsonRpcAngularConfig = { + transport: TransportType.WS, + useWsNativeSocket: true, + rpcPath: 'rpc', + rpcHost: 'ws://localhost:4200', + destroySubjectToken, +}; +const wsConfigWithToken: JsonRpcAngularConfig = { + transport: TransportType.WS, + useWsNativeSocket: true, + tokenSocketInst, + destroySubjectToken, +}; +const ioConfig: JsonRpcAngularConfig = { + transport: TransportType.WS, + useWsNativeSocket: false, + destroySubjectToken, + tokenSocketInst: tokenIoSocketInst, +}; + +bootstrapApplication(AppComponent, { + providers: [ + importProvidersFrom( + JsonRpcAngular.forRoot(httpConfig) + ), + ], +}).catch((err) => + console.error(err) +); + +``` diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/jest.config.ts b/libs/json-rpc/nestjs-json-rpc-sdk/jest.config.ts new file mode 100644 index 0000000..3ea00a7 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'nestjs-json-rpc-sdk', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/json-rpc/nestjs-json-rpc-sdk', +}; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/ng-package.json b/libs/json-rpc/nestjs-json-rpc-sdk/ng-package.json new file mode 100644 index 0000000..e91ee02 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../tmp/angular-lib/nestjs-json-rpc-sdk", + "lib": { + "entryFile": "../../index.ts" + } +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/package.json b/libs/json-rpc/nestjs-json-rpc-sdk/package.json new file mode 100644 index 0000000..86da19e --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/package.json @@ -0,0 +1,30 @@ +{ + "name": "@klerick/nestjs-json-rpc-sdk", + "version": "0.0.1", + "engines": { + "node": ">= 16.0.0" + }, + "description": "Client for RPS server, which use @klerick/nestjs-json-rpc", + "contributors": [ + { + "email": "klerick666@gmain.com", + "name": "Aleksandr Kharkovey" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/klerick/nestjs-json-api.git" + }, + "private": false, + "license": "MIT", + "main": "./src/index.js", + "files": [ + "**/*" + ], + "keywords": [ + "nestjs", + "nest", + "RPC", + "JSON-RPC" + ] +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/project.json b/libs/json-rpc/nestjs-json-rpc-sdk/project.json new file mode 100644 index 0000000..98f4a9a --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/project.json @@ -0,0 +1,102 @@ +{ + "name": "nestjs-json-rpc-sdk", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/json-rpc/nestjs-json-rpc-sdk/src", + "projectType": "library", + "targets": { + "compile-for-angular": { + "executor": "@nx/angular:package", + "outputs": ["{workspaceRoot}/dist/{projectRoot}"], + "options": { + "project": "libs/json-rpc/nestjs-json-rpc-sdk/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/json-rpc/nestjs-json-rpc-sdk/tsconfig-mjs.lib.json" + }, + "development": { + "tsConfig": "libs/json-rpc/nestjs-json-rpc-sdk/tsconfig-mjs.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "nx:run-commands", + "dependsOn": [ + "build-cjs" + ], + "options": { + "outputPath": "dist/libs/json-rpc/nestjs-json-rpc-sdk", + "commands": [ + "node tools/scripts/preparation-hybrid-npm-package.mjs nestjs-json-rpc-sdk", + "node tools/scripts/preparation-npm-package.mjs nestjs-json-rpc-sdk", + "node tools/scripts/prepare-for-angular.mjs nestjs-json-rpc-sdk json-rpc-angular", + "cp tmp/angular-lib/nestjs-json-rpc-sdk/esm2022/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/json-rpc-angular.module.mjs dist/libs/json-rpc/nestjs-json-rpc-sdk/mjs/src/lib/angular/json-rpc-angular.module.js" + ], + "cwd": "./", + "parallel": false + } + }, + "build-cjs": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "dependsOn": [ + "build-mjs" + ], + "options": { + "outputPath": "dist/libs/json-rpc/nestjs-json-rpc-sdk/cjs", + "main": "libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts", + "tsConfig": "libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json", + "assets": ["libs/json-rpc/nestjs-json-rpc-sdk/*.md"], + "external": "none", + "updateBuildableProjectDepsInPackageJson": true, + "buildableProjectDepsInPackageJsonType": "peerDependencies", + "additionalEntryPoints": [ + "libs/json-rpc/nestjs-json-rpc-sdk/src/json-rpc-sdk.module.ts" + ], + "generateExportsField": true + } + }, + "build-mjs": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/json-rpc/nestjs-json-rpc-sdk/mjs", + "main": "libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts", + "tsConfig": "libs/json-rpc/nestjs-json-rpc-sdk/tsconfig-mjs.lib.json", + "assets": ["libs/json-rpc/nestjs-json-rpc-sdk/*.md"], + "external": "none", + "updateBuildableProjectDepsInPackageJson": true, + "buildableProjectDepsInPackageJsonType": "peerDependencies", + "additionalEntryPoints": [ + "libs/json-rpc/nestjs-json-rpc-sdk/src/json-rpc-sdk.module.ts" + ], + "generateExportsField": true + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs nestjs-json-rpc-sdk {args.ver} {args.tag}", + "dependsOn": ["build"] + }, + "upload-badge": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "target": "test" + } + ], + "options": { + "outputPath": "{workspaceRoot}/libs/json-rpc/nestjs-json-rpc-sdk", + "commands": ["node tools/scripts/upload-badge.mjs nestjs-json-rpc-sdk"], + "cwd": "./", + "parallel": false + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/libs/json-rpc/nestjs-json-rpc-sdk" + } + } + }, + "tags": [] +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts new file mode 100644 index 0000000..a4a0891 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts @@ -0,0 +1,17 @@ +export { + axiosTransportFactory, + RpcFactory, + ResultRpcFactoryPromise, + ResultRpcFactory, +} from './lib/factory'; +export { + RpcConfig, + TransportType, + ErrorCodeType, + RpcError, + LoopFunc, + HttpAgentFactory, + Transport, + PayloadRpc, + RpcResult, +} from './lib/types'; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/json-rpc-sdk.module.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/json-rpc-sdk.module.ts new file mode 100644 index 0000000..f3d4ea8 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/json-rpc-sdk.module.ts @@ -0,0 +1 @@ +export * from './lib/json-rpc-angular'; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/factory.ts new file mode 100644 index 0000000..7becf8d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/factory.ts @@ -0,0 +1,94 @@ +import { inject, InjectionToken } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Subject } from 'rxjs'; +import { WebSocketSubject } from 'rxjs/internal/observable/dom/WebSocketSubject'; +import { Socket } from 'socket.io-client'; + +import { + LoopFunc, + PayloadRpc, + RpcResult, + RpcReturnList, + RpcConfig, + TransportType, +} from '../types'; +import { transportFactory } from '../factory'; +import { webSocketFactory, WsResponse } from '../factory/ws-transport.factory'; + +import { JSON_RPC_SDK_CONFIG, JSON_RPC_SDK_TRANSPORT } from './tokens'; +import { RpcBatchFactory, rpcProxy } from '../utils'; + +export function rpcBatchFactory() { + return RpcBatchFactory(inject(JSON_RPC_SDK_TRANSPORT)); +} + +export function rpcFactory() { + return rpcProxy>( + inject(JSON_RPC_SDK_TRANSPORT), + false + ); +} + +export function angularTransportFactory() { + const angularConfig = inject(JSON_RPC_SDK_CONFIG); + const httpClient = inject(HttpClient); + + if (angularConfig.transport === TransportType.HTTP) { + const rpcConfig: RpcConfig = { + transport: angularConfig.transport, + httpAgentFactory: (url: string) => (body: PayloadRpc) => + httpClient.post>(url, body), + rpcPath: angularConfig.rpcPath, + rpcHost: angularConfig.rpcHost, + }; + return transportFactory(rpcConfig); + } + + const destroySubject = + (angularConfig.destroySubjectToken && + inject>(angularConfig.destroySubjectToken, { + optional: true, + })) || + new Subject(); + + if (angularConfig.useWsNativeSocket) { + let socketInst: + | WebSocketSubject | RpcResult>> + | undefined = undefined; + if ('tokenSocketInst' in angularConfig) { + socketInst = + inject< + WebSocketSubject< + WsResponse | RpcResult> + > + >(angularConfig['tokenSocketInst'], { optional: true }) || undefined; + } else { + const url = new URL( + angularConfig.rpcPath, + angularConfig.rpcHost + ).toString(); + socketInst = webSocketFactory( + url, + angularConfig.nativeSocketImplementation + ); + } + + if (socketInst === undefined) throw new Error('Cant create socket inst'); + const rpcConfig: RpcConfig = { + transport: angularConfig.transport, + useWsNativeSocket: angularConfig.useWsNativeSocket, + nativeSocketInstance: socketInst, + destroySubject, + }; + + return transportFactory(rpcConfig); + } + const ioSocketInstance = inject(angularConfig['tokenSocketInst']); + const rpcConfig: RpcConfig = { + transport: angularConfig.transport, + useWsNativeSocket: angularConfig.useWsNativeSocket, + ioSocketInstance: ioSocketInstance, + destroySubject, + }; + return transportFactory(rpcConfig); +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/json-rpc-angular.module.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/json-rpc-angular.module.ts new file mode 100644 index 0000000..3fe46b9 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/json-rpc-angular.module.ts @@ -0,0 +1,24 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; + +import { JSON_RPC_SDK_CONFIG } from './tokens'; +import { JsonRpcAngularConfig } from '../types'; + +@NgModule({ + imports: [HttpClientModule], +}) +export class JsonRpcAngular { + static forRoot( + config: JsonRpcAngularConfig + ): ModuleWithProviders { + return { + ngModule: JsonRpcAngular, + providers: [ + { + useValue: config, + provide: JSON_RPC_SDK_CONFIG, + }, + ], + }; + } +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/tokens.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/tokens.ts new file mode 100644 index 0000000..0f2b07a --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/tokens.ts @@ -0,0 +1,36 @@ +import { InjectionToken } from '@angular/core'; +import { + LoopFunc, + RpcBatch, + RpcReturnList, + Transport, + JsonRpcAngularConfig, +} from '../types'; + +import { + angularTransportFactory, + rpcBatchFactory, + rpcFactory, +} from './factory'; + +export const JSON_RPC_SDK_CONFIG = new InjectionToken( + 'Main config object for sdk' +); + +export const JSON_RPC_SDK_TRANSPORT = new InjectionToken>( + 'Transport for RPC', + { + factory: angularTransportFactory, + } +); + +export const JSON_RPC = new InjectionToken>( + 'Rpc client', + { + factory: rpcFactory, + } +); + +export const RPC_BATCH = new InjectionToken('Rpc client for batch', { + factory: rpcBatchFactory, +}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts new file mode 100644 index 0000000..2b6782b --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts @@ -0,0 +1,2 @@ +export const JSON_RPC_VERSION = '2.0'; +export const WS_EVENT_NAME = 'rpc'; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/axios-transport.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/axios-transport.factory.ts new file mode 100644 index 0000000..451edb7 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/axios-transport.factory.ts @@ -0,0 +1,34 @@ +import { Axios, AxiosResponse } from 'axios'; +import { Observable } from 'rxjs'; + +import { + HttpAgentFactory, + LoopFunc, + PayloadRpc, + ReturnTransportCall, + RpcResult, +} from '../types'; +import { map } from 'rxjs/operators'; + +export function axiosTransportFactory( + axios: Axios +): HttpAgentFactory { + return (url: string) => (body: PayloadRpc) => { + const controller = new AbortController(); + const signal = controller.signal; + + return new Observable>>((subscriber) => { + axios + .post< + ReturnTransportCall, + AxiosResponse, PayloadRpc>, + PayloadRpc + >(url, body, { signal }) + .then((response) => subscriber.next(response)) + .catch((error: unknown) => subscriber.error(error)) + .finally(() => subscriber.complete()); + + return { unsubscribe: () => controller.abort() }; + }).pipe(map((r) => r.data)); + }; +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/fetch-transport.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/fetch-transport.factory.ts new file mode 100644 index 0000000..2b5307e --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/fetch-transport.factory.ts @@ -0,0 +1,13 @@ +import { fromFetch } from 'rxjs/fetch'; +import { LoopFunc, PayloadRpc, RpcResult, Transport } from '../types'; + +export function fetchTransportFactory( + url: string +): Transport { + return (body: PayloadRpc) => + fromFetch>(url, { + method: 'post', + body: JSON.stringify(body), + selector: (r) => r.json(), + }); +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.spec.ts new file mode 100644 index 0000000..b1f5597 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.spec.ts @@ -0,0 +1,9 @@ +import { idRequest } from './id-request'; + +describe('id-request', () => { + it('should be increment', () => { + expect(idRequest()).toBe(1); + expect(idRequest()).toBe(2); + expect(idRequest()).toBe(3); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.ts new file mode 100644 index 0000000..c43bbef --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.ts @@ -0,0 +1,2 @@ +let i = 0; +export const idRequest = () => ++i; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/index.ts new file mode 100644 index 0000000..b44a48a --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/index.ts @@ -0,0 +1,6 @@ +export * from './axios-transport.factory'; +export * from './id-request'; +export * from './rpc.factory'; +export * from './transport.factory'; +export * from './fetch-transport.factory'; +export * from './io-transport.factory'; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/io-transport.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/io-transport.factory.ts new file mode 100644 index 0000000..c6d772c --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/io-transport.factory.ts @@ -0,0 +1,77 @@ +import type { Socket } from 'socket.io-client'; +import { + filter, + Observable, + Observer, + of, + Subject, + Subscription, + switchMap, + take, + takeUntil, + tap, +} from 'rxjs'; +import { Subscriber } from 'rxjs/internal/Subscriber'; +import { TeardownLogic } from 'rxjs/internal/types'; + +import { LoopFunc, PayloadRpc, RpcResult, Transport } from '../types'; +import { WS_EVENT_NAME } from '../constans'; + +interface ServerToClientEvents { + rpc: (result: RpcResult) => void; +} + +interface ClientToServerEvents { + rpc: (payload: PayloadRpc) => void; +} + +class SocketIo extends Observable> { + private messageQueue: PayloadRpc[] = []; + constructor( + private io: Socket, ClientToServerEvents> + ) { + super((subscriber) => this.subscribeForObservable(subscriber)); + this.io.on('connect', () => { + while (this.messageQueue.length > 0) { + const msg = this.messageQueue.shift(); + if (!msg) break; + this.io.emit(WS_EVENT_NAME, msg); + } + }); + } + + private subscribeForObservable( + subscriber: Subscriber> + ): TeardownLogic { + this.io.on(WS_EVENT_NAME, (value) => subscriber.next(value)); + this.io.on('connect_error', (error: Error) => subscriber.error(error)); + this.io.on('disconnect', () => subscriber.complete()); + return { unsubscribe: () => this.io.close() }; + } + + public next(message: PayloadRpc): void { + if (!this.io.connected) { + this.messageQueue.push(message); + return; + } + + this.io.emit(WS_EVENT_NAME, message); + } +} + +export function ioTransportFactory( + io: Socket, ClientToServerEvents>, + destroyFactory: Subject +): Transport { + const socketSubject = new SocketIo(io).pipe(takeUntil(destroyFactory)); + return (body: PayloadRpc) => { + const { id } = body; + return of(true).pipe( + tap(() => io.emit(WS_EVENT_NAME, body)), + switchMap(() => + socketSubject.pipe(filter((response) => response.id === id)) + ), + take(1) + ); + }; +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/rpc.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/rpc.factory.ts new file mode 100644 index 0000000..1499512 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/rpc.factory.ts @@ -0,0 +1,39 @@ +import { RpcConfig, RpcReturnList, RpcBatch, RpcBatchPromise } from '../types'; +import { transportFactory } from './transport.factory'; +import { RpcBatchFactory, rpcProxy, RpcBatchFactoryPromise } from '../utils'; + +export type ResultRpcFactory = { + rpc: RpcReturnList; + rpcBatch: RpcBatch; +}; +export type ResultRpcFactoryPromise = { + rpc: RpcReturnList; + rpcForBatch: RpcReturnList; + rpcBatch: RpcBatchPromise; +}; + +export function RpcFactory( + options: RpcConfig, + usePromise: false +): ResultRpcFactory; +export function RpcFactory( + options: RpcConfig, + usePromise: true +): ResultRpcFactoryPromise; +export function RpcFactory( + options: RpcConfig, + usePromise: true | false = false +): ResultRpcFactory | ResultRpcFactoryPromise { + const transport = transportFactory(options); + let rpc: RpcReturnList | RpcReturnList; + let rpcForBatch: RpcReturnList; + + if (usePromise) { + rpc = rpcProxy>(transport, usePromise); + rpcForBatch = rpcProxy>(transport, false); + return { rpc, rpcForBatch, rpcBatch: RpcBatchFactoryPromise(transport) }; + } else { + rpc = rpcProxy>(transport, usePromise); + return { rpc, rpcBatch: RpcBatchFactory(transport) }; + } +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/transport.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/transport.factory.ts new file mode 100644 index 0000000..a30e443 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/transport.factory.ts @@ -0,0 +1,68 @@ +import { + RpcConfig, + Transport, + TransportType, + RpcHttpConfig, + RpcWsConfig, + LoopFunc, + PayloadRpc, + RpcResult, +} from '../types'; +import { fetchTransportFactory } from './fetch-transport.factory'; +import { + webSocketFactory, + WsResponse, + wsTransportFactory, +} from './ws-transport.factory'; +import { ioTransportFactory } from './io-transport.factory'; +import { Subject } from 'rxjs'; +import { WebSocketSubject } from 'rxjs/internal/observable/dom/WebSocketSubject'; + +function httpTransport( + config: RpcHttpConfig +): Transport { + const url = new URL(config.rpcPath, config.rpcHost).toString(); + if (config.httpAgentFactory) { + return config.httpAgentFactory(url); + } + + return fetchTransportFactory(url); +} + +function wsTransport(config: RpcWsConfig): Transport { + const destroyFactory = config.destroySubject || new Subject(); + if (config.useWsNativeSocket) { + let nativeSocketInstance: WebSocketSubject< + WsResponse | RpcResult> + >; + if ('nativeSocketInstance' in config) { + nativeSocketInstance = config.nativeSocketInstance; + } else { + const url = new URL(config.rpcPath, config.rpcHost).toString(); + nativeSocketInstance = webSocketFactory( + url, + config.nativeSocketImplementation + ); + } + + return wsTransportFactory( + nativeSocketInstance, + config.destroySubject || new Subject() + ); + } + + return ioTransportFactory(config.ioSocketInstance, destroyFactory); +} + +export function transportFactory( + rpcConfig: RpcConfig +): Transport { + switch (rpcConfig.transport) { + case TransportType.HTTP: + return httpTransport(rpcConfig); + case TransportType.WS: + return wsTransport(rpcConfig); + default: + throw new Error('Unknown transport'); + } +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/ws-transport.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/ws-transport.factory.ts new file mode 100644 index 0000000..0655404 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/ws-transport.factory.ts @@ -0,0 +1,56 @@ +import { filter, of, Subject, switchMap, take, takeUntil, tap } from 'rxjs'; +import { webSocket } from 'rxjs/webSocket'; +import { map } from 'rxjs/operators'; +import { WS_EVENT_NAME } from '../constans'; +import { LoopFunc, PayloadRpc, RpcResult, Transport, WsEvent } from '../types'; +import { WebSocketSubject } from 'rxjs/internal/observable/dom/WebSocketSubject'; + +export interface WsResponse { + event: WsEvent; + data: T; +} + +export function webSocketFactory( + url: string, + webSocketCtor?: any +): WebSocketSubject | RpcResult>> { + return webSocket | RpcResult>>({ + url, + ...(webSocketCtor ? { WebSocketCtor: webSocketCtor } : {}), + }); +} + +export function wsTransportFactory( + subject: WebSocketSubject< + WsResponse | RpcResult> + >, + destroyFactory: Subject +): Transport { + const subjectData = new Subject>(); + subject + .pipe( + filter((response): response is WsResponse> => { + if (typeof response !== 'object' || response === null) return false; + return 'event' in response && response['event'] === 'rpc'; + }), + map((response) => response.data), + takeUntil(destroyFactory) + ) + .subscribe((r) => subjectData.next(r)); + + return (body: PayloadRpc) => { + const { id } = body; + return of(true).pipe( + tap(() => + subject.next({ + event: WS_EVENT_NAME, + data: body, + }) + ), + switchMap(() => + subjectData.pipe(filter((response) => response.id === id)) + ), + take(1) + ); + }; +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/json-rpc-angular.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/json-rpc-angular.ts new file mode 100644 index 0000000..9d185b8 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/json-rpc-angular.ts @@ -0,0 +1,3 @@ +export { JsonRpcAngular } from './angular/json-rpc-angular.module'; +export { JsonRpcAngularConfig, TransportType, Rpc } from './types'; +export { JSON_RPC, RPC_BATCH } from './angular/tokens'; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/angular-type.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/angular-type.ts new file mode 100644 index 0000000..e7ed7f5 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/angular-type.ts @@ -0,0 +1,28 @@ +import { + RpcMainHttpConfig, + RpcNativeSocketFactory, + RpcNativeSocketFalse, + RpcNativeSocketTrue, + TransportType, +} from './config'; +import { RpcReturnList } from './rpc'; + +type RpcWsMainConfig = { + transport: TransportType.WS; + destroySubjectToken?: any; +}; + +type RpcTokenForWs = { + tokenSocketInst: any; +}; + +type RpcNativeConfig = RpcNativeSocketTrue & + (RpcNativeSocketFactory | RpcTokenForWs); + +type RpcIoConfig = RpcNativeSocketFalse & RpcTokenForWs; + +type RpcAngularWsConfig = RpcWsMainConfig & (RpcNativeConfig | RpcIoConfig); + +export type JsonRpcAngularConfig = RpcMainHttpConfig | RpcAngularWsConfig; + +export type Rpc = RpcReturnList; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/config.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/config.ts new file mode 100644 index 0000000..c75a43d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/config.ts @@ -0,0 +1,61 @@ +import { Transport } from './rpc'; +import { HttpAgentFactory, LoopFunc } from './utils'; + +import type { Socket } from 'socket.io-client'; +import { Subject } from 'rxjs'; +import { WebSocketSubject } from 'rxjs/internal/observable/dom/WebSocketSubject'; + +export enum TransportType { + HTTP, + WS, +} + +export type RpcMainHttpConfig = { + transport: TransportType.HTTP; + rpcPath: string; + rpcHost: string; +}; + +export type RpcTransportHttpConfig = { + httpAgentFactory?: HttpAgentFactory>; +}; + +export type RpcHttpConfig = RpcMainHttpConfig & RpcTransportHttpConfig; + +export type RpcNativeSocketFactory = { + rpcPath: string; + rpcHost: string; + nativeSocketImplementation?: { + new (url: string, protocols?: string | string[]): any; + }; +}; + +export type RpcNativeSocketInstance = { + nativeSocketInstance: WebSocketSubject; +}; + +export type RpcNativeSocketTrue = { + useWsNativeSocket: true; +}; + +export type RpcNativeSocketFalse = { + useWsNativeSocket: false; +}; + +export type RpcNativeSocket = RpcNativeSocketTrue & + (RpcNativeSocketFactory | RpcNativeSocketInstance); + +export type RpcWsMainConfig = { + transport: TransportType.WS; + destroySubject?: Subject; +}; + +export type RpcIoSocketInstance = { + ioSocketInstance: Socket; +}; + +export type RpcIoSocket = RpcNativeSocketFalse & RpcIoSocketInstance; + +export type RpcWsConfig = RpcWsMainConfig & (RpcNativeSocket | RpcIoSocket); + +export type RpcConfig = RpcHttpConfig | RpcWsConfig; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/index.ts new file mode 100644 index 0000000..5abc258 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/index.ts @@ -0,0 +1,5 @@ +export * from './config'; +export * from './rpc'; +export * from './rpc-error-object'; +export * from './utils'; +export * from './angular-type'; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc-error-object.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc-error-object.ts new file mode 100644 index 0000000..a1c1f30 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc-error-object.ts @@ -0,0 +1,40 @@ +import { JsonRpcVersion } from './rpc'; + +export enum ErrorCodeType { + ParseError = 'Parse error', + InvalidRequest = 'Invalid request', + MethodNotFound = 'Method not found', + InvalidParams = 'Invalid params', + InternalError = 'Internal error', + ServerError = 'Server error', +} + +export type RpcErrorObject = { + jsonrpc: JsonRpcVersion; + error: { + message: ErrorCodeType | string; + code: number; + data?: { + title: string; + description: string; + }; + }; + id: null | number; +}; + +export class RpcError extends Error { + data!: { + title: string; + description: string; + }; + code!: number; + id: null | number = null; + constructor(rpcError: RpcErrorObject) { + super(rpcError.error.message); + this.id = rpcError.id; + this.code = rpcError.error.code; + if (rpcError.error.data) { + this.data = rpcError.error.data; + } + } +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc.ts new file mode 100644 index 0000000..98792a2 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc.ts @@ -0,0 +1,75 @@ +import { Observable } from 'rxjs'; +import { RpcErrorObject } from './rpc-error-object'; +import { LoopFunc, ReturnGenericType } from './utils'; + +export type JsonRpcVersion = '2.0'; + +export type WsEvent = 'rpc'; + +export type PayloadRpc = { + jsonrpc: JsonRpcVersion; + method: string; + params: Parameters; + id: number; +}; + +export type RpcResultObject = { + jsonrpc: JsonRpcVersion; + result: ReturnGenericType; + id: number; +}; + +export type RpcResult = RpcErrorObject | RpcResultObject; + +export type ReturnTransportCall = + | ReturnGenericType + | RpcErrorObject; + +export type Transport = ( + body: PayloadRpc +) => Observable>; + +export type RpcReturnList = { + [K in keyof R]: RpcCallReturnChange; +}; + +export interface WrapperCallRpc extends Observable { + nameSpace: string; + method: string; + arg: P; + id: number; + body: { + jsonrpc: JsonRpcVersion; + method: string; + params: P; + id: number; + }; +} + +type CallFunction = T extends (...args: infer Z) => any + ? ( + ...arg: Z + ) => P extends false + ? WrapperCallRpc, Parameters> + : Promise> + : never; + +export type RpcCallReturnChange = { + [K in keyof R]: R[K] extends LoopFunc ? CallFunction : never; +}; + +export type OutputData = { + [K in keyof T]: T[K] extends WrapperCallRpc + ? O | RpcErrorObject + : never; +}; + +export type RpcBatch = []>( + ...arg: readonly [...A] +) => Observable>; + +export type RpcBatchPromise = < + A extends readonly WrapperCallRpc[] +>( + ...arg: readonly [...A] +) => Promise>; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/utils.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/utils.ts new file mode 100644 index 0000000..6a108d0 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/utils.ts @@ -0,0 +1,10 @@ +import { Transport } from './rpc'; + +export type LoopFunc = (...args: any) => any; + +export type ReturnGenericType = + ReturnType extends Promise ? U : ReturnType; + +export type HttpAgentFactory = ( + url: string +) => Transport; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.spec.ts new file mode 100644 index 0000000..2dc4118 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.spec.ts @@ -0,0 +1,30 @@ +import { generateBody, generateBodyMethod } from './body'; +import { JSON_RPC_VERSION } from '../constans'; + +describe('body', () => { + it('generateBodyMethod', () => { + const nameSpace = 'nameSpace'; + const method = 'method'; + expect(generateBodyMethod(nameSpace, method)).toBe( + `${nameSpace}.${method}` + ); + }); + + it('generateBody', () => { + const nameSpace = 'nameSpace'; + const method = 'method'; + const params = ['param1', 'param2']; + const id = 1; + const result = generateBody( + generateBodyMethod(nameSpace, method), + params, + id + ); + expect(result).toEqual({ + jsonrpc: JSON_RPC_VERSION, + method: generateBodyMethod(nameSpace, method), + params, + id, + }); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.ts new file mode 100644 index 0000000..930bea8 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.ts @@ -0,0 +1,19 @@ +import { LoopFunc, PayloadRpc } from '../types'; +import { JSON_RPC_VERSION } from '../constans'; + +export function generateBodyMethod(nameSpace: string, method: string): string { + return `${nameSpace}.${method}`; +} + +export function generateBody( + method: string, + params: Parameters, + id: number +): PayloadRpc { + return { + jsonrpc: JSON_RPC_VERSION, + params, + method, + id, + }; +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/index.ts new file mode 100644 index 0000000..3d45935 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/index.ts @@ -0,0 +1,4 @@ +export * from './wrapper-call'; +export * from './rpc-proxy'; +export * from './rpc-batch'; +export * from './body'; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/pipe.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/pipe.ts new file mode 100644 index 0000000..2caf9ab --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/pipe.ts @@ -0,0 +1,40 @@ +import { OperatorFunction, pipe, throwError } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { + LoopFunc, + ReturnGenericType, + ReturnTransportCall, + RpcError, + RpcResult, +} from '../types'; + +export const mapParseResponse = (r: RpcResult) => { + if ('error' in r) return r; + return r.result; +}; + +export const throwOrReturnError = (returnError = false) => { + return (r: ReturnTransportCall) => { + if (!(typeof r === 'object' && r !== null && 'error' in r)) { + return r; + } + const error = new RpcError(r); + if (!returnError) throw error; + return error; + }; +}; + +export function parseResponse(): OperatorFunction< + RpcResult, + ReturnTransportCall +> { + return pipe(map(mapParseResponse)); +} + +export function throwRpcError(): OperatorFunction< + ReturnTransportCall, + ReturnGenericType +> { + return pipe(map((r) => throwOrReturnError()(r))); +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.spec.ts new file mode 100644 index 0000000..fdb45b1 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.spec.ts @@ -0,0 +1,107 @@ +import { RpcBatchFactory, RpcBatchFactoryPromise } from './rpc-batch'; +import { WrapperCall } from './wrapper-call'; +import { RpcError } from '../types'; +import { of } from 'rxjs'; + +describe('rpc-batch', () => { + it('RpcBatchFactory', (done) => { + const transport = jest.fn().mockImplementationOnce((data) => { + expect(data.map((i: any) => i.id)).toEqual([1, 2, 3]); + const errorObj = { + error: { + message: 'ErrroMsg', + code: 1, + }, + id: 3, + }; + return of( + data.map((i: any) => { + if (i.id === 3) { + return errorObj; + } else { + return { + result: i.params, + id: i.id, + }; + } + }) + ); + }); + const rpcBatch = RpcBatchFactory(transport); + const call1 = new WrapperCall( + 'TestSpace', + 'TestMethod', + [1, 2], + transport + ) as any; + const call2 = new WrapperCall( + 'TestSpace1', + 'TestMethod1', + [2], + transport + ) as any; + const call3 = new WrapperCall( + 'TestSpace2', + 'TestMethod2', + [3], + transport + ) as any; + + rpcBatch(call3, call1, call2).subscribe((result) => { + const [r3, r1, r2] = result; + expect(r3).toBeInstanceOf(RpcError); + expect(r2).toEqual(call2.arg); + expect(r1).toEqual(call1.arg); + done(); + }); + }); + + it('RpcBatchFactoryPromise', async () => { + const transport = jest.fn().mockImplementationOnce((data) => { + expect(data.map((i: any) => i.id)).toEqual([4, 5, 6]); + const errorObj = { + error: { + message: 'ErrroMsg', + code: 1, + }, + id: 6, + }; + return of( + data.map((i: any) => { + if (i.id === 6) { + return errorObj; + } else { + return { + result: i.params, + id: i.id, + }; + } + }) + ); + }); + const rpcBatch = RpcBatchFactoryPromise(transport); + const call1 = new WrapperCall( + 'TestSpace', + 'TestMethod', + [1, 2], + transport + ) as any; + const call2 = new WrapperCall( + 'TestSpace1', + 'TestMethod1', + [2], + transport + ) as any; + const call3 = new WrapperCall( + 'TestSpace2', + 'TestMethod2', + [3], + transport + ) as any; + + const [r3, r1, r2] = await rpcBatch(call3, call1, call2); + expect(r3).toBeInstanceOf(RpcError); + expect(r2).toEqual(call2.arg); + expect(r1).toEqual(call1.arg); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.ts new file mode 100644 index 0000000..5e0ec1d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.ts @@ -0,0 +1,40 @@ +import { + LoopFunc, + RpcBatch, + RpcBatchPromise, + RpcResult, + Transport, +} from '../types'; +import { map } from 'rxjs/operators'; +import { mapParseResponse, throwOrReturnError } from './pipe'; +import { lastValueFrom } from 'rxjs'; + +export function RpcBatchFactory( + transport: Transport +): RpcBatch { + const returnError = throwOrReturnError(true); + return (...arg) => { + const bodyArray = arg.map((i) => i.body); + + const sortMap = bodyArray.reduce((acum, item, currentIndex) => { + acum[item.id] = currentIndex; + return acum; + }, {} as Record); + + bodyArray.sort((a, b) => a.id - b.id); + + return transport(bodyArray as any).pipe( + map((r) => r as unknown as RpcResult[]), + map((r) => r.sort((a, b) => sortMap[a.id || 0] - sortMap[b.id || 0])), + map((r) => r.map(mapParseResponse) as any), + map((r) => r.map(returnError) as any) + ); + }; +} + +export function RpcBatchFactoryPromise( + transport: Transport +): RpcBatchPromise { + const rpcBatch = RpcBatchFactory(transport); + return (...arg) => lastValueFrom(rpcBatch(...arg)); +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.spec.ts new file mode 100644 index 0000000..059634c --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.spec.ts @@ -0,0 +1,48 @@ +import { Observable, of } from 'rxjs'; + +import { rpcProxy } from './rpc-proxy'; +import { LoopFunc, RpcReturnList, Transport } from '../types'; + +interface TestRpc { + test(a: number, b: number): Promise; +} + +type MapRpc = { + TestRpc: TestRpc; +}; + +describe('rpc-proxy', () => { + it('should be return Observable', () => { + const arg: [number, number] = [1, 2]; + const resultRpc = 1; + const transport = jest.fn().mockImplementationOnce((data) => ({ + result: resultRpc, + id: data.id, + })) as Transport; + const usePromise = false; + const rpc = rpcProxy>(transport, usePromise); + expect(rpc).toHaveProperty('TestRpc'); + expect(rpc.TestRpc).toHaveProperty('test'); + const result = rpc.TestRpc.test(...arg); + expect(result).toBeInstanceOf(Observable); + }); + + it('should be return Promise', async () => { + const arg: [number, number] = [1, 2]; + const resultRpc = 1; + const transport = jest.fn().mockImplementationOnce((data) => + of({ + result: resultRpc, + id: data.id, + }) + ) as Transport; + const usePromise = true; + const rpc = rpcProxy>(transport, usePromise); + expect(rpc).toHaveProperty('TestRpc'); + expect(rpc.TestRpc).toHaveProperty('test'); + const result = rpc.TestRpc.test(...arg); + expect(result).toBeInstanceOf(Promise); + const resultRpcCheck = await result; + expect(resultRpcCheck).toBe(resultRpc); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.ts new file mode 100644 index 0000000..c81c8f2 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.ts @@ -0,0 +1,30 @@ +import { lastValueFrom } from 'rxjs'; + +import { LoopFunc, RpcReturnList, Transport } from '../types'; +import { WrapperCall } from './wrapper-call'; + +export const rpcProxy = >( + transport: Transport, + usePromise = false +): T => { + const mockRpcNameSpace = {} as T; + return new Proxy(mockRpcNameSpace, { + get(target, nameSpace: keyof T) { + const mockRpcmethode = {} as T[typeof nameSpace]; + return new Proxy(mockRpcmethode, { + get(target, method: keyof T[typeof nameSpace]) { + return (...arg: Parameters) => { + const wr = new WrapperCall( + String(nameSpace), + String(method), + arg, + transport + ); + if (usePromise) return lastValueFrom(wr); + return wr; + }; + }, + }); + }, + }); +}; diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.spec.ts new file mode 100644 index 0000000..c258529 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.spec.ts @@ -0,0 +1,39 @@ +import { WrapperCall } from './wrapper-call'; +import { Transport } from '../types'; +import { of } from 'rxjs'; + +function mockRPC(a: number, b: string): number { + return 1; +} + +describe('wrapper-call', () => { + let nameSpace: string; + let method: string; + let arg: Parameters; + let transport: Transport; + + beforeEach(() => { + nameSpace = 'namespace'; + method = 'method'; + arg = [1, 'test']; + }); + + it('should be init Observable', (done) => { + const result = { result: 'result' }; + transport = jest.fn().mockImplementationOnce((input) => { + return of(result); + }); + expect.assertions(2); + const instWrapperCall = new WrapperCall(nameSpace, method, arg, transport); + instWrapperCall.subscribe({ + next: (r) => { + expect(r).toEqual(result.result); + }, + complete: () => { + expect(transport).toHaveBeenCalledWith(instWrapperCall.body); + done(); + }, + error: (err) => done(err), + }); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.ts new file mode 100644 index 0000000..5c3cc2d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.ts @@ -0,0 +1,34 @@ +import { Observable } from 'rxjs'; +import { LoopFunc, PayloadRpc, ReturnTransportCall, Transport } from '../types'; +import { generateBody, generateBodyMethod } from './body'; +import { idRequest } from '../factory'; +import { parseResponse, throwRpcError } from './pipe'; + +export class WrapperCall extends Observable< + ReturnTransportCall +> { + id: number = idRequest(); + body!: PayloadRpc; + constructor( + private nameSpace: string, + private method: string, + private arg: Parameters, + private transport: Transport + ) { + super((subscriber) => { + const transportSubscribe = this.transport(this.body) + .pipe(parseResponse(), throwRpcError()) + .subscribe({ + next: (r) => subscriber.next(r), + error: (err) => subscriber.error(err), + complete: () => subscriber.complete(), + }); + return { unsubscribe: () => transportSubscribe.unsubscribe() }; + }); + this.body = generateBody( + generateBodyMethod(this.nameSpace, this.method), + this.arg, + this.id + ); + } +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig-mjs.lib.json b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig-mjs.lib.json new file mode 100644 index 0000000..b7ab954 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig-mjs.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node"], + "module": "es2015", + "target": "ES2022", + "removeComments": false, + "declaration": true, + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "angularCompilerOptions": { + "compilationMode": "partial", + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.json b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.json new file mode 100644 index 0000000..bf7d88a --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "inlineSources": true, + "inlineSourceMap": true, + "sourceMap": false + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig-mjs.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json new file mode 100644 index 0000000..81ec818 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.spec.json b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.spec.json new file mode 100644 index 0000000..69a251f --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/json-rpc/nestjs-json-rpc/.eslintrc.json b/libs/json-rpc/nestjs-json-rpc/.eslintrc.json new file mode 100644 index 0000000..c9748d2 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/libs/json-rpc/nestjs-json-rpc/README.md b/libs/json-rpc/nestjs-json-rpc/README.md new file mode 100644 index 0000000..19e8e90 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/README.md @@ -0,0 +1,143 @@ +

+ NPM Version + Package License + NPM Downloads + Commitizen friendly + Coverage Badge +

+ +# nestjs-json-rpc + +This plugin allow to create RPC server using [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification). +Now, You can use HTTP or WebSocket as transport protocol. + +## Installation + +```bash +$ npm install @klerick/nestjs-json-rpc +``` +## Example + +Once the installation process is complete, we can import the **NestjsJsonRpcModule** into the root **AppModule**. + +```typescript +import {Module} from '@nestjs/common'; +import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; + +@Module({ + imports: [ + NestjsJsonRpcModule.forRoot({ + path: 'rpc', + transport: TransportType.HTTP, + }), + ], +}) +export class AppModule { +} +``` +so, now you have rpc server which allow: +- POST /rpc + +### If you want to use Websocket: + +```typescript +import {Module} from '@nestjs/common'; +import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; + +@Module({ + imports: [ + NestjsJsonRpcModule.forRoot({ + path: 'rpc', + wsConfig: { + path: '/rpc', + }, + }), + ], +}) +export class AppModule { +} +``` +`wsConfig` - is GatewayMetadata from `@nestjs/websockets/interfaces`; + +***!!!!***: - NestJs by default using **socket.io** adapter, if you want to use native WebSocket, you should use **WsAdapter** +```typescript +import { WsAdapter } from '@nestjs/platform-ws'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app/app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.useWebSocketAdapter(new WsAdapter(app)); + app.init() + await app.listen(3000); +} +``` + +To allow service to your RPC server, you should create class and add to providers the root **AppModule**. + +```typescript +import {Module} from '@nestjs/common'; +import { + NestjsJsonRpcModule, + TransportType, + RpcHandler, + RpcParamsPipe, + createErrorCustomError, +} from '@klerick/nestjs-json-rpc'; + +@RpcHandler() +export class RpcService { + methodeWithObjectParams(a: InputType): Promise { + return Promise.resolve({ + d: `${a.a}`, + c: `${a.b}`, + }); + } + + someMethode(@RpcParamsPipe(ParseIntPipe) firstArg: number): Promise { + if (firstArg === 5) throw createErrorCustomError(-32099, 'Custom Error'); + return Promise.resolve(firstArg); + } + + someOtherMethode(firstArg: number, secondArgument: number): Promise { + return Promise.resolve(''); + } +} + +@Module({ + imports: [ + NestjsJsonRpcModule.forRoot({ + path: 'rpc', + transport: TransportType.HTTP, + }), + ], + providers: [RpcService], +}) +export class AppModule { +} +``` +`@RpcHandler` - decorator which mark class as RPC service + +`@RpcParamsPipe` - decorator for validate input data, + + +After it, you can call you RPC service: + + ``` + POST /rpc +``` + +- **body** - for http request + +```json +{"jsonrpc": "2.0", "method": "RpcService.methodeWithObjectParams", "params": {"a": 23}, "id": 1} +``` + +or RPC call Batch + +```json +[ + {"jsonrpc": "2.0", "method": "RpcService.methodeWithObjectParams", "params": {"a": 23}, "id": 1}, + {"jsonrpc": "2.0", "method": "RpcService.someOtherMethode", "params": [1, 2], "id": 2} +] +``` diff --git a/libs/json-rpc/nestjs-json-rpc/jest.config.ts b/libs/json-rpc/nestjs-json-rpc/jest.config.ts new file mode 100644 index 0000000..f77fba8 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'nestjs-json-rpc', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/json-rpc/nestjs-json-rpc', +}; diff --git a/libs/json-rpc/nestjs-json-rpc/package.json b/libs/json-rpc/nestjs-json-rpc/package.json new file mode 100644 index 0000000..7ef9a81 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/package.json @@ -0,0 +1,37 @@ +{ + "name": "@klerick/nestjs-json-rpc", + "version": "0.0.1", + "engines": { + "node": ">= 16.0.0" + }, + "type": "commonjs", + "description": "JSON-RPC server for NestJs", + "contributors": [ + { + "email": "klerick666@gmain.com", + "name": "Aleksandr Kharkovey" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/klerick/nestjs-json-api.git" + }, + "private": false, + "license": "MIT", + "files": [ + "**/*" + ], + "keywords": [ + "nestjs", + "nest", + "RPC", + "JSON-RPC" + ], + "peerDependencies": { + "socket.io": "*.*.*", + "ws": "*.*.*", + "@nestjs/platform-socket.io": "^10.3.0", + "@nestjs/websockets": "^10.3.0", + "@nestjs/platform-ws": "^10.3.0" + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/project.json b/libs/json-rpc/nestjs-json-rpc/project.json new file mode 100644 index 0000000..b40d8af --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/project.json @@ -0,0 +1,54 @@ +{ + "name": "nestjs-json-rpc", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/json-rpc/nestjs-json-rpc/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/json-rpc/nestjs-json-rpc", + "tsConfig": "libs/json-rpc/nestjs-json-rpc/tsconfig.lib.json", + "main": "libs/json-rpc/nestjs-json-rpc/src/index.ts", + "assets": ["libs/json-rpc/nestjs-json-rpc/*.md"], + "updateBuildableProjectDepsInPackageJson": true, + "buildableProjectDepsInPackageJsonType": "peerDependencies", + "generateExportsField": true + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs nestjs-json-rpc {args.ver} {args.tag}", + "dependsOn": ["build"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/json-rpc/nestjs-json-rpc/jest.config.ts", + "codeCoverage": true, + "coverageReporters": ["json-summary"] + } + }, + "upload-badge": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "target": "test" + } + ], + "options": { + "commands": ["node tools/scripts/upload-badge.mjs nestjs-json-rpc"], + "cwd": "./", + "parallel": false, + "outputPath": "{workspaceRoot}/libs/json-rpc/nestjs-json-rpc" + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/libs/json-rpc/nestjs-json-rpc" + } + } + }, + "tags": [] +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/index.ts b/libs/json-rpc/nestjs-json-rpc/src/index.ts new file mode 100644 index 0000000..0cc2cc6 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/index.ts @@ -0,0 +1,10 @@ +export * from './lib/nestjs-json-rpc.module'; +export { TransportType, CommonRpcConfig, ErrorCodeType } from './lib/types'; +export { + fromRpcErrorToRpcErrorObject, + createError, + RpcError, + createErrorCustomError, +} from './lib/utils'; + +export { RpcHandler, RpcParamsPipe } from './lib/decorators'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/constants/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/constants/index.ts new file mode 100644 index 0000000..47dfe49 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/constants/index.ts @@ -0,0 +1,18 @@ +import { ErrorCodeType } from '../types'; + +export const JsonRpcMetadataKey = '__rpc-metadata__'; +export const JsonRpcMetadataKeyParamPipe = '__rpc-metadata-param-pipe__'; + +export const MAP_HANDLER = Symbol('MAP_HANDLER'); +export const RPC_CONTEXT = Symbol('RPC_CONTEXT'); +export const ASYNC_ITERATOR_FACTORY = Symbol('ASYNC_ITERATOR_FACTORY'); +export const ZOD_INPUT_DATA = Symbol('ZOD_INPUT_DATA'); + +export const ErrorCode: Record = { + [ErrorCodeType.ParseError]: -32700, + [ErrorCodeType.InvalidRequest]: -32600, + [ErrorCodeType.MethodNotFound]: -32601, + [ErrorCodeType.InvalidParams]: -32602, + [ErrorCodeType.InternalError]: -32603, + [ErrorCodeType.ServerError]: -32000, +} as const; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/decorators/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/decorators/index.ts new file mode 100644 index 0000000..42af904 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/decorators/index.ts @@ -0,0 +1,45 @@ +import { + applyDecorators, + Inject, + Injectable, + PipeTransform, + SetMetadata, +} from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces'; +import { + JsonRpcMetadataKey, + JsonRpcMetadataKeyParamPipe, + RPC_CONTEXT, +} from '../constants'; + +export const RpcHandler = () => { + return applyDecorators(SetMetadata(JsonRpcMetadataKey, true), Injectable()); +}; + +// export function InjectContext(): PropertyDecorator { +// return (target, key) => { +// Inject(RPC_CONTEXT)(target, key); +// }; +// } + +export const RpcParamsPipe = ( + pipe: Type | PipeTransform +): ParameterDecorator => { + return (target, key, index) => { + if (!key) { + throw Error('key is undefined'); + } + const args: Record | PipeTransform> = + Reflect.getMetadata( + JsonRpcMetadataKeyParamPipe, + target.constructor, + key + ) || {}; + Reflect.defineMetadata( + JsonRpcMetadataKeyParamPipe, + Object.assign(Object.assign({}, args), { [`params:${index}`]: pipe }), + target.constructor, + key + ); + }; +}; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.spec.ts new file mode 100644 index 0000000..6b9d59e --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.spec.ts @@ -0,0 +1,42 @@ +import { Test } from '@nestjs/testing'; +import { JsonRpcController } from './json-rpc.controller'; +import { UtilModule } from '../../util/util.module'; +import { HandlerService } from '../../util/service'; + +describe('json-rpc.controller', () => { + let jsonRpcController: JsonRpcController; + let handlerService: HandlerService; + beforeEach(async () => { + const testModuleRef = await Test.createTestingModule({ + imports: [UtilModule], + providers: [JsonRpcController], + }).compile(); + + jsonRpcController = testModuleRef.get(JsonRpcController); + handlerService = testModuleRef.get(HandlerService); + }); + + it('Should be call HandlerService', async () => { + const result = { + jsonrpc: '2.0', + id: 1, + result: 1 as any, + }; + const input = { + jsonrpc: '2.0', + id: 1, + params: [1], + method: { + methodName: 'test', + spaceName: 'test', + }, + }; + const spyHandlerServiceCallHandler = jest + .spyOn(handlerService, 'callHandler') + .mockResolvedValue(result as any); + const resultController = jsonRpcController.handler(input as any); + expect(spyHandlerServiceCallHandler).toHaveBeenCalledWith(input); + expect(spyHandlerServiceCallHandler).toHaveBeenCalledTimes(1); + expect(resultController).resolves.toEqual(result); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.ts new file mode 100644 index 0000000..1048002 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.ts @@ -0,0 +1,19 @@ +import { Body, Controller, Inject, Post, UseFilters } from '@nestjs/common'; +import { HandlerService } from '../../util/service'; +import { InputDataPipe } from '../../util/pipe/input-data.pipe'; +import { PayloadRpcData, RpcResult } from '../../../types'; +import { RpcErrorObject } from '../../../types'; +import { RpcErrorExceptionFilter } from '../filter/rpc-error-exception.filter'; + +@Controller('/') +export class JsonRpcController { + @Inject(HandlerService) private readonly handlerService!: HandlerService; + + @Post('') + @UseFilters(new RpcErrorExceptionFilter()) + async handler( + @Body(InputDataPipe) body: PayloadRpcData + ): Promise> { + return this.handlerService.runRpc(body); + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.spec.ts new file mode 100644 index 0000000..e4b95de --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.spec.ts @@ -0,0 +1,56 @@ +import { ArgumentsHost } from '@nestjs/common'; + +import { RpcErrorExceptionFilter } from './rpc-error-exception.filter'; +import { createError, fromRpcErrorToRpcErrorObject } from '../../../utils'; +import { ErrorCodeType } from '../../../types'; + +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; + +describe('rpc-error-exception.filter', () => { + let argumentsHost: ArgumentsHost; + let response: { + send: (arg: any) => void; + }; + let getResponse: () => typeof response; + + beforeEach(() => { + response = { + send() { + return void 0; + }, + }; + getResponse = () => response; + argumentsHost = { + switchToHttp(): HttpArgumentsHost { + return { + getResponse, + } as any; + }, + } as any; + }); + + it('should catch RpcError and transform it to RpcErrorObject', () => { + const filter = new RpcErrorExceptionFilter(); + const exception = createError( + ErrorCodeType.InvalidRequest, + 'InvalidRequest' + ); + const spySend = jest.spyOn(response, 'send'); + filter.catch(exception, argumentsHost); + expect(spySend).toHaveBeenCalledWith( + fromRpcErrorToRpcErrorObject(exception) + ); + }); + + it('should catch Error and transform it to RpcErrorObject', () => { + const filter = new RpcErrorExceptionFilter(); + const exception = new Error('Test Error'); + const spySend = jest.spyOn(response, 'send'); + filter.catch(exception, argumentsHost); + expect(spySend).toHaveBeenCalledWith( + fromRpcErrorToRpcErrorObject( + createError(ErrorCodeType.ServerError, exception.message) + ) + ); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.ts new file mode 100644 index 0000000..b9c5397 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.ts @@ -0,0 +1,10 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; + +import { getBodyError } from '../../../utils'; + +@Catch() +export class RpcErrorExceptionFilter implements ExceptionFilter { + catch(exception: Error, host: ArgumentsHost): void { + host.switchToHttp().getResponse().send(getBodyError(exception)); + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/http-transport.module.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/http-transport.module.ts new file mode 100644 index 0000000..b218fe4 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/http-transport.module.ts @@ -0,0 +1,22 @@ +import { DynamicModule, Provider } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces/type.interface'; +import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface'; + +import { JsonRpcController } from './controllers/json-rpc.controller'; + +export class HttpTransportModule { + static forRoot( + providers: Provider[], + imports: Array< + Type | DynamicModule | Promise | ForwardReference + > = [] + ): DynamicModule { + return { + module: HttpTransportModule, + providers, + controllers: [JsonRpcController], + // exports: [RPC_CONTEXT], + imports, + }; + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/index.ts new file mode 100644 index 0000000..d176f45 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/index.ts @@ -0,0 +1,3 @@ +export * from './http-transport/http-transport.module'; +export * from './util/util.module'; +export * from './ws-socket-transport/ws-socket-transport.module'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.spec.ts new file mode 100644 index 0000000..703aa7c --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.spec.ts @@ -0,0 +1,144 @@ +import { Test } from '@nestjs/testing'; +import { InputDataPipe } from './input-data.pipe'; +import { ErrorCodeType, PayloadRpcData } from '../../../types'; +import { zodInputDataProvider } from '../../../providers/zod-input-data.provider'; +import { RpcError } from '../../../utils'; +import { ErrorCode } from '../../../constants'; + +describe('input-data.pipe', () => { + let inputDataPipe: InputDataPipe; + beforeEach(async () => { + const testModuleRef = await Test.createTestingModule({ + providers: [InputDataPipe, zodInputDataProvider], + }).compile(); + + inputDataPipe = testModuleRef.get(InputDataPipe); + }); + + it('Should be ok one item', () => { + const value = { + jsonrpc: '2.0', + id: 1, + params: ['1', '2'], + method: 'TestClass.testMethode', + }; + const expectedResult: PayloadRpcData = { + jsonrpc: '2.0', + id: 1, + params: ['1', '2'], + method: { + methodName: 'testMethode', + spaceName: 'TestClass', + }, + }; + expect(inputDataPipe.transform(value)).toEqual(expectedResult); + }); + + it('Should be ok array', () => { + const value = [ + { + jsonrpc: '2.0', + id: 1, + params: ['1', '2'], + method: 'TestClass.testMethode', + }, + ]; + const expectedResult: PayloadRpcData = [ + { + jsonrpc: '2.0', + id: 1, + params: ['1', '2'], + method: { + methodName: 'testMethode', + spaceName: 'TestClass', + }, + }, + ]; + expect(inputDataPipe.transform(value)).toEqual(expectedResult); + }); + it('Should be error', () => { + expect.assertions(3); + try { + inputDataPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe( + ErrorCode[ErrorCodeType.InvalidRequest] + ); + expect((e as RpcError).id).toBe(null); + } + }); + it('Should be error after array', () => { + expect.assertions(3); + try { + inputDataPipe.transform([{}]); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe( + ErrorCode[ErrorCodeType.InvalidRequest] + ); + expect((e as RpcError).id).toBe(null); + } + }); + it('Should be error after array with id', () => { + expect.assertions(3); + const val = [ + { + id: 1, + method: 'TestClass', + }, + ]; + try { + inputDataPipe.transform(val); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe( + ErrorCode[ErrorCodeType.InvalidRequest] + ); + + expect((e as RpcError).id).toBe(val[0].id); + } + }); + it('Should be error after array with id inner', () => { + expect.assertions(3); + const val = [ + { + jsonrpc: '2.0', + id: 1, + params: ['1', '2'], + method: 'TestClass.testMethode', + }, + { + id: 2, + method: 'TestClass', + }, + ]; + try { + inputDataPipe.transform(val); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe( + ErrorCode[ErrorCodeType.InvalidRequest] + ); + + expect((e as RpcError).id).toBe(val[1].id); + } + }); + it('Should be error after array with id', () => { + expect.assertions(3); + const val = { + id: 1, + method: 'TestClass', + }; + try { + inputDataPipe.transform(val); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe( + ErrorCode[ErrorCodeType.InvalidRequest] + ); + + expect((e as RpcError).id).toBe(val.id); + } + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.ts new file mode 100644 index 0000000..19baab0 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable, PipeTransform } from '@nestjs/common'; + +import { ZOD_INPUT_DATA } from '../../../constants'; +import { + ZodPayloadRpc, + ErrorCodeType, + PayloadRpcArray, + PayloadRpc, +} from '../../../types'; +import { createError, RpcError } from '../../../utils'; + +@Injectable() +export class InputDataPipe + implements PipeTransform +{ + @Inject(ZOD_INPUT_DATA) zodInputData!: ZodPayloadRpc; + + transform(value: unknown): PayloadRpcArray | PayloadRpc { + if (Array.isArray(value)) { + const resultValue: PayloadRpcArray = []; + for (const item of value) { + try { + resultValue.push(this.zodInputData.parse(item)); + } catch (e) { + throw this.getError(item); + } + } + return resultValue; + } else { + try { + return this.zodInputData.parse(value); + } catch (e) { + throw this.getError(value); + } + } + } + + private getError(value: unknown): RpcError { + const error = createError(ErrorCodeType.InvalidRequest); + if ( + typeof value === 'object' && + value !== null && + 'id' in value && + (typeof value['id'] === 'string' || typeof value['id'] === 'number') + ) { + const id = parseInt(`${value['id']}`); + error.id = isNaN(id) ? null : id; + } + + return error; + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.spec.ts new file mode 100644 index 0000000..88a0b65 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.spec.ts @@ -0,0 +1,26 @@ +import { Test } from '@nestjs/testing'; +import { ExplorerService } from './explorer.service'; +import { RpcHandler } from '../../../decorators'; + +@RpcHandler() +class TestClass {} + +@RpcHandler() +class Test2Class {} + +describe('explorer.service', () => { + let explorerService: ExplorerService; + beforeEach(async () => { + const testModuleRef = await Test.createTestingModule({ + providers: [ExplorerService, TestClass, Test2Class], + }).compile(); + explorerService = testModuleRef.get(ExplorerService); + }); + + it('explorer', async () => { + const result = explorerService.explore(); + expect(result.length).toBe(2); + expect(result).toContain(TestClass); + expect(result).toContain(Test2Class); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.ts new file mode 100644 index 0000000..926d776 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.ts @@ -0,0 +1,33 @@ +import { Inject, Injectable, Type } from '@nestjs/common'; +import { ModulesContainer } from '@nestjs/core'; +import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; + +import { JsonRpcMetadataKey } from '../../../constants'; + +@Injectable() +export class ExplorerService { + @Inject(ModulesContainer) + private readonly modulesContainer!: ModulesContainer; + + explore(): Type[] { + const modules = [...this.modulesContainer.values()]; + return modules + .reduce( + (acum, module) => (acum.push(...module.providers.values()), acum), + [] as InstanceWrapper[] + ) + .map((instanceWrapper) => { + const { instance } = instanceWrapper; + if (!instance) { + return undefined; + } + + const metadata = Reflect.getMetadata( + JsonRpcMetadataKey, + instance.constructor + ); + return metadata ? (instance.constructor as Type) : undefined; + }) + .filter((i): i is Type => !!i); + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.spec.ts new file mode 100644 index 0000000..1a4f90c --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.spec.ts @@ -0,0 +1,358 @@ +import { HandlerService } from './handler.service'; +import { Test } from '@nestjs/testing'; +import { ParseIntPipe, ParseBoolPipe } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; + +import { createError, RpcError } from '../../../utils'; +import { mapHandlerStoreProvider, AsyncIterate } from '../../../providers'; +import { ErrorCode, MAP_HANDLER } from '../../../constants'; +import { RpcParamsPipe } from '../../../decorators'; +import { ErrorCodeType, PayloadRpcData } from '../../../types'; + +class RpcTestClass { + rpcTestMethode( + @RpcParamsPipe(ParseIntPipe) firstInputParam: number, + @RpcParamsPipe(ParseIntPipe) secondInputParam: number + ): { firstInputParam: number; secondInputParam: number } { + return { firstInputParam, secondInputParam }; + } + + rpcTest2Methode( + @RpcParamsPipe(ParseIntPipe) firstInputParam: number, + secondInputParam: boolean + ): { firstInputParam: number; secondInputParam: boolean } { + return { firstInputParam, secondInputParam }; + } +} + +describe('handler.service', () => { + let handlerService: HandlerService; + let mapHandler: Map; + let moduleRef: ModuleRef; + beforeEach(async () => { + const testModuleRef = await Test.createTestingModule({ + providers: [HandlerService, mapHandlerStoreProvider, AsyncIterate], + controllers: [ParseIntPipe], + }).compile(); + + handlerService = testModuleRef.get(HandlerService); + mapHandler = testModuleRef.get(MAP_HANDLER); + moduleRef = testModuleRef.get(ModuleRef); + }); + + describe('runRpc', () => { + it('Should be result', async () => { + const params = ['1', '2']; + const jsonrpc = '2.0'; + const handlerServiceCallHandlerSpy = jest + .spyOn(handlerService, 'callHandler') + .mockResolvedValue({ + jsonrpc, + result: params.map((i) => parseInt(i, 10)), + id: null, + }); + const rpcData: PayloadRpcData = { + params, + jsonrpc, + method: { + methodName: 'Test', + spaceName: 'testMethode', + }, + id: 1, + }; + const result = await handlerService.runRpc(rpcData); + expect(result).toEqual({ + jsonrpc, + result: params.map((i) => parseInt(i, 10)), + id: rpcData.id, + }); + expect(handlerServiceCallHandlerSpy).toHaveBeenCalledWith(rpcData); + }); + it('Should be array result', async () => { + const params = ['1', '2']; + const params2 = ['3', '4']; + const jsonrpc = '2.0'; + const rpcData: PayloadRpcData = [ + { + params, + jsonrpc, + method: { + methodName: 'Test', + spaceName: 'testMethode', + }, + id: 1, + }, + { + params: params2, + jsonrpc, + method: { + methodName: 'Test', + spaceName: 'testMethode', + }, + id: 2, + }, + ]; + let i = 0; + const handlerServiceCallHandlerSpy = jest + .spyOn(handlerService, 'callHandler') + .mockImplementation(() => { + return Promise.resolve({ + jsonrpc, + result: rpcData[i].params.map((i) => parseInt(i as any, 10)), + id: null, + } as any).then((r) => { + i++; + return r; + }); + }); + const result = await handlerService.runRpc(rpcData); + expect(result).toEqual( + rpcData.map((i) => ({ + jsonrpc: i.jsonrpc, + result: i.params.map((i) => parseInt(i as any, 10)), + id: i.id, + })) + ); + expect(handlerServiceCallHandlerSpy).toHaveBeenCalledTimes(2); + expect(handlerServiceCallHandlerSpy).toHaveBeenCalledWith(rpcData[0]); + expect(handlerServiceCallHandlerSpy).toHaveBeenCalledWith(rpcData[1]); + }); + + it('Should be Error rpcObject', async () => { + const params = ['1', '2']; + const jsonrpc = '2.0'; + const title = 'Title Error'; + const description = 'Describe Error'; + const handlerServiceCallHandlerSpy = jest + .spyOn(handlerService, 'callHandler') + .mockRejectedValue( + createError(ErrorCodeType.InvalidRequest, title, description) + ); + const rpcData: PayloadRpcData = { + params, + jsonrpc, + method: { + methodName: 'Test', + spaceName: 'testMethode', + }, + id: 1, + }; + const result = await handlerService.runRpc(rpcData); + expect(handlerServiceCallHandlerSpy).toHaveBeenCalledWith(rpcData); + expect(result).toEqual({ + jsonrpc, + error: { + message: ErrorCodeType.InvalidRequest, + code: ErrorCode[ErrorCodeType.InvalidRequest], + data: { title, description }, + }, + id: rpcData.id, + }); + }); + it('Should be Error rpcObject internalError', async () => { + const params = ['1', '2']; + const jsonrpc = '2.0'; + const title = 'Title Error'; + const handlerServiceCallHandlerSpy = jest + .spyOn(handlerService, 'callHandler') + .mockRejectedValue(new Error(title)); + const rpcData: PayloadRpcData = { + params, + jsonrpc, + method: { + methodName: 'Test', + spaceName: 'testMethode', + }, + id: 1, + }; + const result = await handlerService.runRpc(rpcData); + expect(handlerServiceCallHandlerSpy).toHaveBeenCalledWith(rpcData); + expect(result).toEqual({ + jsonrpc, + error: { + message: ErrorCodeType.ServerError, + code: ErrorCode[ErrorCodeType.ServerError], + data: { title }, + }, + id: rpcData.id, + }); + }); + }); + + describe('callHandler', () => { + it('should be error spaceName', async () => { + expect.assertions(2); + try { + await handlerService.callHandler({ + id: 1, + params: [], + jsonrpc: '2.0', + method: { + methodName: 'test', + spaceName: 'NotFoundSpace', + }, + }); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32601); + } + }); + it('should be error method', async () => { + class TestSpaceName {} + mapHandler.set(TestSpaceName.name, new TestSpaceName()); + expect.assertions(2); + try { + await handlerService.callHandler({ + id: 1, + params: [], + jsonrpc: '2.0', + method: { + methodName: 'test', + spaceName: TestSpaceName.name, + }, + }); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32601); + } + }); + it('Should be correct result', async () => { + mapHandler.set(RpcTestClass.name, new RpcTestClass()); + const params = ['3', '4']; + const result = await handlerService.callHandler({ + id: 1, + params, + jsonrpc: '2.0', + method: { + methodName: 'rpcTestMethode', + spaceName: RpcTestClass.name, + }, + }); + + expect(result).toEqual({ + id: null, + jsonrpc: '2.0', + result: { + firstInputParam: parseInt(params[0], 10), + secondInputParam: parseInt(params[1], 10), + }, + }); + }); + it('Should be validate error', async () => { + mapHandler.set(RpcTestClass.name, new RpcTestClass()); + const params = ['sdfsdfsdf', '4']; + expect.assertions(2); + try { + await handlerService.callHandler({ + id: 1, + params, + jsonrpc: '2.0', + method: { + methodName: 'rpcTestMethode', + spaceName: RpcTestClass.name, + }, + }); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + expect((e as RpcError).code).toBe(-32602); + } + }); + it('Should be other error error', async () => { + class TestSpaceName { + rpcTestMethode() { + throw new Error(); + } + } + mapHandler.set(TestSpaceName.name, new TestSpaceName()); + const params = ['sdfsdfsdf', '4']; + expect.assertions(1); + try { + await handlerService.callHandler({ + id: 1, + params, + jsonrpc: '2.0', + method: { + methodName: 'rpcTestMethode', + spaceName: TestSpaceName.name, + }, + }); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } + }); + }); + + describe('getParamsForHandler', () => { + it('should return new params from class pip', async () => { + const params = ['1', '2']; + const params2: [string, boolean] = ['1', true]; + const rpcTestClassInst = new RpcTestClass(); + const result = await handlerService.getParamsForHandler( + rpcTestClassInst, + 'rpcTestMethode', + params + ); + + expect(result).toEqual(params.map((i) => parseInt(i, 10))); + + const result2 = await handlerService.getParamsForHandler( + rpcTestClassInst, + 'rpcTest2Methode', + params2 + ); + expect(result2).toEqual([parseInt(params2[0], 10), params2[1]]); + }); + + it('should be error', async () => { + const params = ['sdfsdf', '2']; + const rpcTestClassInst = new RpcTestClass(); + expect.assertions(1); + try { + await handlerService.getParamsForHandler( + rpcTestClassInst, + 'rpcTestMethode', + params + ); + } catch (e) { + expect(e).toBeInstanceOf(RpcError); + } + }); + }); + + describe('getPipeByType', () => { + it('should return the pipe instance', async () => { + const pipe = new ParseIntPipe(); + const result = await handlerService.getPipeByType(pipe); + expect(result).toBeInstanceOf(ParseIntPipe); + }); + + it('should return the pipe from pipe type has in provider', async () => { + const moduleRefGetSoy = jest.spyOn(moduleRef, 'get'); + const moduleRefCreateSoy = jest.spyOn(moduleRef, 'create'); + const result = await handlerService.getPipeByType(ParseIntPipe); + expect(result).toBeInstanceOf(ParseIntPipe); + expect(moduleRefCreateSoy).toHaveBeenCalledTimes(0); + expect(moduleRefGetSoy).toHaveBeenCalledTimes(1); + }); + + it('should return the pipe from pipe type has not in provider', async () => { + const moduleRefGetSoy = jest.spyOn(moduleRef, 'get'); + const moduleRefCreateSoy = jest.spyOn(moduleRef, 'create'); + const result = await handlerService.getPipeByType(ParseBoolPipe); + expect(result).toBeInstanceOf(ParseBoolPipe); + expect(moduleRefCreateSoy).toHaveBeenCalledTimes(1); + expect(moduleRefGetSoy).toHaveBeenCalledTimes(1); + }); + + it('should return the pipe from pipe type has not in provider twice', async () => { + const moduleRefGetSoy = jest.spyOn(moduleRef, 'get'); + const moduleRefCreateSoy = jest.spyOn(moduleRef, 'create'); + const result = await handlerService.getPipeByType(ParseBoolPipe); + expect(result).toBeInstanceOf(ParseBoolPipe); + const result2 = await handlerService.getPipeByType(ParseBoolPipe); + expect(result2).toBeInstanceOf(ParseBoolPipe); + expect(moduleRefCreateSoy).toHaveBeenCalledTimes(1); + expect(moduleRefGetSoy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.ts new file mode 100644 index 0000000..8df8937 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.ts @@ -0,0 +1,251 @@ +import { + ArgumentMetadata, + BadRequestException, + Inject, + Injectable, + PipeTransform, + Type, +} from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { + ErrorCodeType, + PayloadRpc, + PayloadRpcArray, + PayloadRpcData, + RpcResult, +} from '../../../types'; +import { + createError, + fromRpcErrorToRpcErrorObject, + RpcError, +} from '../../../utils'; +import { + ASYNC_ITERATOR_FACTORY, + JsonRpcMetadataKeyParamPipe, + MAP_HANDLER, +} from '../../../constants'; +import { IterateFactory } from '../../../providers'; +import { RpcErrorObject } from '../../../types'; + +type toString = T extends string ? T : never; + +function isArrayData(data: PayloadRpcData): data is PayloadRpcArray { + return Array.isArray(data); +} + +function isTypeTransform( + pipe: Type | PipeTransform +): pipe is PipeTransform { + return !(typeof pipe === 'function' && /^\s*class\s+/.test(pipe.toString())); +} + +function isMethod( + handler: T, + methodName: unknown +): methodName is keyof T { + if (typeof methodName !== 'string') return false; + const methode = Reflect.get(handler, methodName); + return methode && typeof methode === 'function'; +} + +type CallBackReturnType = { + pipe: PipeTransform | undefined; + metatype: ArgumentMetadata; + params: PayloadRpc['params'][number]; + index: number; +}; + +@Injectable() +export class HandlerService { + @Inject(MAP_HANDLER) private readonly mapHandler!: Map; + @Inject(ModuleRef) private readonly moduleRef!: ModuleRef; + @Inject(ASYNC_ITERATOR_FACTORY) + private readonly asyncIterate!: IterateFactory< + PayloadRpc['params'], + ( + params: PayloadRpc['params'][number], + index: number + ) => Promise + >; + + private mapInjectPipe: Map, PipeTransform> = new Map< + Type, + PipeTransform + >(); + + public async runRpc( + data: PayloadRpcData + ): Promise> { + if (isArrayData(data)) { + const result: (RpcResult | RpcErrorObject)[] = []; + for (const item of data) { + const callRpcResult = await this.callRpc(item, item.id); + result.push(callRpcResult); + } + return result; + } else { + return this.callRpc(data, data.id); + } + } + + private async callRpc( + rpcData: PayloadRpc, + id: number + ): Promise { + try { + const result = await this.callHandler(rpcData); + return { + ...result, + id, + }; + } catch (e) { + if (e instanceof RpcError) { + return fromRpcErrorToRpcErrorObject(e, id); + } + return fromRpcErrorToRpcErrorObject( + createError(ErrorCodeType.ServerError, (e as Error).message), + id + ); + } + } + + async callHandler(rpcData: PayloadRpc): Promise { + const handler = this.mapHandler.get(rpcData.method.spaceName); + if (!handler) { + throw createError( + ErrorCodeType.MethodNotFound, + `${rpcData.method.spaceName} not found` + ); + } + const methodName = rpcData.method.methodName; + + if (!isMethod(handler, methodName)) { + throw createError( + ErrorCodeType.MethodNotFound, + `${rpcData.method.spaceName}.${rpcData.method.methodName} not found` + ); + } + + const params = await this.getParamsForHandler( + handler, + methodName, + rpcData.params + ); + + const result = await ( + handler[methodName] as ( + ...arg: PayloadRpc['params'] + ) => Promise + )(...params); + const { jsonrpc } = rpcData; + + return { + jsonrpc, + result, + id: null, + }; + } + + async getParamsForHandler( + nameSpaceInst: T, + methodeName: toString, + params: PayloadRpc['params'] + ): Promise { + const pipesMetadata: Record = Reflect.getMetadata( + JsonRpcMetadataKeyParamPipe, + nameSpaceInst.constructor, + methodeName + ); + + const pipeObjectByIndex: Map | PipeTransform> = + new Map(); + + for (const item in pipesMetadata) { + const index = item.split(':').at(-1); + if (!index) { + continue; + } + pipeObjectByIndex.set(parseInt(index, 10), pipesMetadata[item]); + } + + const paramsType = Reflect.getMetadata( + 'design:paramtypes', + nameSpaceInst.constructor.prototype, + methodeName + ) as ArgumentMetadata[]; + + const callbackFunc = async ( + params: PayloadRpc['params'][number], + index: number + ): Promise => { + let pipe = pipeObjectByIndex.get(index); + if (pipe) { + pipe = await this.getPipeByType(pipe); + } + let metatype: ArgumentMetadata = { + type: 'custom', + data: '', + metatype: undefined, + }; + if (paramsType && paramsType[index]) { + metatype = paramsType[index]; + } + return { + pipe, + metatype, + params, + index, + }; + }; + + const iterate = this.asyncIterate.createIterator(params, callbackFunc); + const argAfterParse: PayloadRpc['params'] = []; + for await (const paramItem of iterate) { + const { params, pipe, metatype, index } = paramItem; + if (!pipe) { + argAfterParse.push(params); + continue; + } + try { + const transFromParams = await pipe.transform(params, metatype); + argAfterParse.push(transFromParams); + } catch (e) { + throw createError( + e instanceof BadRequestException + ? ErrorCodeType.InvalidParams + : ErrorCodeType.InternalError, + (e as Error).message, + `Argument: #${index}` + ); + } + } + + return argAfterParse; + } + + async getPipeByType | PipeTransform>( + pipe: T + ): Promise { + let targetPipeTransform: PipeTransform; + if (isTypeTransform(pipe)) { + targetPipeTransform = pipe; + } else { + const hasInMap = this.mapInjectPipe.get(pipe); + if (hasInMap) { + targetPipeTransform = hasInMap; + } else { + try { + return this.moduleRef.get(pipe, { + strict: false, + }); + } catch (e) { + targetPipeTransform = await this.moduleRef.create( + pipe + ); + this.mapInjectPipe.set(pipe, targetPipeTransform); + } + } + } + return targetPipeTransform; + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/index.ts new file mode 100644 index 0000000..5c1277d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/index.ts @@ -0,0 +1,2 @@ +export * from './handler.service'; +export * from './explorer.service'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.spec.ts new file mode 100644 index 0000000..b27de31 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.spec.ts @@ -0,0 +1,33 @@ +import { Test } from '@nestjs/testing'; +import { UtilModule } from './util.module'; +import { ExplorerService } from './service'; +import { MAP_HANDLER } from '../../constants'; + +class TestClass {} +class Test2Class {} + +describe('Check util module', () => { + let utilModule: UtilModule; + let explorerService: ExplorerService; + let mapHandler: Map; + beforeEach(async () => { + const testModuleRef = await Test.createTestingModule({ + imports: [UtilModule], + providers: [TestClass, Test2Class], + }).compile(); + + explorerService = testModuleRef.get(ExplorerService); + utilModule = testModuleRef.get(UtilModule); + mapHandler = testModuleRef.get(MAP_HANDLER); + }); + + it('onApplicationBootstrap', async () => { + jest + .spyOn(explorerService, 'explore') + .mockReturnValue([TestClass, Test2Class]); + utilModule.onApplicationBootstrap(); + expect(mapHandler.size).toBe(2); + expect(mapHandler.get(TestClass.name)).toBeInstanceOf(TestClass); + expect(mapHandler.get(Test2Class.name)).toBeInstanceOf(Test2Class); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.ts new file mode 100644 index 0000000..235a0fd --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.ts @@ -0,0 +1,43 @@ +import { Inject, Module, OnApplicationBootstrap } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { MAP_HANDLER } from '../../constants'; +import { mapHandlerStoreProvider, AsyncIterate } from '../../providers'; + +import { HandlerService, ExplorerService } from './service'; +import { zodInputDataProvider } from '../../providers/zod-input-data.provider'; +import { InputDataPipe } from './pipe/input-data.pipe'; + +@Module({ + controllers: [], + providers: [ + mapHandlerStoreProvider, + HandlerService, + ExplorerService, + AsyncIterate, + zodInputDataProvider, + InputDataPipe, + ], + exports: [ + mapHandlerStoreProvider, + HandlerService, + AsyncIterate, + zodInputDataProvider, + InputDataPipe, + ], +}) +export class UtilModule implements OnApplicationBootstrap { + @Inject(MAP_HANDLER) private readonly mapHandler!: Map; + @Inject(ExplorerService) private readonly explorerService!: ExplorerService; + @Inject(ModuleRef) private readonly moduleRef!: ModuleRef; + + onApplicationBootstrap() { + const handlerList = this.explorerService.explore(); + for (const handler of handlerList) { + const instance = this.moduleRef.get(handler, { strict: false }); + if (!instance) { + return; + } + this.mapHandler.set(handler.name, instance); + } + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/constants/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/constants/index.ts new file mode 100644 index 0000000..d2578e0 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/constants/index.ts @@ -0,0 +1 @@ +export const WS_EVENT_NAME = 'rpc'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/create-gateway.factory.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/create-gateway.factory.ts new file mode 100644 index 0000000..d693648 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/create-gateway.factory.ts @@ -0,0 +1,12 @@ +import { WebSocketGatewayService } from '../service'; +import { GatewayMetadata } from '@nestjs/websockets/interfaces'; +import { WebSocketGateway } from '@nestjs/websockets'; +import { Type } from '@nestjs/common'; + +export function createGatewayFactory( + service: Type, + config: GatewayMetadata +): Type { + WebSocketGateway(config)(service); + return service; +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/index.ts new file mode 100644 index 0000000..c568c11 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/index.ts @@ -0,0 +1 @@ +export * from './create-gateway.factory'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.spec.ts new file mode 100644 index 0000000..69ae8c9 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.spec.ts @@ -0,0 +1,118 @@ +import { ArgumentsHost } from '@nestjs/common'; +import { WsArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface'; +import { WebSocket } from 'ws'; +import { Socket } from 'socket.io'; + +import { RpcWsErrorExceptionFilter } from './rpc-ws-error-exception.filter'; +import { createError, fromRpcErrorToRpcErrorObject } from '../../../utils'; +import { ErrorCodeType } from '../../../types'; +import { WS_EVENT_NAME } from '../constants'; + +describe('rpc-ws-error-exception.filter', () => { + describe('WebSocket', () => { + const WebSocketInst = new WebSocket( + 'wss://demo.piesocket.com/v3/channel_123', + {} + ); + let argumentsHost: ArgumentsHost; + let getClient: () => WebSocket; + + beforeEach(() => { + getClient = () => WebSocketInst; + argumentsHost = { + switchToWs(): WsArgumentsHost { + return { + getClient, + } as any; + }, + } as any; + }); + + it('should catch RpcError and transform it to RpcErrorObject', () => { + const filter = new RpcWsErrorExceptionFilter(); + const exception = createError( + ErrorCodeType.InvalidRequest, + 'InvalidRequest' + ); + + const spySend = jest.spyOn(WebSocketInst, 'send').mockImplementation(); + filter.catch(exception, argumentsHost); + expect(spySend).toHaveBeenCalledWith( + JSON.stringify({ + event: WS_EVENT_NAME, + data: fromRpcErrorToRpcErrorObject(exception), + }) + ); + }); + + it('should catch Error and transform it to RpcErrorObject', () => { + const filter = new RpcWsErrorExceptionFilter(); + const exception = new Error('Test Error'); + const spySend = jest.spyOn(WebSocketInst, 'send').mockImplementation(); + filter.catch(exception, argumentsHost); + expect(spySend).toHaveBeenCalledWith( + JSON.stringify({ + event: WS_EVENT_NAME, + data: fromRpcErrorToRpcErrorObject( + createError(ErrorCodeType.ServerError, exception.message) + ), + }) + ); + }); + }); + + describe('socket.io', () => { + // @ts-ignore + const WebSocketInst = new Socket( + { + server: { _opts: {} }, + }, + { + conn: { + protocol: 1, + }, + } + ); + let argumentsHost: ArgumentsHost; + let getClient: () => Socket; + + beforeEach(() => { + getClient = () => WebSocketInst; + argumentsHost = { + switchToWs(): WsArgumentsHost { + return { + getClient, + } as any; + }, + } as any; + }); + + it('should catch RpcError and transform it to RpcErrorObject', () => { + const filter = new RpcWsErrorExceptionFilter(); + const exception = createError( + ErrorCodeType.InvalidRequest, + 'InvalidRequest' + ); + + const spySend = jest.spyOn(WebSocketInst, 'emit').mockImplementation(); + filter.catch(exception, argumentsHost); + expect(spySend).toHaveBeenCalledWith( + WS_EVENT_NAME, + fromRpcErrorToRpcErrorObject(exception) + ); + }); + + it('should catch Error and transform it to RpcErrorObject', () => { + const filter = new RpcWsErrorExceptionFilter(); + const exception = new Error('Test Error'); + const spySend = jest.spyOn(WebSocketInst, 'emit').mockImplementation(); + filter.catch(exception, argumentsHost); + expect(spySend).toHaveBeenCalledWith( + WS_EVENT_NAME, + fromRpcErrorToRpcErrorObject( + createError(ErrorCodeType.ServerError, exception.message) + ) + ); + }); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.ts new file mode 100644 index 0000000..7ea2be7 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.ts @@ -0,0 +1,19 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; +import { WebSocket } from 'ws'; +import { Socket } from 'socket.io'; + +import { getBodyError } from '../../../utils'; +import { WS_EVENT_NAME } from '../constants'; + +@Catch() +export class RpcWsErrorExceptionFilter implements ExceptionFilter { + catch(exception: Error, host: ArgumentsHost): void { + const body = getBodyError(exception); + const client = host.switchToWs().getClient(); + if (client instanceof WebSocket) { + client.send(JSON.stringify({ event: WS_EVENT_NAME, data: body })); + } else { + client.emit(WS_EVENT_NAME, body); + } + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/index.ts new file mode 100644 index 0000000..128b13b --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/index.ts @@ -0,0 +1 @@ +export * from './web-socket-gateway.service'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/web-socket-gateway.service.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/web-socket-gateway.service.ts new file mode 100644 index 0000000..5b675a2 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/web-socket-gateway.service.ts @@ -0,0 +1,29 @@ +import { + MessageBody, + SubscribeMessage, + WebSocketGateway, + WsResponse, +} from '@nestjs/websockets'; +import { Inject, UseFilters, UsePipes } from '@nestjs/common'; +import { HandlerService } from '../../util/service'; +import { PayloadRpcData, RpcErrorObject, RpcResult } from '../../../types'; +import { InputDataPipe } from '../../util/pipe/input-data.pipe'; +import { WS_EVENT_NAME } from '../constants'; +import { RpcWsErrorExceptionFilter } from '../filter/rpc-ws-error-exception.filter'; + +type WsRpcResponse = WsResponse< + RpcResult | RpcErrorObject | Array +>; + +@WebSocketGateway() +export class WebSocketGatewayService { + @Inject(HandlerService) private readonly handlerService!: HandlerService; + + @UsePipes(InputDataPipe) + @UseFilters(new RpcWsErrorExceptionFilter()) + @SubscribeMessage(WS_EVENT_NAME) + async run(@MessageBody() body: PayloadRpcData): Promise { + const result = await this.handlerService.runRpc(body); + return { data: result, event: WS_EVENT_NAME }; + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/ws-socket-transport.module.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/ws-socket-transport.module.ts new file mode 100644 index 0000000..4d3c669 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/ws-socket-transport.module.ts @@ -0,0 +1,26 @@ +import { DynamicModule, Provider } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces/type.interface'; +import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface'; +import { GatewayMetadata } from '@nestjs/websockets/interfaces'; + +import { WebSocketGatewayService } from './service'; +import { createGatewayFactory } from './factory'; + +export class WsSocketTransportModule { + static forRoot( + wsConfig: GatewayMetadata, + providers: Provider[], + imports: Array< + Type | DynamicModule | Promise | ForwardReference + > = [] + ): DynamicModule { + return { + module: WsSocketTransportModule, + providers: [ + ...providers, + createGatewayFactory(WebSocketGatewayService, wsConfig), + ], + imports, + }; + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/nestjs-json-rpc.module.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/nestjs-json-rpc.module.ts new file mode 100644 index 0000000..ff80490 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/nestjs-json-rpc.module.ts @@ -0,0 +1,54 @@ +import { DynamicModule, Module, Provider } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { JsonRpcConfig, TransportType } from './types'; +import { + HttpTransportModule, + UtilModule, + WsSocketTransportModule, +} from './modules'; + +@Module({ + controllers: [], + providers: [], + exports: [], +}) +export class NestjsJsonRpcModule { + static forRoot(options: JsonRpcConfig): DynamicModule { + const providers: Provider[] = []; + const { transport } = options; + switch (options.transport) { + case TransportType.HTTP: { + const httpModule = HttpTransportModule.forRoot(providers, [UtilModule]); + return { + module: NestjsJsonRpcModule, + imports: [ + ...(options.imports || []), + httpModule, + RouterModule.register([ + { + path: options.path, + module: HttpTransportModule, + }, + ]), + ], + exports: [httpModule], + }; + } + case TransportType.WS: { + const wsModule = WsSocketTransportModule.forRoot( + options.wsConfig, + providers, + [UtilModule] + ); + return { + module: NestjsJsonRpcModule, + imports: [...(options.imports || []), wsModule], + exports: [wsModule], + }; + } + default: { + throw new Error(`Transport ${transport} not implement`); + } + } + } +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/providers/async-iterator.provider.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/async-iterator.provider.ts new file mode 100644 index 0000000..dd0eef7 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/async-iterator.provider.ts @@ -0,0 +1,84 @@ +import { Provider } from '@nestjs/common'; +import { ASYNC_ITERATOR_FACTORY } from '../constants'; +import { PayloadRpc } from '../types'; +import { undefined } from 'zod'; + +type ParamsReturn = R extends (...arg: any) => infer P + ? P extends Promise + ? T extends [infer K, ...any] + ? K + : T + : P + : never; + +export type IterateFactory< + I extends unknown[], + C extends (...arg: any[]) => any +> = { + createIterator: ( + iterateObject: I, + callback: C + ) => { + [Symbol.asyncIterator](): GeneralAsyncIterator; + }; +}; + +class GeneralAsyncIterator< + C extends (...arg: any[]) => any, + K extends unknown[], + T = K[number], + TReturn = ParamsReturn +> { + private counter = 0; + private maxLimit!: number; + + constructor(private iterateObject: K, private callback: C) { + if (!Array.isArray(iterateObject)) { + throw new Error('Expected iterateObject to be an array'); + } + this.maxLimit = iterateObject.length; + } + + async next(): Promise | { done: true }> { + const items = !Array.isArray(this.iterateObject[this.counter]) + ? [this.iterateObject[this.counter]] + : (this.iterateObject[this.counter] as T[]); + items.push(this.counter); + this.counter++; + + if (this.counter <= this.maxLimit) { + return this.callback(...items).then((r: TReturn) => ({ + done: false, + value: r, + })); + } else { + return Promise.resolve({ done: true }); + } + } +} + +export const AsyncIterate: Provider< + IterateFactory< + PayloadRpc['params'], + ( + item: PayloadRpc['params'][number], + index: number + ) => PayloadRpc['params'][number] + > +> = { + provide: ASYNC_ITERATOR_FACTORY, + useFactory: () => ({ + createIterator( + iterateObject: PayloadRpc['params'], + callback: ( + item: PayloadRpc['params'][number], + index: number + ) => PayloadRpc['params'][number] + ) { + return { + [Symbol.asyncIterator]: () => + new GeneralAsyncIterator(iterateObject, callback), + }; + }, + }), +}; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/providers/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/index.ts new file mode 100644 index 0000000..2d1906b --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/index.ts @@ -0,0 +1,2 @@ +export * from './async-iterator.provider'; +export * from './map-handler-store.provider'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/providers/map-handler-store.provider.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/map-handler-store.provider.ts new file mode 100644 index 0000000..07199f0 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/map-handler-store.provider.ts @@ -0,0 +1,7 @@ +import { ValueProvider } from '@nestjs/common'; +import { MAP_HANDLER } from '../constants'; + +export const mapHandlerStoreProvider: ValueProvider = { + provide: MAP_HANDLER, + useValue: new Map(), +}; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/providers/zod-input-data.provider.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/zod-input-data.provider.ts new file mode 100644 index 0000000..e7de6bd --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/zod-input-data.provider.ts @@ -0,0 +1,8 @@ +import { ValueProvider } from '@nestjs/common'; +import { ZOD_INPUT_DATA } from '../constants'; +import { ZPayloadRpc } from '../types'; + +export const zodInputDataProvider: ValueProvider = { + provide: ZOD_INPUT_DATA, + useValue: ZPayloadRpc, +}; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/types/error-code-type.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/types/error-code-type.ts new file mode 100644 index 0000000..def8b9d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/types/error-code-type.ts @@ -0,0 +1,8 @@ +export enum ErrorCodeType { + ParseError = 'Parse error', + InvalidRequest = 'Invalid request', + MethodNotFound = 'Method not found', + InvalidParams = 'Invalid params', + InternalError = 'Internal error', + ServerError = 'Server error', +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/types/error-payloade.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/types/error-payloade.ts new file mode 100644 index 0000000..161cc64 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/types/error-payloade.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +import { zVersion } from './payloade'; + +const zRpcIdError = z.union([z.number(), z.null()]); +const zRpcErrorData = z.object({ + title: z.string(), + description: z.string().optional(), +}); +const zRpcError = z.object({ + message: z.string(), + code: z.number(), + data: zRpcErrorData.optional(), +}); + +export const ZRpcError = z.object({ + jsonrpc: zVersion, + error: zRpcError, + id: zRpcIdError, +}); + +export type RpcErrorObject = z.infer; +export type RpcErrorData = z.infer; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/types/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/types/index.ts new file mode 100644 index 0000000..fbefa40 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/types/index.ts @@ -0,0 +1,5 @@ +export * from './module-options'; +export * from './payloade'; +export * from './error-code-type'; +export * from './utils'; +export * from './error-payloade'; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/types/module-options.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/types/module-options.ts new file mode 100644 index 0000000..7af5e65 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/types/module-options.ts @@ -0,0 +1,34 @@ +import { + DynamicModule, + ForwardReference, + Provider, + Type, +} from '@nestjs/common'; +import { GatewayMetadata } from '@nestjs/websockets/interfaces'; + +export enum TransportType { + HTTP, + WS, +} + +export type HttpTransportConfig = { + transport: TransportType.HTTP; + path: string; +}; + +export type WSTransportConfig = { + transport: TransportType.WS; + wsConfig: GatewayMetadata; +}; + +export type CommonRpcConfig = { + providers?: Provider[]; + imports?: Array< + Type | DynamicModule | Promise | ForwardReference + >; +}; + +export type JsonRpcHttpConfig = CommonRpcConfig & HttpTransportConfig; +export type JsonRpcWsConfig = CommonRpcConfig & WSTransportConfig; + +export type JsonRpcConfig = JsonRpcHttpConfig | JsonRpcWsConfig; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/types/payloade.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/types/payloade.ts new file mode 100644 index 0000000..a59c173 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/types/payloade.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; +const jsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +const zParams = z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + jsonSchema, +]); + +export const zVersion = z.literal('2.0'); +const zMethod = z.string().transform((params, ctx) => { + const result = params.split('.'); + if (result.length !== 2) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Not a 2 items', + }); + return z.NEVER; + } + return { + spaceName: result[0], + methodName: result[1], + }; +}); + +const zRpcParams = z + .union([z.array(zParams), z.record(zParams)]) + .transform((params) => + Array.isArray(params) ? params : Object.values(params) + ); + +const zRpcId = z.union([ + z.string().regex(/^\d+$/).transform(Number), + z.number(), +]); + +export const ZPayloadRpc = z.object({ + jsonrpc: zVersion, + method: zMethod, + params: zRpcParams, + id: zRpcId, +}); + +export const ZPayloadRpcArray = z.array(ZPayloadRpc).min(1); + +export const ZPayloadRpcData = z.union([ZPayloadRpc, ZPayloadRpcArray]); + +export type ZodPayloadRpc = typeof ZPayloadRpc; + +export type PayloadRpc = z.infer; +export type PayloadRpcArray = z.infer; +export type PayloadRpcData = z.infer; + +export const ZRpcResult = z.object({ + jsonrpc: zVersion, + result: zRpcParams, + id: z.union([zRpcId, z.null()]), +}); + +export type RpcResult = z.infer; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/types/utils.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/types/utils.ts new file mode 100644 index 0000000..5f2cf2c --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/types/utils.ts @@ -0,0 +1 @@ +export type ValueOf = T[keyof T]; diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts new file mode 100644 index 0000000..56a4b3e --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts @@ -0,0 +1,78 @@ +import { ErrorCode } from '../constants'; +import { RpcErrorData, RpcErrorObject } from '../types'; +import { ErrorCodeType } from '../types'; + +export class RpcError extends Error { + id: number | null = null; + constructor( + message: keyof typeof ErrorCode, + public code: number, + public data?: RpcErrorData + ) { + super(message); + } +} + +function getErrorData( + title?: string, + description?: string +): undefined | RpcErrorData { + let data: undefined | RpcErrorData = undefined; + if (title) { + data = { title }; + } + + if (title && description) { + data = { title, description }; + } + return data; +} + +export function createErrorCustomError( + code: number, + title?: string, + description?: string +): RpcError { + const absCode = Math.abs(code); + let resultCode = 3200; + if (code < 0 && absCode > 3200 && absCode <= 32099) { + resultCode = code; + } + + return new RpcError( + ErrorCodeType.ServerError, + resultCode, + getErrorData(title, description) + ); +} +export function createError( + type: keyof typeof ErrorCode, + title?: string, + description?: string +): RpcError { + return new RpcError(type, ErrorCode[type], getErrorData(title, description)); +} + +export function fromRpcErrorToRpcErrorObject( + error: RpcError, + id: null | number = null +): RpcErrorObject { + return { + jsonrpc: '2.0', + error: { + message: error.message, + code: error.code, + ...(error.data ? { data: error.data } : {}), + }, + id: error.id ? error.id : id, + }; +} + +export function getBodyError(exception: Error): RpcErrorObject { + if (exception instanceof RpcError) { + return fromRpcErrorToRpcErrorObject(exception); + } + return fromRpcErrorToRpcErrorObject( + createError(ErrorCodeType.ServerError, exception.message) + ); +} diff --git a/libs/json-rpc/nestjs-json-rpc/src/lib/utils/index.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/index.ts new file mode 100644 index 0000000..93ae819 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './error'; diff --git a/libs/json-rpc/nestjs-json-rpc/tsconfig.json b/libs/json-rpc/nestjs-json-rpc/tsconfig.json new file mode 100644 index 0000000..8122543 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/json-rpc/nestjs-json-rpc/tsconfig.lib.json b/libs/json-rpc/nestjs-json-rpc/tsconfig.lib.json new file mode 100644 index 0000000..dbf54fd --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/json-rpc/nestjs-json-rpc/tsconfig.spec.json b/libs/json-rpc/nestjs-json-rpc/tsconfig.spec.json new file mode 100644 index 0000000..69a251f --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/type-for-rpc/.eslintrc.json b/libs/type-for-rpc/.eslintrc.json new file mode 100644 index 0000000..fdc2e11 --- /dev/null +++ b/libs/type-for-rpc/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/libs/type-for-rpc/README.md b/libs/type-for-rpc/README.md new file mode 100644 index 0000000..769c8ba --- /dev/null +++ b/libs/type-for-rpc/README.md @@ -0,0 +1,11 @@ +# type-for-rpc + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build type-for-rpc` to build the library. + +## Running unit tests + +Run `nx test type-for-rpc` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/type-for-rpc/jest.config.ts b/libs/type-for-rpc/jest.config.ts new file mode 100644 index 0000000..d5f4a6a --- /dev/null +++ b/libs/type-for-rpc/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'type-for-rpc', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/type-for-rpc', +}; diff --git a/libs/type-for-rpc/package.json b/libs/type-for-rpc/package.json new file mode 100644 index 0000000..33c4267 --- /dev/null +++ b/libs/type-for-rpc/package.json @@ -0,0 +1,10 @@ +{ + "name": "@nestjs-json-api/type-for-rpc", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} diff --git a/libs/type-for-rpc/project.json b/libs/type-for-rpc/project.json new file mode 100644 index 0000000..4dc6fb0 --- /dev/null +++ b/libs/type-for-rpc/project.json @@ -0,0 +1,19 @@ +{ + "name": "type-for-rpc", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/type-for-rpc/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/type-for-rpc", + "main": "libs/type-for-rpc/src/index.ts", + "tsConfig": "libs/type-for-rpc/tsconfig.lib.json", + "assets": ["libs/type-for-rpc/*.md"] + } + } + }, + "tags": [] +} diff --git a/libs/type-for-rpc/src/index.ts b/libs/type-for-rpc/src/index.ts new file mode 100644 index 0000000..9274489 --- /dev/null +++ b/libs/type-for-rpc/src/index.ts @@ -0,0 +1 @@ +export * from './lib/rpc-service'; diff --git a/libs/type-for-rpc/src/lib/rpc-service.ts b/libs/type-for-rpc/src/lib/rpc-service.ts new file mode 100644 index 0000000..1ddc94a --- /dev/null +++ b/libs/type-for-rpc/src/lib/rpc-service.ts @@ -0,0 +1,15 @@ +export type InputType = { + a: number; + b: number; +}; + +export type OutputType = { + c: string; + d: string; +}; + +export interface RpcService { + someMethode(firstArg: number): Promise; + someOtherMethode(firstArg: number, secondArgument: number): Promise; + methodeWithObjectParams(a: InputType): Promise; +} diff --git a/libs/type-for-rpc/tsconfig.json b/libs/type-for-rpc/tsconfig.json new file mode 100644 index 0000000..f5b8565 --- /dev/null +++ b/libs/type-for-rpc/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/type-for-rpc/tsconfig.lib.json b/libs/type-for-rpc/tsconfig.lib.json new file mode 100644 index 0000000..33eca2c --- /dev/null +++ b/libs/type-for-rpc/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/type-for-rpc/tsconfig.spec.json b/libs/type-for-rpc/tsconfig.spec.json new file mode 100644 index 0000000..9b2a121 --- /dev/null +++ b/libs/type-for-rpc/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index 001c055..dbb7cb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,14 +22,18 @@ "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", + "@nestjs/platform-socket.io": "^10.3.7", + "@nestjs/platform-ws": "^10.3.7", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.3.7", "axios": "1.6.7", "nestjs-pino": "4.0.0", "pg": "8.11.3", "pino-http": "9.0.0", "reflect-metadata": "0.2.1", "rxjs": "^7.8.0", + "socket.io-client": "^4.7.5", "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^9.0.1", @@ -4810,6 +4814,42 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", + "dependencies": { + "socket.io": "4.7.5", + "tslib": "2.6.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/platform-ws": { + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-10.3.7.tgz", + "integrity": "sha512-lOvZ8u5UdL0FgAOdWosDXefVgDikPd4j5el81emkx+H0pFsysfHXSSoSvMLQijdhENqHSl3buRhO5n3M3uia1w==", + "dependencies": { + "tslib": "2.6.2", + "ws": "8.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/schematics": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz", @@ -4981,6 +5021,36 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/websockets": { + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.6.2" + }, + "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 + } + } + }, + "node_modules/@nestjs/websockets/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/@ngtools/webpack": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.1.tgz", @@ -7261,6 +7331,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@sqltools/formatter": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", @@ -7319,7 +7394,7 @@ "version": "1.3.107", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz", "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "@swc/counter": "^0.1.1", @@ -7360,6 +7435,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -7375,6 +7451,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -7390,6 +7467,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7405,6 +7483,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7420,6 +7499,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7435,6 +7515,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7450,6 +7531,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7465,6 +7547,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -7480,6 +7563,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" @@ -7495,6 +7579,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -7507,13 +7592,13 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "devOptional": true + "dev": true }, "node_modules/@swc/helpers": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", - "devOptional": true, + "dev": true, "dependencies": { "tslib": "^2.4.0" } @@ -7522,7 +7607,7 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "devOptional": true + "dev": true }, "node_modules/@tootallnate/once": { "version": "2.0.0", @@ -7667,6 +7752,19 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.3.tgz", @@ -7819,8 +7917,7 @@ "node_modules/@types/node": { "version": "18.16.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.20.tgz", - "integrity": "sha512-nL54VfDjThdP2UXJXZao5wp76CDiDw4zSRO8d4Tk7UgDqNKGKVEQB0/t3ti63NS+YNNkIQDvwEAF04BO+WYu7Q==", - "devOptional": true + "integrity": "sha512-nL54VfDjThdP2UXJXZao5wp76CDiDw4zSRO8d4Tk7UgDqNKGKVEQB0/t3ti63NS+YNNkIQDvwEAF04BO+WYu7Q==" }, "node_modules/@types/node-forge": { "version": "1.3.11", @@ -10054,6 +10151,14 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -12203,6 +12308,94 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -21597,6 +21790,78 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -24455,7 +24720,6 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "dev": true, "engines": { "node": ">=10.0.0" }, @@ -24487,6 +24751,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 03ea733..a5fb64e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "typeorm:run": "npm run typeorm migration:run", "typeorm:revert": "npm run typeorm migration:revert", "seed:run": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/database/tsconfig.lib.json ./node_modules/@jorgebodega/typeorm-seeding/dist/cli.js -d libs/database/src/lib/config-cli.ts seed libs/database/src/lib/seeders/root.seeder.ts", - "demo:json-api": "nx run json-api-server:serve:development" + "demo:json-api": "nx run json-api-server:serve:development", + "demo:json-api-front": "nx run json-api-front:serve:development" }, "private": true, "dependencies": { @@ -24,14 +25,18 @@ "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", + "@nestjs/platform-socket.io": "^10.3.7", + "@nestjs/platform-ws": "^10.3.7", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.3.7", "axios": "1.6.7", "nestjs-pino": "4.0.0", "pg": "8.11.3", "pino-http": "9.0.0", "reflect-metadata": "0.2.1", "rxjs": "^7.8.0", + "socket.io-client": "^4.7.5", "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^9.0.1", @@ -92,5 +97,8 @@ "typescript": "~5.3.2", "verdaccio": "^5.0.4", "webpack-cli": "^5.1.4" + }, + "nx": { + "includedScripts": [] } } diff --git a/tools/scripts/preparation-hybrid-npm-package.mjs b/tools/scripts/preparation-hybrid-npm-package.mjs index e3bc0b4..c999903 100644 --- a/tools/scripts/preparation-hybrid-npm-package.mjs +++ b/tools/scripts/preparation-hybrid-npm-package.mjs @@ -45,9 +45,15 @@ invariant( ); process.chdir(outputPath); +const angularModuleMap = { + 'nestjs-json-rpc-sdk': 'json-rpc-sdk.module', + 'json-api-nestjs-sdk': 'json-api-nestjs-sdk.module' +} + const mjsJson = readJson(); -const angularModule = 'json-api-nestjs-sdk.module'; +const angularModule = angularModuleMap[name]; const angularModulePath = `./${angularModule}`; + const angularPath = mjsJson.exports[angularModulePath] mjsJson.module = addTypeToPath(mjsJson.main) @@ -81,8 +87,24 @@ writeFileSync( 'README.md', readFileSync(join('mjs', 'README.md').toString()), ) -unlinkSync(join('mjs', 'package.json')) -unlinkSync(join('cjs', 'package.json')) -unlinkSync(join('mjs', 'README.md')) -unlinkSync(join('cjs', 'README.md')) +try { + unlinkSync(join('cjs', 'package.json')) + +} catch (e) { + +} +try { + unlinkSync(join('mjs', 'package.json')) +} catch (e) { +} +try { + unlinkSync(join('mjs', 'README.md')) +} catch (e) { + +} +try { + unlinkSync(join('cjs', 'README.md')) +} catch (e) { + +} diff --git a/tools/scripts/prepare-for-angular.mjs b/tools/scripts/prepare-for-angular.mjs index 0122693..3020a14 100644 --- a/tools/scripts/prepare-for-angular.mjs +++ b/tools/scripts/prepare-for-angular.mjs @@ -1,18 +1,36 @@ +import devkit from '@nx/devkit'; import { readFileSync, writeFileSync, unlinkSync } from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; +const { readCachedProjectGraph } = devkit; + +const [, , name, angularName] = process.argv; + +const graph = readCachedProjectGraph(); +const project = graph.nodes[name]; + +const outputPath = project.data?.targets?.build?.options?.outputPath; +const sourceRoot = project.data?.sourceRoot + +const packageJson = JSON.parse(readFileSync(`${outputPath}/package.json`).toString()); +const angularFile = Object.keys(packageJson.exports).filter(i => !['./package.json', '.'].includes(i)).pop().replace('./', '') +const [_, ...pathToModule] = sourceRoot.split('/') + +const [nameSpace] = pathToModule const TEMP_FILE_PATH = 'libs/index.ts' -const ANGULAR_MODULE_PATH = 'tmp/angular-lib/json-api-nestjs-sdk/esm2022/json-api/json-api-nestjs-sdk/src/lib/json-api-angular.mjs' -const LIB_ANGULAR_MODULE_PATH = 'dist/libs/json-api/json-api-nestjs-sdk/mjs/src/lib/json-api-angular.js' +const ANGULAR_MODULE_PATH = `tmp/angular-lib/${name}/esm2022/${nameSpace}/${name}/src/lib/${angularName}.mjs` +const LIB_ANGULAR_MODULE_PATH = `${outputPath}/mjs/src/lib/${angularName}.js` + + writeFileSync( TEMP_FILE_PATH, - 'export * from \'./json-api/json-api-nestjs-sdk/src/json-api-nestjs-sdk.module\';' + `export * from './${pathToModule.join('/')}/${angularFile}';` ); promisify(exec)( - `nx run json-api-nestjs-sdk:compile-for-angular` + `nx run ${name}:compile-for-angular` ).then(r => { writeFileSync(LIB_ANGULAR_MODULE_PATH, readFileSync(ANGULAR_MODULE_PATH)) }).finally(() => { diff --git a/tsconfig.base.json b/tsconfig.base.json index cf0c7f6..016220a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,16 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@klerick/nestjs-json-rpc": [ + "libs/json-rpc/nestjs-json-rpc/src/index.ts" + ], + "@klerick/nestjs-json-rpc-sdk": [ + "libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts" + ], + "@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module": [ + "libs/json-rpc/nestjs-json-rpc-sdk/src/json-rpc-sdk.module.ts" + ], + "@nestjs-json-api/type-for-rpc": ["libs/type-for-rpc/src/index.ts"], "database": ["libs/database/src/index.ts"], "json-api-nestjs": ["libs/json-api/json-api-nestjs/src/index.ts"], "json-api-nestjs-sdk": ["libs/json-api/json-api-nestjs-sdk/src/index.ts"],