From f89f7cfcd20b772600a0c98995bf1582e1b6e9de Mon Sep 17 00:00:00 2001 From: Alex H Date: Mon, 11 Mar 2024 17:59:11 +0100 Subject: [PATCH 01/16] feat(nestjs-json-rpc): default transport for rpc - http transport - validation - explorer need service Closes: #77 --- .verdaccio/config.yml | 28 -- apps/json-api-server/src/app/app.module.ts | 2 + .../json-api-server/src/app/rpc/rpc.module.ts | 13 + libs/json-rpc/nestjs-json-rpc/.eslintrc.json | 25 ++ libs/json-rpc/nestjs-json-rpc/README.md | 11 + libs/json-rpc/nestjs-json-rpc/jest.config.ts | 11 + libs/json-rpc/nestjs-json-rpc/package.json | 10 + libs/json-rpc/nestjs-json-rpc/project.json | 24 ++ libs/json-rpc/nestjs-json-rpc/src/index.ts | 7 + .../src/lib/constants/index.ts | 18 + .../src/lib/decorators/index.ts | 45 +++ .../controllers/json-rpc.controller.ts | 19 + .../filter/rpc-error-exception.filter.ts | 23 ++ .../http-transport/http-transport.module.ts | 22 ++ .../nestjs-json-rpc/src/lib/modules/index.ts | 2 + .../modules/util/pipe/input-data.pipe.spec.ts | 144 +++++++ .../lib/modules/util/pipe/input-data.pipe.ts | 51 +++ .../util/service/explorer.service.spec.ts | 26 ++ .../modules/util/service/explorer.service.ts | 33 ++ .../util/service/handler.service.spec.ts | 358 ++++++++++++++++++ .../modules/util/service/handler.service.ts | 243 ++++++++++++ .../src/lib/modules/util/service/index.ts | 2 + .../src/lib/modules/util/util.module.spec.ts | 33 ++ .../src/lib/modules/util/util.module.ts | 35 ++ .../src/lib/nestjs-json-rpc.module.ts | 37 ++ .../lib/providers/async-iterator.provider.ts | 84 ++++ .../src/lib/providers/index.ts | 2 + .../providers/map-handler-store.provider.ts | 7 + .../lib/providers/zod-input-data.provider.ts | 8 + .../src/lib/types/error-code-type.ts | 8 + .../src/lib/types/error-payloade.ts | 23 ++ .../nestjs-json-rpc/src/lib/types/index.ts | 5 + .../src/lib/types/module-options.ts | 25 ++ .../nestjs-json-rpc/src/lib/types/payloade.ts | 68 ++++ .../nestjs-json-rpc/src/lib/types/utils.ts | 1 + .../nestjs-json-rpc/src/lib/utils/error.ts | 45 +++ .../nestjs-json-rpc/src/lib/utils/index.ts | 1 + libs/json-rpc/nestjs-json-rpc/tsconfig.json | 22 ++ .../nestjs-json-rpc/tsconfig.lib.json | 16 + .../nestjs-json-rpc/tsconfig.spec.json | 14 + tsconfig.base.json | 3 + 41 files changed, 1526 insertions(+), 28 deletions(-) delete mode 100644 .verdaccio/config.yml create mode 100644 apps/json-api-server/src/app/rpc/rpc.module.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/.eslintrc.json create mode 100644 libs/json-rpc/nestjs-json-rpc/README.md create mode 100644 libs/json-rpc/nestjs-json-rpc/jest.config.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/package.json create mode 100644 libs/json-rpc/nestjs-json-rpc/project.json create mode 100644 libs/json-rpc/nestjs-json-rpc/src/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/constants/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/decorators/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/http-transport.module.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/explorer.service.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/nestjs-json-rpc.module.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/providers/async-iterator.provider.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/providers/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/providers/map-handler-store.provider.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/providers/zod-input-data.provider.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/types/error-code-type.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/types/error-payloade.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/types/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/types/module-options.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/types/payloade.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/types/utils.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/utils/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/tsconfig.json create mode 100644 libs/json-rpc/nestjs-json-rpc/tsconfig.lib.json create mode 100644 libs/json-rpc/nestjs-json-rpc/tsconfig.spec.json diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml deleted file mode 100644 index a007fe82..00000000 --- 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/apps/json-api-server/src/app/app.module.ts b/apps/json-api-server/src/app/app.module.ts index 3d7ea992..45403244 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 00000000..7420b11c --- /dev/null +++ b/apps/json-api-server/src/app/rpc/rpc.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; + +@Module({ + imports: [ + NestjsJsonRpcModule.forRootAsync({ + path: 'rpc', + transport: TransportType.HTTP, + }), + ], + // providers: [ContestRpc, ParseIntArrayPipe, LineUpSchemaPipe], +}) +export class RpcModule {} 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 00000000..c9748d24 --- /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 00000000..7fdb71b4 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/README.md @@ -0,0 +1,11 @@ +# nestjs-json-rpc + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build nestjs-json-rpc` to build the library. + +## Running unit tests + +Run `nx test nestjs-json-rpc` to execute the unit tests via [Jest](https://jestjs.io). 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 00000000..f77fba82 --- /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 00000000..44d0fa3e --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/package.json @@ -0,0 +1,10 @@ +{ + "name": "@klerick/nestjs-json-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/json-rpc/nestjs-json-rpc/project.json b/libs/json-rpc/nestjs-json-rpc/project.json new file mode 100644 index 00000000..219a861e --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/project.json @@ -0,0 +1,24 @@ +{ + "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", + "packageJson": "libs/json-rpc/nestjs-json-rpc/package.json", + "main": "libs/json-rpc/nestjs-json-rpc/src/index.ts", + "assets": ["libs/json-rpc/nestjs-json-rpc/*.md"] + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs nestjs-json-rpc {args.ver} {args.tag}", + "dependsOn": ["build"] + } + }, + "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 00000000..06743032 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/index.ts @@ -0,0 +1,7 @@ +export * from './lib/nestjs-json-rpc.module'; +export { TransportType, CommonRpcConfig, ErrorCodeType } from './lib/types'; +export { + fromRpcErrorToRpcErrorObject, + createError, + RpcError, +} from './lib/utils'; 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 00000000..47dfe494 --- /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 00000000..42af9043 --- /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.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.ts new file mode 100644 index 00000000..b9cb28c5 --- /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/error-payloade'; +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.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.ts new file mode 100644 index 00000000..16b204f1 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.ts @@ -0,0 +1,23 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; + +import { + RpcError, + fromRpcErrorToRpcErrorObject, + createError, +} from '../../../utils'; +import { RpcErrorObject, ErrorCodeType } from '../../../types'; + +@Catch() +export class RpcErrorExceptionFilter implements ExceptionFilter { + catch(exception: Error, host: ArgumentsHost): void { + let body: RpcErrorObject; + if (exception instanceof RpcError) { + body = fromRpcErrorToRpcErrorObject(exception); + } else { + body = fromRpcErrorToRpcErrorObject( + createError(ErrorCodeType.ServerError, exception.message) + ); + } + host.switchToHttp().getResponse().send(body); + } +} 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 00000000..b218fe49 --- /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 00000000..e720cd3b --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/index.ts @@ -0,0 +1,2 @@ +export * from './http-transport/http-transport.module'; +export * from './util/util.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 00000000..5bab2d26 --- /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 '@klerick/nestjs-json-rpc'; +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 00000000..68ff6ea8 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.ts @@ -0,0 +1,51 @@ +import { Inject, PipeTransform } from '@nestjs/common'; + +import { ZOD_INPUT_DATA } from '../../../constants'; +import { + ZodPayloadRpc, + ErrorCodeType, + PayloadRpcArray, + PayloadRpc, +} from '../../../types'; +import { createError, RpcError } from '../../../utils'; + +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 00000000..88a0b652 --- /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 00000000..926d7766 --- /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 00000000..b29872fe --- /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(-32600); + } + }); + 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 00000000..5434500d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.ts @@ -0,0 +1,243 @@ +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/error-payloade'; + +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, 1); + } + } + + 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); + } + return { + pipe, + metatype: paramsType[index], + 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.InvalidRequest + : ErrorCodeType.ServerError, + (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 00000000..5c1277d4 --- /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 00000000..b27de31b --- /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 00000000..2e1cc5b0 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/util.module.ts @@ -0,0 +1,35 @@ +import { Inject, Module, OnApplicationBootstrap } from '@nestjs/common'; +import { MAP_HANDLER } from '../../constants'; +import { mapHandlerStoreProvider, AsyncIterate } from '../../providers'; + +import { HandlerService, ExplorerService } from './service'; +import { ModuleRef } from '@nestjs/core'; +import { zodInputDataProvider } from '../../providers/zod-input-data.provider'; + +@Module({ + controllers: [], + providers: [ + mapHandlerStoreProvider, + HandlerService, + ExplorerService, + AsyncIterate, + zodInputDataProvider, + ], + exports: [mapHandlerStoreProvider, HandlerService, AsyncIterate], +}) +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/nestjs-json-rpc.module.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/nestjs-json-rpc.module.ts new file mode 100644 index 00000000..e935678d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/nestjs-json-rpc.module.ts @@ -0,0 +1,37 @@ +import { DynamicModule, Module, Provider } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { JsonRpcConfig, TransportType } from './types'; +import { HttpTransportModule, UtilModule } from './modules'; + +@Module({ + controllers: [], + providers: [], + exports: [], +}) +export class NestjsJsonRpcModule { + static forRootAsync(options: JsonRpcConfig): DynamicModule { + const providers: Provider[] = []; + + 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], + }; + } + default: + throw new Error(`Transport ${options.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 00000000..dd0eef7f --- /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 00000000..2d1906b1 --- /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 00000000..87b19b47 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/map-handler-store.provider.ts @@ -0,0 +1,7 @@ +import { ValueProvider } from '@angular/core'; +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 00000000..5e4ce734 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/providers/zod-input-data.provider.ts @@ -0,0 +1,8 @@ +import { ValueProvider } from '@angular/core'; +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 00000000..def8b9d5 --- /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 00000000..161cc64f --- /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 00000000..fbefa402 --- /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 00000000..86fc93b5 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/types/module-options.ts @@ -0,0 +1,25 @@ +import { + DynamicModule, + ForwardReference, + Provider, + Type, +} from '@nestjs/common'; + +export enum TransportType { + HTTP, + WS, +} + +export type HttpTransportConfig = { + transport: TransportType.HTTP; + path: string; +}; + +export type CommonRpcConfig = { + providers?: Provider[]; + imports?: Array< + Type | DynamicModule | Promise | ForwardReference + >; +}; + +export type JsonRpcConfig = CommonRpcConfig & HttpTransportConfig; 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 00000000..a59c173d --- /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 00000000..5f2cf2cf --- /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 00000000..4127cd59 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts @@ -0,0 +1,45 @@ +import { ErrorCode } from '../constants'; +import { RpcErrorData, RpcErrorObject } from '../types/error-payloade'; + +export class RpcError extends Error { + id: number | null = null; + constructor( + message: keyof typeof ErrorCode, + public code: number, + public data?: RpcErrorData + ) { + super(message); + } +} + +export function createError( + type: keyof typeof ErrorCode, + title?: string, + description?: string +): RpcError { + let data: undefined | RpcErrorData = undefined; + if (title) { + data = { title }; + } + + if (title && description) { + data = { title, description }; + } + + return new RpcError(type, ErrorCode[type], data); +} + +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, + }; +} 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 00000000..93ae819e --- /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 00000000..8122543a --- /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 00000000..dbf54fd7 --- /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 00000000..69a251f3 --- /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/tsconfig.base.json b/tsconfig.base.json index cf0c7f6f..c0008867 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,9 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@klerick/nestjs-json-rpc": [ + "libs/json-rpc/nestjs-json-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"], From eaa48e6cb15a06a7e1bb0bb43e041add49cc6f60 Mon Sep 17 00:00:00 2001 From: Alex H Date: Thu, 14 Mar 2024 16:13:43 +0100 Subject: [PATCH 02/16] feat(nestjs-json-rpc-sdk): sdk for rpc - http transport - factory for native js - angular module Closes: #77 --- apps/json-api-front/src/app/app.component.ts | 32 ++++- apps/json-api-front/src/app/app.config.ts | 11 ++ .../nestjs-json-rpc-sdk/.eslintrc.json | 25 ++++ libs/json-rpc/nestjs-json-rpc-sdk/README.md | 11 ++ .../nestjs-json-rpc-sdk/jest.config.ts | 11 ++ .../json-rpc/nestjs-json-rpc-sdk/package.json | 7 ++ .../json-rpc/nestjs-json-rpc-sdk/project.json | 23 ++++ .../json-rpc/nestjs-json-rpc-sdk/src/index.ts | 1 + .../src/json-rpc-sdk.module.ts | 1 + .../src/lib/constans/index.ts | 1 + .../lib/factory/axios-transport.factory.ts | 34 ++++++ .../src/lib/factory/id-request.spec.ts | 9 ++ .../src/lib/factory/id-request.ts | 2 + .../src/lib/factory/index.ts | 4 + .../src/lib/factory/rpc.factory.ts | 39 ++++++ .../src/lib/factory/transport.factory.ts | 39 ++++++ .../src/lib/json-rpc-angular.ts | 80 ++++++++++++ .../src/lib/nestjs-json-rpc-sdk.spec.ts | 7 ++ .../src/lib/nestjs-json-rpc-sdk.ts | 5 + .../src/lib/types/config.ts | 43 +++++++ .../src/lib/types/index.ts | 4 + .../src/lib/types/rpc-error-object.ts | 40 ++++++ .../nestjs-json-rpc-sdk/src/lib/types/rpc.ts | 75 ++++++++++++ .../src/lib/types/utils.ts | 10 ++ .../src/lib/utils/body.spec.ts | 30 +++++ .../nestjs-json-rpc-sdk/src/lib/utils/body.ts | 19 +++ .../src/lib/utils/index.ts | 4 + .../nestjs-json-rpc-sdk/src/lib/utils/pipe.ts | 40 ++++++ .../src/lib/utils/rpc-batch.spec.ts | 108 +++++++++++++++++ .../src/lib/utils/rpc-batch.ts | 40 ++++++ .../src/lib/utils/rpc-proxy.spec.ts | 48 ++++++++ .../src/lib/utils/rpc-proxy.ts | 30 +++++ .../src/lib/utils/wrapper-call.spec.ts | 39 ++++++ .../src/lib/utils/wrapper-call.ts | 34 ++++++ .../nestjs-json-rpc-sdk/tsconfig.json | 22 ++++ .../nestjs-json-rpc-sdk/tsconfig.lib.json | 10 ++ .../nestjs-json-rpc-sdk/tsconfig.spec.json | 14 +++ libs/json-rpc/nestjs-json-rpc/project.json | 9 ++ .../controllers/json-rpc.controller.spec.ts | 42 +++++++ .../controllers/json-rpc.controller.ts | 2 +- .../filter/rpc-error-exception.filter.spec.ts | 62 ++++++++++ .../modules/util/pipe/input-data.pipe.spec.ts | 2 +- .../lib/modules/util/pipe/input-data.pipe.ts | 3 +- .../src/lib/modules/util/util.module.ts | 12 +- package-lock.json | 114 ++++++------------ package.json | 3 + tsconfig.base.json | 6 + 47 files changed, 1117 insertions(+), 90 deletions(-) create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/.eslintrc.json create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/README.md create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/jest.config.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/package.json create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/project.json create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/json-rpc-sdk.module.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/axios-transport.factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/rpc.factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/transport.factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/json-rpc-angular.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/config.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc-error-object.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/rpc.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/utils.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/body.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/pipe.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-proxy.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/wrapper-call.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.json create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.spec.json create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/controllers/json-rpc.controller.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.spec.ts diff --git a/apps/json-api-front/src/app/app.component.ts b/apps/json-api-front/src/app/app.component.ts index 4f91abb8..30f84451 100644 --- a/apps/json-api-front/src/app/app.component.ts +++ b/apps/json-api-front/src/app/app.component.ts @@ -2,8 +2,23 @@ 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 { switchMap } from 'rxjs'; +interface TestRpc { + test(a: number, b: number): Promise; + test2(firstArg: string, secondArg: number): Promise; +} + +type RpcMap = { + TestRpc: TestRpc; +}; + @Component({ standalone: true, imports: [NxWelcomeComponent], @@ -14,14 +29,19 @@ 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.TestRpc.test(1, 2); + const rpc2 = this.rpc.TestRpc.test2('string', 2); + 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; diff --git a/apps/json-api-front/src/app/app.config.ts b/apps/json-api-front/src/app/app.config.ts index fe16eb73..f02b6456 100644 --- a/apps/json-api-front/src/app/app.config.ts +++ b/apps/json-api-front/src/app/app.config.ts @@ -1,5 +1,9 @@ import { ApplicationConfig, importProvidersFrom } from '@angular/core'; import { JsonApiAngular } from 'json-api-nestjs-sdk/json-api-nestjs-sdk.module'; +import { + JsonRpcAngular, + TransportType, +} from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module'; export const appConfig: ApplicationConfig = { providers: [ @@ -11,5 +15,12 @@ export const appConfig: ApplicationConfig = { operationUrl: 'operation', }) ), + importProvidersFrom( + JsonRpcAngular.forRoot({ + transport: TransportType.HTTP, + rpcPath: 'rpc', + rpcHost: 'http://localhost:4200', + }) + ), ], }; 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 00000000..c9748d24 --- /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 00000000..0b83b728 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/README.md @@ -0,0 +1,11 @@ +# nestjs-json-rpc-sdk + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build nestjs-json-rpc-sdk` to build the library. + +## Running unit tests + +Run `nx test nestjs-json-rpc-sdk` to execute the unit tests via [Jest](https://jestjs.io). 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 00000000..3ea00a7c --- /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/package.json b/libs/json-rpc/nestjs-json-rpc-sdk/package.json new file mode 100644 index 00000000..53bedc30 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/package.json @@ -0,0 +1,7 @@ +{ + "name": "@klerick/nestjs-json-rpc-sdk", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + } +} 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 00000000..dc7adb79 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/project.json @@ -0,0 +1,23 @@ +{ + "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": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/json-rpc/nestjs-json-rpc-sdk", + "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"] + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs nestjs-json-rpc-sdk {args.ver} {args.tag}", + "dependsOn": ["build"] + } + }, + "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 00000000..c7e2c779 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts @@ -0,0 +1 @@ +export * from './lib/nestjs-json-rpc-sdk'; 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 00000000..f3d4ea8f --- /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/constans/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts new file mode 100644 index 00000000..af8cbdd8 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts @@ -0,0 +1 @@ +export const JSON_RPC_VERSION = '2.0'; 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 00000000..451edb73 --- /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/id-request.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/id-request.spec.ts new file mode 100644 index 00000000..b1f55970 --- /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 00000000..c43bbefb --- /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 00000000..da8d7739 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/index.ts @@ -0,0 +1,4 @@ +export * from './axios-transport.factory'; +export * from './id-request'; +export * from './rpc.factory'; +export * from './transport.factory'; 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 00000000..b9006e08 --- /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'; + +type ResultRpcFactory = { + rpc: RpcReturnList; + rpcBatch: RpcBatch; +}; +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, usePromise); + 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 00000000..c8ef5a7d --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/transport.factory.ts @@ -0,0 +1,39 @@ +import { fromFetch } from 'rxjs/fetch'; +import { + RpcConfig, + Transport, + TransportType, + RpcHttpConfig, + LoopFunc, + PayloadRpc, + RpcResult, +} from '../types'; + +function httpTransport( + config: RpcHttpConfig +): Transport { + const url = new URL(config.rpcPath, config.rpcHost).toString(); + if (config.httpAgentFactory) { + return config.httpAgentFactory(url); + } + + return (body: PayloadRpc) => + fromFetch>(url, { + method: 'post', + body: JSON.stringify(body), + selector: (r) => r.json(), + }); +} + +export function transportFactory( + rpcConfig: RpcConfig +): Transport { + switch (rpcConfig.transport) { + case TransportType.HTTP: + return httpTransport(rpcConfig); + case TransportType.WS: + throw new Error('Unknown transport'); + default: + throw new Error('Unknown transport'); + } +} 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 00000000..65d202a1 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/json-rpc-angular.ts @@ -0,0 +1,80 @@ +import { + Component, + inject, + InjectionToken, + ModuleWithProviders, + NgModule, +} from '@angular/core'; +import { HttpClientModule, HttpClient } from '@angular/common/http'; +import { + LoopFunc, + RpcMainHttpConfig, + RpcHttpConfig, + RpcWsConfig, + Transport, + TransportType, + PayloadRpc, + RpcResult, + RpcReturnList, + RpcBatch, +} from './types'; +import { transportFactory } from './factory'; +import { RpcBatchFactory, rpcProxy } from './utils'; + +type Rpc = RpcReturnList; + +export { TransportType, Rpc }; + +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: () => { + const config = inject(JSON_RPC_SDK_CONFIG); + const httpClient = inject(HttpClient); + if (config.transport === TransportType.HTTP) { + (config as unknown as RpcHttpConfig)['httpAgentFactory'] = + (url: string) => (body: PayloadRpc) => { + return httpClient.post>(url, body); + }; + } + return transportFactory(config); + }, + } +); + +export const JSON_RPC = new InjectionToken>( + 'Rpc client', + { + factory: () => + rpcProxy>(inject(JSON_RPC_SDK_TRANSPORT), false), + } +); + +export const RPC_BATCH = new InjectionToken('Rpc client for batch', { + factory: () => RpcBatchFactory(inject(JSON_RPC_SDK_TRANSPORT)), +}); + +export type JsonRpcAngularConfig = RpcMainHttpConfig | RpcWsConfig; + +@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/nestjs-json-rpc-sdk.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.spec.ts new file mode 100644 index 00000000..7959f327 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.spec.ts @@ -0,0 +1,7 @@ +import { nestjsJsonRpcSdk } from './nestjs-json-rpc-sdk'; + +describe('nestjsJsonRpcSdk', () => { + it('should work', () => { + expect(nestjsJsonRpcSdk()).toEqual('nestjs-json-rpc-sdk'); + }); +}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts new file mode 100644 index 00000000..4876f676 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts @@ -0,0 +1,5 @@ +import { RpcConfig } from './types'; + +export function nestjsJsonRpcSdk(rpcConfig: RpcConfig): string { + return 'nestjs-json-rpc-sdk'; +} 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 00000000..23e2d56f --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/config.ts @@ -0,0 +1,43 @@ +import { Observable } from 'rxjs'; +import { Transport } from './rpc'; +import { HttpAgentFactory, LoopFunc } from './utils'; + +export enum TransportType { + HTTP, + WS, +} + +// export type RpcHttpConfig = { +// rpcPath: string; +// rpcHost: string; +// transport: TransportType.HTTP; +// httpAgent?: (data: T) => Promise; +// }; + +// export type HttpConfig = { +// transport: TransportType.HTTP, +// rpcPath: string; +// rpcHost: string; +// httpAgent?: (data: T) => Promise; +// }; + +export type RpcMainHttpConfig = { + transport: TransportType.HTTP; + rpcPath: string; + rpcHost: string; +}; + +export type RpcTransportHttpConfig = { + httpAgentFactory?: HttpAgentFactory>; +}; + +export type RpcHttpConfig = RpcMainHttpConfig & RpcTransportHttpConfig; + +export type RpcWsConfig = { + transport: TransportType.WS; + rpcPath: string; + rpcHost: string; + rpcPort: number; +}; + +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 00000000..4f0e4368 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './config'; +export * from './rpc'; +export * from './rpc-error-object'; +export * from './utils'; 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 00000000..a1c1f30e --- /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 00000000..f16381a8 --- /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 PayloadRpc = { + jsonrpc: JsonRpcVersion; + method: string; + params: Parameters; + id: number; +}; + +export type IdRequest = () => 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 00000000..6a108d0e --- /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 00000000..2dc41189 --- /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 00000000..930bea82 --- /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 00000000..3d459351 --- /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 00000000..2caf9ab6 --- /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 00000000..96ef2726 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/utils/rpc-batch.spec.ts @@ -0,0 +1,108 @@ +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([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 = 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 00000000..5e0ec1d3 --- /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 00000000..059634c6 --- /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 00000000..c81c8f2b --- /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 00000000..c258529c --- /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 00000000..ccfb738a --- /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/id-request'; +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.json b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.json new file mode 100644 index 00000000..8122543a --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/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-sdk/tsconfig.lib.json b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json new file mode 100644 index 00000000..4befa7f0 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/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/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 00000000..69a251f3 --- /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/project.json b/libs/json-rpc/nestjs-json-rpc/project.json index 219a861e..2e8aaeaa 100644 --- a/libs/json-rpc/nestjs-json-rpc/project.json +++ b/libs/json-rpc/nestjs-json-rpc/project.json @@ -18,6 +18,15 @@ "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"] + } } }, "tags": [] 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 00000000..6b9d59ee --- /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 index b9cb28c5..10480024 100644 --- 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 @@ -2,7 +2,7 @@ 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/error-payloade'; +import { RpcErrorObject } from '../../../types'; import { RpcErrorExceptionFilter } from '../filter/rpc-error-exception.filter'; @Controller('/') 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 00000000..8ddd631f --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/http-transport/filter/rpc-error-exception.filter.spec.ts @@ -0,0 +1,62 @@ +import { ArgumentsHost } from '@nestjs/common'; + +import { RpcErrorExceptionFilter } from './rpc-error-exception.filter'; +import { + createError, + fromRpcErrorToRpcErrorObject, + RpcError, +} from '../../../utils'; +import { ErrorCodeType } from '../../../types'; + +import * as ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import Response = ts.server.protocol.Response; +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/util/pipe/input-data.pipe.spec.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/pipe/input-data.pipe.spec.ts index 5bab2d26..703aa7c3 100644 --- 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 @@ -2,7 +2,7 @@ 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 '@klerick/nestjs-json-rpc'; +import { RpcError } from '../../../utils'; import { ErrorCode } from '../../../constants'; describe('input-data.pipe', () => { 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 index 68ff6ea8..19baab01 100644 --- 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 @@ -1,4 +1,4 @@ -import { Inject, PipeTransform } from '@nestjs/common'; +import { Inject, Injectable, PipeTransform } from '@nestjs/common'; import { ZOD_INPUT_DATA } from '../../../constants'; import { @@ -9,6 +9,7 @@ import { } from '../../../types'; import { createError, RpcError } from '../../../utils'; +@Injectable() export class InputDataPipe implements PipeTransform { 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 index 2e1cc5b0..235a0fde 100644 --- 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 @@ -1,10 +1,11 @@ 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 { ModuleRef } from '@nestjs/core'; import { zodInputDataProvider } from '../../providers/zod-input-data.provider'; +import { InputDataPipe } from './pipe/input-data.pipe'; @Module({ controllers: [], @@ -14,8 +15,15 @@ import { zodInputDataProvider } from '../../providers/zod-input-data.provider'; ExplorerService, AsyncIterate, zodInputDataProvider, + InputDataPipe, + ], + exports: [ + mapHandlerStoreProvider, + HandlerService, + AsyncIterate, + zodInputDataProvider, + InputDataPipe, ], - exports: [mapHandlerStoreProvider, HandlerService, AsyncIterate], }) export class UtilModule implements OnApplicationBootstrap { @Inject(MAP_HANDLER) private readonly mapHandler!: Map; diff --git a/package-lock.json b/package-lock.json index 001c0555..f4c49902 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3013,7 +3013,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -3025,7 +3025,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4635,7 +4635,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -4663,7 +4663,7 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "devOptional": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.23", @@ -7319,7 +7319,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 +7360,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -7375,6 +7376,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -7390,6 +7392,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7405,6 +7408,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7420,6 +7424,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7435,6 +7440,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7450,6 +7456,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -7465,6 +7472,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -7480,6 +7488,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" @@ -7495,6 +7504,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -7507,13 +7517,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 +7532,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", @@ -7546,25 +7556,25 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", @@ -7820,7 +7830,7 @@ "version": "18.16.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.20.tgz", "integrity": "sha512-nL54VfDjThdP2UXJXZao5wp76CDiDw4zSRO8d4Tk7UgDqNKGKVEQB0/t3ti63NS+YNNkIQDvwEAF04BO+WYu7Q==", - "devOptional": true + "dev": true }, "node_modules/@types/node-forge": { "version": "1.3.11", @@ -9225,7 +9235,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "devOptional": true, + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -9265,7 +9275,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.4.0" } @@ -9488,7 +9498,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -11332,7 +11342,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "node_modules/critters": { "version": "0.0.20", @@ -11907,7 +11917,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.3.1" } @@ -12177,6 +12187,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -12186,6 +12197,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -15107,23 +15119,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-circus/node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, "node_modules/jest-circus/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15158,24 +15153,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/jest-circus/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -17525,7 +17502,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "node_modules/make-fetch-happen": { "version": "13.0.0", @@ -18883,27 +18860,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi3-ts": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.2.2.tgz", - "integrity": "sha512-+9g4actZKeb3czfi9gVQ4Br2Ju3KwhCAQJBNaKgye5KggqcBLIhFHH+nIkcm0BUX00TrAJl6dH4JWgM4G4JWrw==", - "peer": true, - "dependencies": { - "yaml": "^2.3.4" - } - }, - "node_modules/openapi3-ts/node_modules/yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -22768,7 +22724,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -23253,7 +23209,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23481,7 +23437,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "node_modules/v8-to-istanbul": { "version": "9.2.0", @@ -24556,7 +24512,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index 03ea7336..42bcf34a 100644 --- a/package.json +++ b/package.json @@ -92,5 +92,8 @@ "typescript": "~5.3.2", "verdaccio": "^5.0.4", "webpack-cli": "^5.1.4" + }, + "nx": { + "includedScripts": [] } } diff --git a/tsconfig.base.json b/tsconfig.base.json index c0008867..c2d9ac22 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,12 @@ "@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" + ], "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"], From d21466649aabc0682ab3cf1d788939ca0098f8af Mon Sep 17 00:00:00 2001 From: Alex H Date: Thu, 14 Mar 2024 16:16:36 +0100 Subject: [PATCH 03/16] ci(nestjs-json-rpc-sdk): sync package-lock.json --- package-lock.json | 96 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4c49902..7cdded59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3013,7 +3013,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -3025,7 +3025,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4635,7 +4635,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -4663,7 +4663,7 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.23", @@ -7556,25 +7556,25 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "devOptional": true }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", @@ -7830,7 +7830,7 @@ "version": "18.16.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.20.tgz", "integrity": "sha512-nL54VfDjThdP2UXJXZao5wp76CDiDw4zSRO8d4Tk7UgDqNKGKVEQB0/t3ti63NS+YNNkIQDvwEAF04BO+WYu7Q==", - "dev": true + "devOptional": true }, "node_modules/@types/node-forge": { "version": "1.3.11", @@ -9235,7 +9235,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -9275,7 +9275,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -9498,7 +9498,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/argparse": { "version": "2.0.1", @@ -11342,7 +11342,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/critters": { "version": "0.0.20", @@ -11917,7 +11917,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -12187,7 +12187,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -12197,7 +12196,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -15119,6 +15117,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/jest-circus/node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/jest-circus/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15153,6 +15168,24 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/jest-circus/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -17502,7 +17535,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/make-fetch-happen": { "version": "13.0.0", @@ -18860,6 +18893,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi3-ts": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.2.2.tgz", + "integrity": "sha512-+9g4actZKeb3czfi9gVQ4Br2Ju3KwhCAQJBNaKgye5KggqcBLIhFHH+nIkcm0BUX00TrAJl6dH4JWgM4G4JWrw==", + "peer": true, + "dependencies": { + "yaml": "^2.3.4" + } + }, + "node_modules/openapi3-ts/node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -22724,7 +22778,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -23209,7 +23263,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23437,7 +23491,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "9.2.0", @@ -24512,7 +24566,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } From bb102fbec23e2d0943e5123beadf070ad91b712f Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 20 Mar 2024 17:24:04 +0100 Subject: [PATCH 04/16] ci(nestjs-json-rpc-sdk): fix test and create e2e --- .../json-api/json-rpc/run-json-rpc.spec.ts | 102 ++++++++++++++++++ .../src/json-api/utils/run-ppplication.ts | 24 ++++- .../json-api-server/src/app/rpc/rpc.module.ts | 3 +- .../src/app/rpc/service/rpc.service.ts | 31 ++++++ .../json-rpc/nestjs-json-rpc-sdk/src/index.ts | 8 +- .../src/lib/factory/rpc.factory.ts | 6 +- .../src/lib/json-rpc-angular.ts | 1 - .../src/lib/nestjs-json-rpc-sdk.spec.ts | 7 -- .../src/lib/nestjs-json-rpc-sdk.ts | 5 - .../src/lib/utils/rpc-batch.spec.ts | 7 +- libs/json-rpc/nestjs-json-rpc/src/index.ts | 3 + .../modules/util/service/handler.service.ts | 19 +++- .../nestjs-json-rpc/src/lib/utils/error.ts | 32 +++++- libs/type-for-rpc/.eslintrc.json | 25 +++++ libs/type-for-rpc/README.md | 11 ++ libs/type-for-rpc/jest.config.ts | 11 ++ libs/type-for-rpc/package.json | 10 ++ libs/type-for-rpc/project.json | 19 ++++ libs/type-for-rpc/src/index.ts | 1 + libs/type-for-rpc/src/lib/rpc-service.ts | 15 +++ libs/type-for-rpc/tsconfig.json | 22 ++++ libs/type-for-rpc/tsconfig.lib.json | 10 ++ libs/type-for-rpc/tsconfig.spec.json | 14 +++ tsconfig.base.json | 1 + 24 files changed, 355 insertions(+), 32 deletions(-) create mode 100644 apps/json-api-server-e2e/src/json-api/json-rpc/run-json-rpc.spec.ts create mode 100644 apps/json-api-server/src/app/rpc/service/rpc.service.ts delete mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.spec.ts delete mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts create mode 100644 libs/type-for-rpc/.eslintrc.json create mode 100644 libs/type-for-rpc/README.md create mode 100644 libs/type-for-rpc/jest.config.ts create mode 100644 libs/type-for-rpc/package.json create mode 100644 libs/type-for-rpc/project.json create mode 100644 libs/type-for-rpc/src/index.ts create mode 100644 libs/type-for-rpc/src/lib/rpc-service.ts create mode 100644 libs/type-for-rpc/tsconfig.json create mode 100644 libs/type-for-rpc/tsconfig.lib.json create mode 100644 libs/type-for-rpc/tsconfig.spec.json 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 00000000..7ce927b3 --- /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-ppplication'; + +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/utils/run-ppplication.ts b/apps/json-api-server-e2e/src/json-api/utils/run-ppplication.ts index 922bb7a3..8f1de126 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-ppplication.ts @@ -1,13 +1,19 @@ 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 axios from 'axios'; import { Logger } from 'nestjs-pino'; import { AppModule } from '../../../../json-api-server/src/app/app.module'; import { JsonConfig } from '../../../../../libs/json-api/json-api-nestjs-sdk/src/lib/types'; +import { TransportType } from '@klerick/nestjs-json-rpc-sdk'; export const axiosAdapter = adapterForAxios(axios); let saveApp: INestApplication; @@ -45,3 +51,19 @@ export const creatSdk = (config: Partial = {}) => }, true ); + +export type MapperRpc = { + RpcService: RpcService; +}; + +export const creatRpcSdk = (config: Partial = {}) => + RpcFactory( + { + ...config, + rpcHost: `http://localhost:${port}`, + rpcPath: `${globalPrefix}/rpc`, + transport: TransportType.HTTP, + httpAgentFactory: axiosTransportFactory(axios), + }, + true + ); diff --git a/apps/json-api-server/src/app/rpc/rpc.module.ts b/apps/json-api-server/src/app/rpc/rpc.module.ts index 7420b11c..d86d9d85 100644 --- a/apps/json-api-server/src/app/rpc/rpc.module.ts +++ b/apps/json-api-server/src/app/rpc/rpc.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; +import { RpcService } from './service/rpc.service'; @Module({ imports: [ @@ -8,6 +9,6 @@ import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; transport: TransportType.HTTP, }), ], - // providers: [ContestRpc, ParseIntArrayPipe, LineUpSchemaPipe], + 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 00000000..0e44c56d --- /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/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts index c7e2c779..396c2641 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts @@ -1 +1,7 @@ -export * from './lib/nestjs-json-rpc-sdk'; +export { + axiosTransportFactory, + RpcFactory, + ResultRpcFactoryPromise, + ResultRpcFactory, +} from './lib/factory'; +export { RpcConfig, TransportType, ErrorCodeType, RpcError } from './lib/types'; 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 index b9006e08..1499512d 100644 --- 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 @@ -2,11 +2,11 @@ import { RpcConfig, RpcReturnList, RpcBatch, RpcBatchPromise } from '../types'; import { transportFactory } from './transport.factory'; import { RpcBatchFactory, rpcProxy, RpcBatchFactoryPromise } from '../utils'; -type ResultRpcFactory = { +export type ResultRpcFactory = { rpc: RpcReturnList; rpcBatch: RpcBatch; }; -type ResultRpcFactoryPromise = { +export type ResultRpcFactoryPromise = { rpc: RpcReturnList; rpcForBatch: RpcReturnList; rpcBatch: RpcBatchPromise; @@ -30,7 +30,7 @@ export function RpcFactory( if (usePromise) { rpc = rpcProxy>(transport, usePromise); - rpcForBatch = rpcProxy>(transport, usePromise); + rpcForBatch = rpcProxy>(transport, false); return { rpc, rpcForBatch, rpcBatch: RpcBatchFactoryPromise(transport) }; } else { rpc = rpcProxy>(transport, usePromise); 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 index 65d202a1..993adcca 100644 --- 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 @@ -1,5 +1,4 @@ import { - Component, inject, InjectionToken, ModuleWithProviders, diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.spec.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.spec.ts deleted file mode 100644 index 7959f327..00000000 --- a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { nestjsJsonRpcSdk } from './nestjs-json-rpc-sdk'; - -describe('nestjsJsonRpcSdk', () => { - it('should work', () => { - expect(nestjsJsonRpcSdk()).toEqual('nestjs-json-rpc-sdk'); - }); -}); diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts deleted file mode 100644 index 4876f676..00000000 --- a/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/nestjs-json-rpc-sdk.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { RpcConfig } from './types'; - -export function nestjsJsonRpcSdk(rpcConfig: RpcConfig): string { - return 'nestjs-json-rpc-sdk'; -} 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 index 96ef2726..fdb45b16 100644 --- 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 @@ -58,17 +58,17 @@ describe('rpc-batch', () => { it('RpcBatchFactoryPromise', async () => { const transport = jest.fn().mockImplementationOnce((data) => { - expect(data.map((i: any) => i.id)).toEqual([1, 2, 3]); + expect(data.map((i: any) => i.id)).toEqual([4, 5, 6]); const errorObj = { error: { message: 'ErrroMsg', code: 1, }, - id: 3, + id: 6, }; return of( data.map((i: any) => { - if (i.id === 3) { + if (i.id === 6) { return errorObj; } else { return { @@ -100,7 +100,6 @@ describe('rpc-batch', () => { ) 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/src/index.ts b/libs/json-rpc/nestjs-json-rpc/src/index.ts index 06743032..0cc2cc64 100644 --- a/libs/json-rpc/nestjs-json-rpc/src/index.ts +++ b/libs/json-rpc/nestjs-json-rpc/src/index.ts @@ -4,4 +4,7 @@ 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/modules/util/service/handler.service.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/util/service/handler.service.ts index 5434500d..b2b2f2e0 100644 --- 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 @@ -25,7 +25,7 @@ import { MAP_HANDLER, } from '../../../constants'; import { IterateFactory } from '../../../providers'; -import { RpcErrorObject } from '../../../types/error-payloade'; +import { RpcErrorObject } from '../../../types'; type toString = T extends string ? T : never; @@ -84,7 +84,7 @@ export class HandlerService { } return result; } else { - return this.callRpc(data, 1); + return this.callRpc(data, data.id); } } @@ -99,6 +99,7 @@ export class HandlerService { id, }; } catch (e) { + console.log(e); if (e instanceof RpcError) { return fromRpcErrorToRpcErrorObject(e, id); } @@ -182,9 +183,17 @@ export class HandlerService { 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: paramsType[index], + metatype, params, index, }; @@ -204,8 +213,8 @@ export class HandlerService { } catch (e) { throw createError( e instanceof BadRequestException - ? ErrorCodeType.InvalidRequest - : ErrorCodeType.ServerError, + ? ErrorCodeType.InvalidParams + : ErrorCodeType.InternalError, (e as Error).message, `Argument: #${index}` ); 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 index 4127cd59..e1075007 100644 --- a/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts @@ -1,5 +1,6 @@ import { ErrorCode } from '../constants'; import { RpcErrorData, RpcErrorObject } from '../types/error-payloade'; +import { ErrorCodeType } from '../types'; export class RpcError extends Error { id: number | null = null; @@ -12,11 +13,10 @@ export class RpcError extends Error { } } -export function createError( - type: keyof typeof ErrorCode, +function getErrorData( title?: string, description?: string -): RpcError { +): undefined | RpcErrorData { let data: undefined | RpcErrorData = undefined; if (title) { data = { title }; @@ -25,8 +25,32 @@ export function createError( if (title && description) { data = { title, description }; } + return data; +} - return new RpcError(type, ErrorCode[type], 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( diff --git a/libs/type-for-rpc/.eslintrc.json b/libs/type-for-rpc/.eslintrc.json new file mode 100644 index 00000000..fdc2e11c --- /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 00000000..769c8bac --- /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 00000000..d5f4a6af --- /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 00000000..33c4267e --- /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 00000000..4dc6fb00 --- /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 00000000..92744892 --- /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 00000000..1ddc94a9 --- /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 00000000..f5b85657 --- /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 00000000..33eca2c2 --- /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 00000000..9b2a121d --- /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/tsconfig.base.json b/tsconfig.base.json index c2d9ac22..016220a1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,6 +24,7 @@ "@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"], From 1532cf92eaa1814cfc8f09525790eb657492e3f5 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 20 Mar 2024 17:28:51 +0100 Subject: [PATCH 05/16] ci(nestjs-json-rpc-sdk): exclude type-for-rpc from test run --- .github/workflows/ci.yml | 2 +- .../src/lib/modules/util/service/handler.service.spec.ts | 2 +- .../src/lib/modules/util/service/handler.service.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3c41033..a917a9fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ 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' + 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/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 index b29872fe..1a4f90c9 100644 --- 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 @@ -254,7 +254,7 @@ describe('handler.service', () => { }); } catch (e) { expect(e).toBeInstanceOf(RpcError); - expect((e as RpcError).code).toBe(-32600); + expect((e as RpcError).code).toBe(-32602); } }); it('Should be other error error', async () => { 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 index b2b2f2e0..8df89372 100644 --- 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 @@ -99,7 +99,6 @@ export class HandlerService { id, }; } catch (e) { - console.log(e); if (e instanceof RpcError) { return fromRpcErrorToRpcErrorObject(e, id); } From 31450b3da4aad07b83bb2f4df0dad21175becf4b Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 3 Apr 2024 21:43:35 +0200 Subject: [PATCH 06/16] feat(nestjs-json-rpc,nestjs-json-rpc-sdk): add ws transport Add WebSocket transport for backend Add Websocket transport for sdk, --- apps/json-api-front/proxy.conf.json | 5 + apps/json-api-front/src/app/app.component.ts | 24 +- apps/json-api-front/src/app/app.config.ts | 8 +- .../json-api/json-api-sdk/atomic-sdk.spec.ts | 2 +- .../check-common-decorator.spec.ts | 2 +- .../json-api-sdk/check-othe-call.spec.ts | 2 +- .../json-api/json-api-sdk/get-method.spec.ts | 2 +- .../json-api-sdk/patch-methode.spec.ts | 2 +- .../json-api/json-api-sdk/post-method.spec.ts | 2 +- .../json-api/json-rpc/run-json-rpc.spec.ts | 2 +- .../json-api/json-rpc/run-ws-json-rpc.spec.ts | 102 +++++++ ...{run-ppplication.ts => run-application.ts} | 19 +- .../json-api-server/src/app/rpc/rpc.module.ts | 43 ++- apps/json-api-server/src/main.ts | 4 +- .../src/lib/constans/index.ts | 1 + .../lib/factory/fetch-transport.factory.ts | 13 + .../src/lib/factory/index.ts | 2 + .../src/lib/factory/io-transport.factory.ts | 30 ++ .../src/lib/factory/transport.factory.ts | 25 +- .../src/lib/factory/ws-transport.factory.ts | 46 +++ .../src/lib/types/config.ts | 36 ++- .../nestjs-json-rpc-sdk/src/lib/types/rpc.ts | 2 + .../src/lib/utils/wrapper-call.ts | 2 +- .../filter/rpc-error-exception.filter.spec.ts | 8 +- .../filter/rpc-error-exception.filter.ts | 17 +- .../nestjs-json-rpc/src/lib/modules/index.ts | 1 + .../ws-socket-transport/constants/index.ts | 1 + .../factory/create-gateway.factory.ts | 12 + .../ws-socket-transport/factory/index.ts | 1 + .../rpc-ws-error-exception.filter.spec.ts | 115 ++++++++ .../filter/rpc-ws-error-exception.filter.ts | 19 ++ .../ws-socket-transport/service/index.ts | 1 + .../service/web-socket-gateway.service.ts | 29 ++ .../ws-socket-transport.module.ts | 26 ++ .../src/lib/nestjs-json-rpc.module.ts | 25 +- .../src/lib/types/module-options.ts | 11 +- .../nestjs-json-rpc/src/lib/utils/error.ts | 11 +- package-lock.json | 268 +++++++++++++++++- package.json | 7 +- 39 files changed, 842 insertions(+), 86 deletions(-) create mode 100644 apps/json-api-server-e2e/src/json-api/json-rpc/run-ws-json-rpc.spec.ts rename apps/json-api-server-e2e/src/json-api/utils/{run-ppplication.ts => run-application.ts} (80%) create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/fetch-transport.factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/io-transport.factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/ws-transport.factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/constants/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/create-gateway.factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/factory/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.spec.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/filter/rpc-ws-error-exception.filter.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/index.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/service/web-socket-gateway.service.ts create mode 100644 libs/json-rpc/nestjs-json-rpc/src/lib/modules/ws-socket-transport/ws-socket-transport.module.ts diff --git a/apps/json-api-front/proxy.conf.json b/apps/json-api-front/proxy.conf.json index 63dd6275..6974fc0e 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 30f84451..43a3508d 100644 --- a/apps/json-api-front/src/app/app.component.ts +++ b/apps/json-api-front/src/app/app.component.ts @@ -8,15 +8,11 @@ import { 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'; -interface TestRpc { - test(a: number, b: number): Promise; - test2(firstArg: string, secondArg: number): Promise; -} - type RpcMap = { - TestRpc: TestRpc; + RpcService: IRpcService; }; @Component({ @@ -33,9 +29,15 @@ export class AppComponent implements OnInit { private rpcBatch = inject(RPC_BATCH); ngOnInit(): void { - const rpc1 = this.rpc.TestRpc.test(1, 2); - const rpc2 = this.rpc.TestRpc.test2('string', 2); + 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, @@ -74,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 f02b6456..d916fafd 100644 --- a/apps/json-api-front/src/app/app.config.ts +++ b/apps/json-api-front/src/app/app.config.ts @@ -4,6 +4,7 @@ import { JsonRpcAngular, TransportType, } from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module'; +import io from 'socket.io-client'; export const appConfig: ApplicationConfig = { providers: [ @@ -17,9 +18,12 @@ export const appConfig: ApplicationConfig = { ), importProvidersFrom( JsonRpcAngular.forRoot({ - transport: TransportType.HTTP, + transport: TransportType.WS, rpcPath: 'rpc', - rpcHost: 'http://localhost:4200', + rpcHost: 'ws://localhost:4200', + useWsNativeSocket: true, + // useWsNativeSocket: false, + // webSocketCtor: io('http://localhost:3000', { path: '/rpc' }), }) ), ], 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 2bbf853e..7b7f3c2d 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 91751fb0..47b18d9f 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 3a44773f..076dc277 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 8dec9b0c..03e470cc 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 ffdd50f8..b2bf2263 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 640eb997..ebc9b484 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 index 7ce927b3..3c15ada0 100644 --- 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 @@ -5,7 +5,7 @@ import { RpcError, } from '@klerick/nestjs-json-rpc-sdk'; -import { creatRpcSdk, MapperRpc, run } from '../utils/run-ppplication'; +import { creatRpcSdk, MapperRpc, run } from '../utils/run-application'; let app: INestApplication; 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 00000000..7f0752a6 --- /dev/null +++ b/apps/json-api-server-e2e/src/json-api/json-rpc/run-ws-json-rpc.spec.ts @@ -0,0 +1,102 @@ +import { INestApplication } from '@nestjs/common'; +import { + ResultRpcFactoryPromise, + ErrorCodeType, + RpcError, +} from '@klerick/nestjs-json-rpc-sdk'; + +import { creatWsRpcSdk, MapperRpc, run } from '../utils/run-application'; + +let app: INestApplication; + +beforeAll(async () => { + app = await run(); +}); + +afterAll(async () => { + 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 80% 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 8f1de126..4493a226 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 @@ -7,13 +7,15 @@ import { 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 { TransportType } from '@klerick/nestjs-json-rpc-sdk'; +import { WsAdapter } from '@nestjs/platform-ws'; export const axiosAdapter = adapterForAxios(axios); let saveApp: INestApplication; @@ -32,6 +34,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); @@ -61,9 +64,21 @@ export const creatRpcSdk = (config: Partial = {}) => { ...config, rpcHost: `http://localhost:${port}`, - rpcPath: `${globalPrefix}/rpc`, + rpcPath: `/rpc`, transport: TransportType.HTTP, httpAgentFactory: axiosTransportFactory(axios), }, true ); + +export const creatWsRpcSdk = (config: Partial = {}) => + RpcFactory( + { + transport: TransportType.WS, + useWsNativeSocket: true, + webSocketCtor: WebSocket, + rpcHost: `http://localhost:${port}`, + rpcPath: `/rpc`, + }, + true + ); diff --git a/apps/json-api-server/src/app/rpc/rpc.module.ts b/apps/json-api-server/src/app/rpc/rpc.module.ts index d86d9d85..38cdfb38 100644 --- a/apps/json-api-server/src/app/rpc/rpc.module.ts +++ b/apps/json-api-server/src/app/rpc/rpc.module.ts @@ -1,6 +1,38 @@ -import { Module } from '@nestjs/common'; +import { Injectable, Module, ParseIntPipe, UsePipes } from '@nestjs/common'; import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; import { RpcService } from './service/rpc.service'; +import { + MessageBody, + SubscribeMessage, + WebSocketGateway, + WsResponse, +} from '@nestjs/websockets'; +import { from, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { + ArgumentMetadata, + PipeTransform, +} from '@nestjs/common/interfaces/features/pipe-transform.interface'; + +@WebSocketGateway({ + cors: { + origin: '*', + }, + path: '/rpc/', +}) +class TesWebSocketService { + constructor() { + console.log(1213); + } + + @UsePipes(ParseIntPipe) + @SubscribeMessage('events') + findAll(@MessageBody() data: number): Observable> { + return from([1, 2, 3]).pipe( + map((item) => ({ event: 'events', data: item })) + ); + } +} @Module({ imports: [ @@ -8,6 +40,15 @@ import { RpcService } from './service/rpc.service'; path: 'rpc', transport: TransportType.HTTP, }), + NestjsJsonRpcModule.forRootAsync({ + transport: TransportType.WS, + wsConfig: { + path: '/rpc', + cors: { + origin: '*', + }, + }, + }), ], providers: [RpcService], }) diff --git a/apps/json-api-server/src/main.ts b/apps/json-api-server/src/main.ts index 9fbec19a..4f765097 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-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/constans/index.ts index af8cbdd8..2b6782b9 100644 --- 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 @@ -1 +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/fetch-transport.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/fetch-transport.factory.ts new file mode 100644 index 00000000..2b5307e8 --- /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/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/index.ts index da8d7739..b44a48a6 100644 --- 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 @@ -2,3 +2,5 @@ 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 00000000..d64aeb31 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/io-transport.factory.ts @@ -0,0 +1,30 @@ +import { filter, of, Subject, switchMap, take, tap } from 'rxjs'; +import type { Socket } from 'socket.io-client'; +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; +} + +export function ioTransportFactory( + io: Socket, ClientToServerEvents> +): Transport { + const subjectData = new Subject>(); + io.on(WS_EVENT_NAME, (event) => subjectData.next(event)); + + return (body: PayloadRpc) => { + const { id } = body; + return of(true).pipe( + tap(() => io.emit(WS_EVENT_NAME, body)), + switchMap(() => + subjectData.pipe(filter((response) => response.id === id)) + ), + take(1) + ); + }; +} 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 index c8ef5a7d..025a9e3b 100644 --- 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 @@ -1,13 +1,14 @@ -import { fromFetch } from 'rxjs/fetch'; import { RpcConfig, Transport, TransportType, RpcHttpConfig, + RpcWsConfig, LoopFunc, - PayloadRpc, - RpcResult, } from '../types'; +import { fetchTransportFactory } from './fetch-transport.factory'; +import { wsTransportFactory } from './ws-transport.factory'; +import { ioTransportFactory } from './io-transport.factory'; function httpTransport( config: RpcHttpConfig @@ -17,12 +18,16 @@ function httpTransport( return config.httpAgentFactory(url); } - return (body: PayloadRpc) => - fromFetch>(url, { - method: 'post', - body: JSON.stringify(body), - selector: (r) => r.json(), - }); + return fetchTransportFactory(url); +} + +function wsTransport(config: RpcWsConfig): Transport { + if (config.useWsNativeSocket) { + const url = new URL(config.rpcPath, config.rpcHost).toString(); + return wsTransportFactory(url, config.webSocketCtor); + } + + return ioTransportFactory(config.webSocketCtor); } export function transportFactory( @@ -32,7 +37,7 @@ export function transportFactory( case TransportType.HTTP: return httpTransport(rpcConfig); case TransportType.WS: - throw new Error('Unknown transport'); + 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 00000000..15a13046 --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/ws-transport.factory.ts @@ -0,0 +1,46 @@ +import { filter, of, Subject, switchMap, take, 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'; + +export interface WsResponse { + event: WsEvent; + data: T; +} + +export function wsTransportFactory( + url: string, + webSocketCtor?: any +): Transport { + const subject = webSocket | RpcResult>>({ + url, + ...(webSocketCtor ? { WebSocketCtor: webSocketCtor } : {}), + }); + 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) + ) + .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/types/config.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/config.ts index 23e2d56f..6d264dfb 100644 --- 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 @@ -1,26 +1,13 @@ -import { Observable } from 'rxjs'; import { Transport } from './rpc'; import { HttpAgentFactory, LoopFunc } from './utils'; +import type { Socket } from 'socket.io-client'; + export enum TransportType { HTTP, WS, } -// export type RpcHttpConfig = { -// rpcPath: string; -// rpcHost: string; -// transport: TransportType.HTTP; -// httpAgent?: (data: T) => Promise; -// }; - -// export type HttpConfig = { -// transport: TransportType.HTTP, -// rpcPath: string; -// rpcHost: string; -// httpAgent?: (data: T) => Promise; -// }; - export type RpcMainHttpConfig = { transport: TransportType.HTTP; rpcPath: string; @@ -33,11 +20,22 @@ export type RpcTransportHttpConfig = { export type RpcHttpConfig = RpcMainHttpConfig & RpcTransportHttpConfig; +type UseNativeSocket = + | { + useWsNativeSocket: true; + rpcPath: string; + rpcHost: string; + webSocketCtor?: { + new (url: string, protocols?: string | string[]): any; + }; + } + | { + useWsNativeSocket: false; + webSocketCtor: Socket; + }; + export type RpcWsConfig = { transport: TransportType.WS; - rpcPath: string; - rpcHost: string; - rpcPort: number; -}; +} & UseNativeSocket; export type RpcConfig = RpcHttpConfig | RpcWsConfig; 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 index f16381a8..23be2cc3 100644 --- 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 @@ -4,6 +4,8 @@ import { LoopFunc, ReturnGenericType } from './utils'; export type JsonRpcVersion = '2.0'; +export type WsEvent = 'rpc'; + export type PayloadRpc = { jsonrpc: JsonRpcVersion; method: string; 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 index ccfb738a..5c3cc2d5 100644 --- 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 @@ -1,7 +1,7 @@ import { Observable } from 'rxjs'; import { LoopFunc, PayloadRpc, ReturnTransportCall, Transport } from '../types'; import { generateBody, generateBodyMethod } from './body'; -import { idRequest } from '../factory/id-request'; +import { idRequest } from '../factory'; import { parseResponse, throwRpcError } from './pipe'; export class WrapperCall extends Observable< 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 index 8ddd631f..e4b95dec 100644 --- 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 @@ -1,15 +1,9 @@ import { ArgumentsHost } from '@nestjs/common'; import { RpcErrorExceptionFilter } from './rpc-error-exception.filter'; -import { - createError, - fromRpcErrorToRpcErrorObject, - RpcError, -} from '../../../utils'; +import { createError, fromRpcErrorToRpcErrorObject } from '../../../utils'; import { ErrorCodeType } from '../../../types'; -import * as ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript'; -import Response = ts.server.protocol.Response; import { HttpArgumentsHost } from '@nestjs/common/interfaces'; describe('rpc-error-exception.filter', () => { 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 index 16b204f1..b9c5397c 100644 --- 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 @@ -1,23 +1,10 @@ import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; -import { - RpcError, - fromRpcErrorToRpcErrorObject, - createError, -} from '../../../utils'; -import { RpcErrorObject, ErrorCodeType } from '../../../types'; +import { getBodyError } from '../../../utils'; @Catch() export class RpcErrorExceptionFilter implements ExceptionFilter { catch(exception: Error, host: ArgumentsHost): void { - let body: RpcErrorObject; - if (exception instanceof RpcError) { - body = fromRpcErrorToRpcErrorObject(exception); - } else { - body = fromRpcErrorToRpcErrorObject( - createError(ErrorCodeType.ServerError, exception.message) - ); - } - host.switchToHttp().getResponse().send(body); + host.switchToHttp().getResponse().send(getBodyError(exception)); } } 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 index e720cd3b..d176f45d 100644 --- a/libs/json-rpc/nestjs-json-rpc/src/lib/modules/index.ts +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/modules/index.ts @@ -1,2 +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/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 00000000..d2578e02 --- /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 00000000..d6936486 --- /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 00000000..c568c110 --- /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 00000000..485a61e5 --- /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,115 @@ +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('http://0.0.0.0', {}); + 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 00000000..7ea2be7a --- /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 00000000..128b13b0 --- /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 00000000..5b675a23 --- /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 00000000..4d3c6695 --- /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 index e935678d..1a30a93e 100644 --- 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 @@ -1,7 +1,11 @@ import { DynamicModule, Module, Provider } from '@nestjs/common'; import { RouterModule } from '@nestjs/core'; import { JsonRpcConfig, TransportType } from './types'; -import { HttpTransportModule, UtilModule } from './modules'; +import { + HttpTransportModule, + UtilModule, + WsSocketTransportModule, +} from './modules'; @Module({ controllers: [], @@ -11,7 +15,7 @@ import { HttpTransportModule, UtilModule } from './modules'; export class NestjsJsonRpcModule { static forRootAsync(options: JsonRpcConfig): DynamicModule { const providers: Provider[] = []; - + const { transport } = options; switch (options.transport) { case TransportType.HTTP: { const httpModule = HttpTransportModule.forRoot(providers, [UtilModule]); @@ -30,8 +34,21 @@ export class NestjsJsonRpcModule { exports: [httpModule], }; } - default: - throw new Error(`Transport ${options.transport} not implement`); + 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/types/module-options.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/types/module-options.ts index 86fc93b5..7af5e654 100644 --- 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 @@ -4,6 +4,7 @@ import { Provider, Type, } from '@nestjs/common'; +import { GatewayMetadata } from '@nestjs/websockets/interfaces'; export enum TransportType { HTTP, @@ -15,6 +16,11 @@ export type HttpTransportConfig = { path: string; }; +export type WSTransportConfig = { + transport: TransportType.WS; + wsConfig: GatewayMetadata; +}; + export type CommonRpcConfig = { providers?: Provider[]; imports?: Array< @@ -22,4 +28,7 @@ export type CommonRpcConfig = { >; }; -export type JsonRpcConfig = CommonRpcConfig & HttpTransportConfig; +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/utils/error.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts index e1075007..56a4b3ef 100644 --- a/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts +++ b/libs/json-rpc/nestjs-json-rpc/src/lib/utils/error.ts @@ -1,5 +1,5 @@ import { ErrorCode } from '../constants'; -import { RpcErrorData, RpcErrorObject } from '../types/error-payloade'; +import { RpcErrorData, RpcErrorObject } from '../types'; import { ErrorCodeType } from '../types'; export class RpcError extends Error { @@ -67,3 +67,12 @@ export function fromRpcErrorToRpcErrorObject( 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/package-lock.json b/package-lock.json index 7cdded59..dbb7cb41 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", @@ -7677,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", @@ -7829,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", @@ -10064,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", @@ -12213,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", @@ -21607,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", @@ -24465,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" }, @@ -24497,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 42bcf34a..a5fb64ea 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", From aefaf956f15d45c830d33758fa803bac0a306c04 Mon Sep 17 00:00:00 2001 From: Alex H Date: Thu, 4 Apr 2024 16:50:21 +0200 Subject: [PATCH 07/16] feat(nestjs-json-rpc-sdk): add takeUntil for disconnect socket --- README.md | 2 + apps/json-api-front/src/app/app.config.ts | 68 ++++++++++++-- .../json-api/json-rpc/run-ws-json-rpc.spec.ts | 9 +- .../src/json-api/utils/run-application.ts | 6 +- .../src/lib/angular/factory.ts | 94 +++++++++++++++++++ .../lib/angular/json-rpc-angular.module.ts | 24 +++++ .../src/lib/angular/tokens.ts | 31 ++++++ .../src/lib/factory/io-transport.factory.ts | 59 ++++++++++-- .../src/lib/factory/transport.factory.ts | 32 ++++++- .../src/lib/factory/ws-transport.factory.ts | 20 +++- .../src/lib/json-rpc-angular.ts | 82 +--------------- .../src/lib/types/angular-type.ts | 28 ++++++ .../src/lib/types/config.ts | 43 ++++++++- .../src/lib/types/index.ts | 1 + 14 files changed, 389 insertions(+), 110 deletions(-) create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/factory.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/json-rpc-angular.module.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/tokens.ts create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/src/lib/types/angular-type.ts diff --git a/README.md b/README.md index aeb53707..9bdbabaf 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/src/app/app.config.ts b/apps/json-api-front/src/app/app.config.ts index d916fafd..e6d0f358 100644 --- a/apps/json-api-front/src/app/app.config.ts +++ b/apps/json-api-front/src/app/app.config.ts @@ -1,10 +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 io from 'socket.io-client'; +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: [ @@ -17,14 +67,12 @@ export const appConfig: ApplicationConfig = { }) ), importProvidersFrom( - JsonRpcAngular.forRoot({ - transport: TransportType.WS, - rpcPath: 'rpc', - rpcHost: 'ws://localhost:4200', - useWsNativeSocket: true, - // useWsNativeSocket: false, - // webSocketCtor: io('http://localhost:3000', { path: '/rpc' }), - }) + JsonRpcAngular.forRoot( + // httpConfig + // wsConfig + // wsConfigWithToken, + ioConfig + ) ), ], }; 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 index 7f0752a6..09803e17 100644 --- 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 @@ -5,7 +5,12 @@ import { RpcError, } from '@klerick/nestjs-json-rpc-sdk'; -import { creatWsRpcSdk, MapperRpc, run } from '../utils/run-application'; +import { + creatWsRpcSdk, + MapperRpc, + run, + destroySubject, +} from '../utils/run-application'; let app: INestApplication; @@ -14,6 +19,8 @@ beforeAll(async () => { }); afterAll(async () => { + destroySubject.next(true); + destroySubject.complete(); await app.close(); }); diff --git a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts index 4493a226..0dbe371e 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts @@ -16,6 +16,7 @@ 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; @@ -70,15 +71,16 @@ export const creatRpcSdk = (config: Partial = {}) => }, true ); - +export const destroySubject = new Subject(); export const creatWsRpcSdk = (config: Partial = {}) => RpcFactory( { transport: TransportType.WS, useWsNativeSocket: true, - webSocketCtor: WebSocket, + nativeSocketImplementation: WebSocket, rpcHost: `http://localhost:${port}`, rpcPath: `/rpc`, + destroySubject, }, true ); 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 00000000..7becf8d1 --- /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 00000000..3fe46b93 --- /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 00000000..a979eaef --- /dev/null +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/tokens.ts @@ -0,0 +1,31 @@ +import { InjectionToken } from '@angular/core'; +import { LoopFunc, RpcBatch, RpcReturnList, Transport } from '../types'; + +import { JsonRpcAngularConfig } from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module'; +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/factory/io-transport.factory.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/factory/io-transport.factory.ts index d64aeb31..c6d772c6 100644 --- 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 @@ -1,5 +1,19 @@ -import { filter, of, Subject, switchMap, take, tap } from 'rxjs'; 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'; @@ -11,18 +25,51 @@ 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> + io: Socket, ClientToServerEvents>, + destroyFactory: Subject ): Transport { - const subjectData = new Subject>(); - io.on(WS_EVENT_NAME, (event) => subjectData.next(event)); - + 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(() => - subjectData.pipe(filter((response) => response.id === id)) + socketSubject.pipe(filter((response) => response.id === id)) ), take(1) ); 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 index 025a9e3b..a30e4438 100644 --- 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 @@ -5,10 +5,18 @@ import { RpcHttpConfig, RpcWsConfig, LoopFunc, + PayloadRpc, + RpcResult, } from '../types'; import { fetchTransportFactory } from './fetch-transport.factory'; -import { wsTransportFactory } from './ws-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 @@ -22,12 +30,28 @@ function httpTransport( } function wsTransport(config: RpcWsConfig): Transport { + const destroyFactory = config.destroySubject || new Subject(); if (config.useWsNativeSocket) { - const url = new URL(config.rpcPath, config.rpcHost).toString(); - return wsTransportFactory(url, config.webSocketCtor); + 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.webSocketCtor); + return ioTransportFactory(config.ioSocketInstance, destroyFactory); } export function transportFactory( 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 index 15a13046..0655404b 100644 --- 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 @@ -1,22 +1,31 @@ -import { filter, of, Subject, switchMap, take, tap } from 'rxjs'; +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 wsTransportFactory( +export function webSocketFactory( url: string, webSocketCtor?: any -): Transport { - const subject = webSocket | RpcResult>>({ +): 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( @@ -24,7 +33,8 @@ export function wsTransportFactory( if (typeof response !== 'object' || response === null) return false; return 'event' in response && response['event'] === 'rpc'; }), - map((response) => response.data) + map((response) => response.data), + takeUntil(destroyFactory) ) .subscribe((r) => subjectData.next(r)); 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 index 993adcca..9d185b89 100644 --- 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 @@ -1,79 +1,3 @@ -import { - inject, - InjectionToken, - ModuleWithProviders, - NgModule, -} from '@angular/core'; -import { HttpClientModule, HttpClient } from '@angular/common/http'; -import { - LoopFunc, - RpcMainHttpConfig, - RpcHttpConfig, - RpcWsConfig, - Transport, - TransportType, - PayloadRpc, - RpcResult, - RpcReturnList, - RpcBatch, -} from './types'; -import { transportFactory } from './factory'; -import { RpcBatchFactory, rpcProxy } from './utils'; - -type Rpc = RpcReturnList; - -export { TransportType, Rpc }; - -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: () => { - const config = inject(JSON_RPC_SDK_CONFIG); - const httpClient = inject(HttpClient); - if (config.transport === TransportType.HTTP) { - (config as unknown as RpcHttpConfig)['httpAgentFactory'] = - (url: string) => (body: PayloadRpc) => { - return httpClient.post>(url, body); - }; - } - return transportFactory(config); - }, - } -); - -export const JSON_RPC = new InjectionToken>( - 'Rpc client', - { - factory: () => - rpcProxy>(inject(JSON_RPC_SDK_TRANSPORT), false), - } -); - -export const RPC_BATCH = new InjectionToken('Rpc client for batch', { - factory: () => RpcBatchFactory(inject(JSON_RPC_SDK_TRANSPORT)), -}); - -export type JsonRpcAngularConfig = RpcMainHttpConfig | RpcWsConfig; - -@NgModule({ - imports: [HttpClientModule], -}) -export class JsonRpcAngular { - static forRoot( - config: JsonRpcAngularConfig - ): ModuleWithProviders { - return { - ngModule: JsonRpcAngular, - providers: [ - { - useValue: config, - provide: JSON_RPC_SDK_CONFIG, - }, - ], - }; - } -} +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 00000000..e7ed7f5a --- /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 index 6d264dfb..ff33f905 100644 --- 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 @@ -1,7 +1,10 @@ -import { Transport } from './rpc'; +import { PayloadRpc, RpcResult, 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'; +import { WsResponse } from '../factory/ws-transport.factory'; export enum TransportType { HTTP, @@ -34,8 +37,42 @@ type UseNativeSocket = webSocketCtor: Socket; }; -export type RpcWsConfig = { +export type RpcNativeSocketFactory = { + rpcPath: string; + rpcHost: string; + nativeSocketImplementation?: { + new (url: string, protocols?: string | string[]): any; + }; +}; + +export type RpcNativeSocketInstance = { + nativeSocketInstance: WebSocketSubject< + WsResponse | RpcResult> + >; +}; + +export type RpcNativeSocketTrue = { + useWsNativeSocket: true; +}; + +export type RpcNativeSocketFalse = { + useWsNativeSocket: false; +}; + +export type RpcNativeSocket = RpcNativeSocketTrue & + (RpcNativeSocketFactory | RpcNativeSocketInstance); + +export type RpcWsMainConfig = { transport: TransportType.WS; -} & UseNativeSocket; + 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 index 4f0e4368..5abc2585 100644 --- 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 @@ -2,3 +2,4 @@ export * from './config'; export * from './rpc'; export * from './rpc-error-object'; export * from './utils'; +export * from './angular-type'; From e09dd841a26a62f3abd879c4c3cdee463c02d8f6 Mon Sep 17 00:00:00 2001 From: Alex H Date: Thu, 4 Apr 2024 16:59:11 +0200 Subject: [PATCH 08/16] test: Fix tests --- apps/json-api-server-e2e/src/json-api/utils/run-application.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts index 0dbe371e..b4e57aca 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts @@ -65,7 +65,7 @@ export const creatRpcSdk = (config: Partial = {}) => { ...config, rpcHost: `http://localhost:${port}`, - rpcPath: `/rpc`, + rpcPath: `/api/rpc`, transport: TransportType.HTTP, httpAgentFactory: axiosTransportFactory(axios), }, From bd08c75375ff6bdee74ac02297c94c29bebfe63b Mon Sep 17 00:00:00 2001 From: Alex H Date: Thu, 4 Apr 2024 21:37:57 +0200 Subject: [PATCH 09/16] docs(nestjs-json-rpc,nestjs-json-rpc-sdk): Add readme --- .../json-api-server/src/app/rpc/rpc.module.ts | 38 +-- libs/json-api/json-api-nestjs-sdk/README.md | 2 +- libs/json-rpc/nestjs-json-rpc-sdk/README.md | 264 +++++++++++++++++- .../src/lib/types/config.ts | 21 +- libs/json-rpc/nestjs-json-rpc/README.md | 127 ++++++++- .../src/lib/nestjs-json-rpc.module.ts | 2 +- 6 files changed, 388 insertions(+), 66 deletions(-) diff --git a/apps/json-api-server/src/app/rpc/rpc.module.ts b/apps/json-api-server/src/app/rpc/rpc.module.ts index 38cdfb38..f3a8a998 100644 --- a/apps/json-api-server/src/app/rpc/rpc.module.ts +++ b/apps/json-api-server/src/app/rpc/rpc.module.ts @@ -1,46 +1,14 @@ -import { Injectable, Module, ParseIntPipe, UsePipes } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { NestjsJsonRpcModule, TransportType } from '@klerick/nestjs-json-rpc'; import { RpcService } from './service/rpc.service'; -import { - MessageBody, - SubscribeMessage, - WebSocketGateway, - WsResponse, -} from '@nestjs/websockets'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { - ArgumentMetadata, - PipeTransform, -} from '@nestjs/common/interfaces/features/pipe-transform.interface'; - -@WebSocketGateway({ - cors: { - origin: '*', - }, - path: '/rpc/', -}) -class TesWebSocketService { - constructor() { - console.log(1213); - } - - @UsePipes(ParseIntPipe) - @SubscribeMessage('events') - findAll(@MessageBody() data: number): Observable> { - return from([1, 2, 3]).pipe( - map((item) => ({ event: 'events', data: item })) - ); - } -} @Module({ imports: [ - NestjsJsonRpcModule.forRootAsync({ + NestjsJsonRpcModule.forRoot({ path: 'rpc', transport: TransportType.HTTP, }), - NestjsJsonRpcModule.forRootAsync({ + NestjsJsonRpcModule.forRoot({ transport: TransportType.WS, wsConfig: { path: '/rpc', diff --git a/libs/json-api/json-api-nestjs-sdk/README.md b/libs/json-api/json-api-nestjs-sdk/README.md index 89417633..55ee0cb8 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-rpc/nestjs-json-rpc-sdk/README.md b/libs/json-rpc/nestjs-json-rpc-sdk/README.md index 0b83b728..cce3ecda 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/README.md +++ b/libs/json-rpc/nestjs-json-rpc-sdk/README.md @@ -1,11 +1,265 @@ +

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

+ # nestjs-json-rpc-sdk -This library was generated with [Nx](https://nx.dev). +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 serve 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 { + someMethode(firstArg: number): Promise; + someOtherMethode(firstArg: number, secondArgument: number): Promise; + methodeWithObjectParams(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.someMethode(1).sibcribe(r => console.log(r)) + +const call1 = rpcForBatch.RpcService.someMethode(1); +const call2 = rpcForBatch.RpcService.methodeWithObjectParams({ + a: 1, + b: 2, +}); + +rpcBatch(call1, call2).sibcribe(([result1, result2]) => console.log(result1, result2)) + +``` +That's all:) + +You can use typescript 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 = rpcForBatch.RpcService.someMethode('inccorectParam'); + +``` + + +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 +); +``` + +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), + }, + false +); +const result = await rpcForBatch.RpcService.someMethode(1) + +const call1 = rpcForBatch.RpcService.someMethode(1); +const call2 = rpcForBatch.RpcService.methodeWithObjectParams({ + a: 1, + b: 2, +}); + +const [result1, result2] = await rpcBatch(call1, call2); +``` + +For use **WebSocket** +```typescript +import axios from 'axios'; +import { + RpcFactory, +} from '@klerick/nestjs-json-rpc-sdk'; +import { WebSocket } from 'ws'; +import { webSocket } from 'rxjs/webSocket'; +const someUrl = 'ws://localhost:4200/rpc' +const destroySubject = new Subject(); +const nativeSocketInstance = webSocket(destroySubject); +const { rpc, rpcBatch, rpcForBatch } = RpcFactory( + { + transport: TransportType.WS, + useWsNativeSocket: true, // - Will be use native WebSocket + //nativeSocketImplementation: WebSocket, - 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 axios from 'axios'; +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, rpcForBatch } = RpcFactory( + { + transport: TransportType.WS, + useWsNativeSocket: false, // - Will be use native WebSocket + destroySubject, // - If you need close connection you need call destroySubject.next(true), + ioSocketInstance + }, + false +); +``` + +You can use module for Angular: + +```typescript + +import { + JsonRpcAngular, + JsonRpcAngularConfig, + TransportType, +} from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module' +import { Subject } from 'rxjs'; +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(), +}); -## Building +const tokenSocketInst = new InjectionToken('tokenSocketInst', { + factory: () => webSocket('ws://localhost:4200/rpc'), +}); +const tokenIoSocketInst = new InjectionToken('tokenIoSocketInst', { + factory: () => io('http://localhost:4200', { path: '/rpc' }), +}); -Run `nx build nestjs-json-rpc-sdk` to build the library. +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, +}; -## Running unit tests +bootstrapApplication(AppComponent, { + providers: [ + importProvidersFrom( + JsonRpcAngular.forRoot(httpConfig) + ), + ], +}).catch((err) => + console.error(err) +); -Run `nx test nestjs-json-rpc-sdk` to execute the unit tests via [Jest](https://jestjs.io). +``` 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 index ff33f905..c75a43d1 100644 --- 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 @@ -1,10 +1,9 @@ -import { PayloadRpc, RpcResult, Transport } from './rpc'; +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'; -import { WsResponse } from '../factory/ws-transport.factory'; export enum TransportType { HTTP, @@ -23,20 +22,6 @@ export type RpcTransportHttpConfig = { export type RpcHttpConfig = RpcMainHttpConfig & RpcTransportHttpConfig; -type UseNativeSocket = - | { - useWsNativeSocket: true; - rpcPath: string; - rpcHost: string; - webSocketCtor?: { - new (url: string, protocols?: string | string[]): any; - }; - } - | { - useWsNativeSocket: false; - webSocketCtor: Socket; - }; - export type RpcNativeSocketFactory = { rpcPath: string; rpcHost: string; @@ -46,9 +31,7 @@ export type RpcNativeSocketFactory = { }; export type RpcNativeSocketInstance = { - nativeSocketInstance: WebSocketSubject< - WsResponse | RpcResult> - >; + nativeSocketInstance: WebSocketSubject; }; export type RpcNativeSocketTrue = { diff --git a/libs/json-rpc/nestjs-json-rpc/README.md b/libs/json-rpc/nestjs-json-rpc/README.md index 7fdb71b4..f034d5e5 100644 --- a/libs/json-rpc/nestjs-json-rpc/README.md +++ b/libs/json-rpc/nestjs-json-rpc/README.md @@ -1,11 +1,128 @@ +

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

+ # nestjs-json-rpc -This library was generated with [Nx](https://nx.dev). +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`; + +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 +``` -## Building +- **body** - Create new User and add link to address -Run `nx build nestjs-json-rpc` to build the library. +```json +{"jsonrpc": "2.0", "method": "RpcService.methodeWithObjectParams", "params": {"a": 23}, "id": 1} +``` -## Running unit tests +or RPC call Batch -Run `nx test nestjs-json-rpc` to execute the unit tests via [Jest](https://jestjs.io). +```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/src/lib/nestjs-json-rpc.module.ts b/libs/json-rpc/nestjs-json-rpc/src/lib/nestjs-json-rpc.module.ts index 1a30a93e..ff80490e 100644 --- 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 @@ -13,7 +13,7 @@ import { exports: [], }) export class NestjsJsonRpcModule { - static forRootAsync(options: JsonRpcConfig): DynamicModule { + static forRoot(options: JsonRpcConfig): DynamicModule { const providers: Provider[] = []; const { transport } = options; switch (options.transport) { From fcbc6b0c11f71064d98d46f1e9d94dee8c26b8ef Mon Sep 17 00:00:00 2001 From: Alex H Date: Sat, 6 Apr 2024 12:32:36 +0200 Subject: [PATCH 10/16] docs(nestjs-json-rpc,nestjs-json-rpc-sdk): Add readme --- libs/json-rpc/nestjs-json-rpc-sdk/README.md | 63 ++++++++++++------- .../json-rpc/nestjs-json-rpc-sdk/src/index.ts | 12 +++- .../nestjs-json-rpc-sdk/src/lib/types/rpc.ts | 2 - libs/json-rpc/nestjs-json-rpc/README.md | 17 ++++- libs/json-rpc/nestjs-json-rpc/project.json | 19 ++++++ .../rpc-ws-error-exception.filter.spec.ts | 5 +- 6 files changed, 92 insertions(+), 26 deletions(-) diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/README.md b/libs/json-rpc/nestjs-json-rpc-sdk/README.md index cce3ecda..fd3c7b3b 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/README.md +++ b/libs/json-rpc/nestjs-json-rpc-sdk/README.md @@ -20,7 +20,7 @@ npm install @klerick/nestjs-json-rpc-sdk ## Example Once the installation process is complete, we can import the **RpcFactory**. -For example, we have RPC serve which have service which implement this interface: +For example, we have RPC server which have service which implement this interface: ```typescript export type InputType = { @@ -34,9 +34,9 @@ export type OutputType = { }; export interface RpcService { - someMethode(firstArg: number): Promise; - someOtherMethode(firstArg: number, secondArgument: number): Promise; - methodeWithObjectParams(a: InputType): Promise; + someMethod(firstArg: number): Promise; + someOtherMethod(firstArg: number, secondArgument: number): Promise; + methodWithObjectParams(a: InputType): Promise; } ``` @@ -53,10 +53,10 @@ const { rpc, rpcBatch } = RpcFactory( false ); -rpc.RpcService.someMethode(1).sibcribe(r => console.log(r)) +rpc.RpcService.someMethod(1).sibcribe(r => console.log(r)) -const call1 = rpcForBatch.RpcService.someMethode(1); -const call2 = rpcForBatch.RpcService.methodeWithObjectParams({ +const call1 = rpcForBatch.RpcService.someMethod(1); +const call2 = rpcForBatch.RpcService.methodWithObjectParams({ a: 1, b: 2, }); @@ -66,7 +66,7 @@ rpcBatch(call1, call2).sibcribe(([result1, result2]) => console.log(result1, res ``` That's all:) -You can use typescript type checking: +You can use typescript for type checking: ```typescript import { RpcFactory, @@ -87,7 +87,11 @@ const { rpc, rpcBatch } = RpcFactory( false ); //TS2345: Argument of type string is not assignable to parameter of type number -const call = rpcForBatch.RpcService.someMethode('inccorectParam'); +const call = rpcForBatch.RpcService.someMethod('inccorectParam'); +//TS2339: Property IncorrectService does not exist on type MapperRpc +const call2 = rpcForBatch.IncorrectService.someMethod(1); +//TS2339: Property incorrectMethod does not exist on type RpcService +const call3 = rpcForBatch.RpcService.incorrectMethod(1); ``` @@ -111,6 +115,22 @@ const { rpc, rpcBatch } = RpcFactory( 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** @@ -129,12 +149,12 @@ const { rpc, rpcBatch, rpcForBatch } = RpcFactory( transport: TransportType.HTTP, httpAgentFactory: axiosTransportFactory(axios), }, - false + true // need true for use promise as result ); -const result = await rpcForBatch.RpcService.someMethode(1) +const result = await rpcForBatch.RpcService.someMethod(1) -const call1 = rpcForBatch.RpcService.someMethode(1); -const call2 = rpcForBatch.RpcService.methodeWithObjectParams({ +const call1 = rpcForBatch.RpcService.someMethod(1); +const call2 = rpcForBatch.RpcService.methodWithObjectParams({ a: 1, b: 2, }); @@ -144,20 +164,21 @@ const [result1, result2] = await rpcBatch(call1, call2); For use **WebSocket** ```typescript -import axios from 'axios'; import { RpcFactory, } from '@klerick/nestjs-json-rpc-sdk'; -import { WebSocket } from 'ws'; +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, rpcForBatch } = RpcFactory( + +const { rpc, rpcBatch } = RpcFactory( { transport: TransportType.WS, useWsNativeSocket: true, // - Will be use native WebSocket - //nativeSocketImplementation: WebSocket, - if you use NodeJS you can use other implementation + //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), @@ -168,7 +189,6 @@ const { rpc, rpcBatch, rpcForBatch } = RpcFactory( ``` You can use **socket.io** ```typescript -import axios from 'axios'; import { RpcFactory, } from '@klerick/nestjs-json-rpc-sdk'; @@ -178,10 +198,10 @@ import { io } from 'socket.io-client'; const someUrl = 'ws://localhost:4200' const destroySubject = new Subject(); const ioSocketInstance = io(someUrl, { path: '/rpc' }) -const { rpc, rpcBatch, rpcForBatch } = RpcFactory( +const { rpc, rpcBatch } = RpcFactory( { transport: TransportType.WS, - useWsNativeSocket: false, // - Will be use native WebSocket + useWsNativeSocket: false, // - Will be use socket.io destroySubject, // - If you need close connection you need call destroySubject.next(true), ioSocketInstance }, @@ -189,7 +209,7 @@ const { rpc, rpcBatch, rpcForBatch } = RpcFactory( ); ``` -You can use module for Angular: +You can use Angular module: ```typescript @@ -199,6 +219,7 @@ import { 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, diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts index 396c2641..a4a08917 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts +++ b/libs/json-rpc/nestjs-json-rpc-sdk/src/index.ts @@ -4,4 +4,14 @@ export { ResultRpcFactoryPromise, ResultRpcFactory, } from './lib/factory'; -export { RpcConfig, TransportType, ErrorCodeType, RpcError } from './lib/types'; +export { + RpcConfig, + TransportType, + ErrorCodeType, + RpcError, + LoopFunc, + HttpAgentFactory, + Transport, + PayloadRpc, + RpcResult, +} from './lib/types'; 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 index 23be2cc3..98792a29 100644 --- 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 @@ -13,8 +13,6 @@ export type PayloadRpc = { id: number; }; -export type IdRequest = () => number; - export type RpcResultObject = { jsonrpc: JsonRpcVersion; result: ReturnGenericType; diff --git a/libs/json-rpc/nestjs-json-rpc/README.md b/libs/json-rpc/nestjs-json-rpc/README.md index f034d5e5..19e8e900 100644 --- a/libs/json-rpc/nestjs-json-rpc/README.md +++ b/libs/json-rpc/nestjs-json-rpc/README.md @@ -59,6 +59,20 @@ 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 @@ -103,6 +117,7 @@ export class AppModule { } ``` `@RpcHandler` - decorator which mark class as RPC service + `@RpcParamsPipe` - decorator for validate input data, @@ -112,7 +127,7 @@ After it, you can call you RPC service: POST /rpc ``` -- **body** - Create new User and add link to address +- **body** - for http request ```json {"jsonrpc": "2.0", "method": "RpcService.methodeWithObjectParams", "params": {"a": 23}, "id": 1} diff --git a/libs/json-rpc/nestjs-json-rpc/project.json b/libs/json-rpc/nestjs-json-rpc/project.json index 2e8aaeaa..bb46a4c0 100644 --- a/libs/json-rpc/nestjs-json-rpc/project.json +++ b/libs/json-rpc/nestjs-json-rpc/project.json @@ -27,6 +27,25 @@ "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/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 index 485a61e5..69ae8c9f 100644 --- 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 @@ -10,7 +10,10 @@ import { WS_EVENT_NAME } from '../constants'; describe('rpc-ws-error-exception.filter', () => { describe('WebSocket', () => { - const WebSocketInst = new WebSocket('http://0.0.0.0', {}); + const WebSocketInst = new WebSocket( + 'wss://demo.piesocket.com/v3/channel_123', + {} + ); let argumentsHost: ArgumentsHost; let getClient: () => WebSocket; From b9da21d0ddc88abfb254e0775a2940a42dccd0fc Mon Sep 17 00:00:00 2001 From: Alex H Date: Sat, 6 Apr 2024 12:36:38 +0200 Subject: [PATCH 11/16] ci: try fix nx cache --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a917a9fa..2f78f19a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: key: ${{ runner.os }}-nx-${{ steps.branch-names.outputs.current_branch }} - run: git branch --track main origin/master - name: Test and build + 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 From e5e9936193c6aa6f61d83d8bd5d78265673df2d1 Mon Sep 17 00:00:00 2001 From: Alex H Date: Sat, 6 Apr 2024 15:09:48 +0200 Subject: [PATCH 12/16] fix(json-api-nestjs): Fix validation Fix validation if try create conditional for date filed. If check field for null validation throw error: "String should be as date" Closes: #81 --- .../zod/zod-query-schema/filter.spec.ts | 12 +++++++++++ .../lib/helper/zod/zod-query-schema/filter.ts | 20 +++++++++++++------ .../src/lib/helper/zod/zod-utils.ts | 7 ++++--- .../service/typeorm-utils.service.spec.ts | 6 +++--- 4 files changed, 33 insertions(+), 12 deletions(-) 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 69db0fcd..0a04e1c1 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 fec4b032..99f9c7cb 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 eec27e43..f265e25b 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 2777d323..96aa3b5e 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, }; From 344f9694a23735847226c730fa2b80756ac9e94b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 6 Apr 2024 13:18:35 +0000 Subject: [PATCH 13/16] chore(release): publish - project: json-api-nestjs 7.0.1 --- libs/json-api/json-api-nestjs/CHANGELOG.md | 12 ++++++++++++ libs/json-api/json-api-nestjs/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/json-api/json-api-nestjs/CHANGELOG.md b/libs/json-api/json-api-nestjs/CHANGELOG.md index 9017d972..a9b78b32 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 57ec8e7b..f7885828 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" }, From 26d33653e12ae30a8c8af587a86ef000c09085ce Mon Sep 17 00:00:00 2001 From: Alex H Date: Sat, 6 Apr 2024 15:23:08 +0200 Subject: [PATCH 14/16] ci: remove command ls from ci --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ea608c30..ffbf673f 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 From 512aedfa4edf2c0866fa338c7e49559ef550dde7 Mon Sep 17 00:00:00 2001 From: Alex H Date: Mon, 8 Apr 2024 12:54:00 +0200 Subject: [PATCH 15/16] ci(nestjs-json-rpc-sdk): Prepare task for publish npm --- .../json-api/json-api-nestjs-sdk/project.json | 2 +- libs/json-rpc/nestjs-json-rpc-sdk/README.md | 8 +- .../nestjs-json-rpc-sdk/ng-package.json | 7 ++ .../json-rpc/nestjs-json-rpc-sdk/package.json | 31 ++++++- .../json-rpc/nestjs-json-rpc-sdk/project.json | 83 ++++++++++++++++++- .../src/lib/angular/tokens.ts | 9 +- .../nestjs-json-rpc-sdk/tsconfig-mjs.lib.json | 19 +++++ .../nestjs-json-rpc-sdk/tsconfig.json | 10 ++- .../nestjs-json-rpc-sdk/tsconfig.lib.json | 4 +- .../preparation-hybrid-npm-package.mjs | 8 +- tools/scripts/prepare-for-angular.mjs | 26 +++++- 11 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/ng-package.json create mode 100644 libs/json-rpc/nestjs-json-rpc-sdk/tsconfig-mjs.lib.json diff --git a/libs/json-api/json-api-nestjs-sdk/project.json b/libs/json-api/json-api-nestjs-sdk/project.json index 6b3576a1..6d8102a2 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-rpc/nestjs-json-rpc-sdk/README.md b/libs/json-rpc/nestjs-json-rpc-sdk/README.md index fd3c7b3b..7df1511c 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/README.md +++ b/libs/json-rpc/nestjs-json-rpc-sdk/README.md @@ -87,11 +87,11 @@ const { rpc, rpcBatch } = RpcFactory( false ); //TS2345: Argument of type string is not assignable to parameter of type number -const call = rpcForBatch.RpcService.someMethod('inccorectParam'); +const call = rpc.RpcService.someMethod('inccorectParam'); //TS2339: Property IncorrectService does not exist on type MapperRpc -const call2 = rpcForBatch.IncorrectService.someMethod(1); +const call2 = rpc.IncorrectService.someMethod(1); //TS2339: Property incorrectMethod does not exist on type RpcService -const call3 = rpcForBatch.RpcService.incorrectMethod(1); +const call3 = rpc.RpcService.incorrectMethod(1); ``` @@ -151,7 +151,7 @@ const { rpc, rpcBatch, rpcForBatch } = RpcFactory( }, true // need true for use promise as result ); -const result = await rpcForBatch.RpcService.someMethod(1) +const result = await rpc.RpcService.someMethod(1) const call1 = rpcForBatch.RpcService.someMethod(1); const call2 = rpcForBatch.RpcService.methodWithObjectParams({ 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 00000000..e91ee026 --- /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 index 53bedc30..3a4a13b2 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/package.json +++ b/libs/json-rpc/nestjs-json-rpc-sdk/package.json @@ -1,7 +1,30 @@ { "name": "@klerick/nestjs-json-rpc-sdk", - "version": "0.0.1", - "dependencies": { - "tslib": "^2.3.0" - } + "version": "1.0.0", + "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 index dc7adb79..98f4a9a1 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/project.json +++ b/libs/json-rpc/nestjs-json-rpc-sdk/project.json @@ -4,19 +4,98 @@ "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", + "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"] + "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/lib/angular/tokens.ts b/libs/json-rpc/nestjs-json-rpc-sdk/src/lib/angular/tokens.ts index a979eaef..0f2b07a1 100644 --- 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 @@ -1,7 +1,12 @@ import { InjectionToken } from '@angular/core'; -import { LoopFunc, RpcBatch, RpcReturnList, Transport } from '../types'; +import { + LoopFunc, + RpcBatch, + RpcReturnList, + Transport, + JsonRpcAngularConfig, +} from '../types'; -import { JsonRpcAngularConfig } from '@klerick/nestjs-json-rpc-sdk/json-rpc-sdk.module'; import { angularTransportFactory, rpcBatchFactory, 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 00000000..b7ab9544 --- /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 index 8122543a..bf7d88a8 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.json +++ b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.json @@ -1,13 +1,16 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "module": "commonjs", + "outDir": "../../../dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "inlineSources": true, + "inlineSourceMap": true, + "sourceMap": false }, "files": [], "include": [], @@ -15,6 +18,9 @@ { "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 index 4befa7f0..81ec818a 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json +++ b/libs/json-rpc/nestjs-json-rpc-sdk/tsconfig.lib.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, + "module": "commonjs", + "declaration": false, "types": ["node"] }, "include": ["src/**/*.ts"], diff --git a/tools/scripts/preparation-hybrid-npm-package.mjs b/tools/scripts/preparation-hybrid-npm-package.mjs index e3bc0b42..35f53f17 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) diff --git a/tools/scripts/prepare-for-angular.mjs b/tools/scripts/prepare-for-angular.mjs index 01226937..3020a145 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(() => { From c852bff17b29e5d4cdebecf54ead06e987d6650f Mon Sep 17 00:00:00 2001 From: Alex H Date: Fri, 12 Apr 2024 15:04:17 +0200 Subject: [PATCH 16/16] chore(nestjs-json-rpc,nestjs-json-rpc-sdk): Prepare npm package before publish --- .../json-rpc/nestjs-json-rpc-sdk/package.json | 2 +- libs/json-rpc/nestjs-json-rpc/package.json | 35 ++++++++++++++++--- libs/json-rpc/nestjs-json-rpc/project.json | 6 ++-- .../providers/map-handler-store.provider.ts | 2 +- .../lib/providers/zod-input-data.provider.ts | 2 +- .../preparation-hybrid-npm-package.mjs | 24 ++++++++++--- 6 files changed, 58 insertions(+), 13 deletions(-) diff --git a/libs/json-rpc/nestjs-json-rpc-sdk/package.json b/libs/json-rpc/nestjs-json-rpc-sdk/package.json index 3a4a13b2..86da19e7 100644 --- a/libs/json-rpc/nestjs-json-rpc-sdk/package.json +++ b/libs/json-rpc/nestjs-json-rpc-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@klerick/nestjs-json-rpc-sdk", - "version": "1.0.0", + "version": "0.0.1", "engines": { "node": ">= 16.0.0" }, diff --git a/libs/json-rpc/nestjs-json-rpc/package.json b/libs/json-rpc/nestjs-json-rpc/package.json index 44d0fa3e..7ef9a81f 100644 --- a/libs/json-rpc/nestjs-json-rpc/package.json +++ b/libs/json-rpc/nestjs-json-rpc/package.json @@ -1,10 +1,37 @@ { "name": "@klerick/nestjs-json-rpc", "version": "0.0.1", - "dependencies": { - "tslib": "^2.3.0" + "engines": { + "node": ">= 16.0.0" }, "type": "commonjs", - "main": "./src/index.js", - "typings": "./src/index.d.ts" + "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 index bb46a4c0..b40d8afb 100644 --- a/libs/json-rpc/nestjs-json-rpc/project.json +++ b/libs/json-rpc/nestjs-json-rpc/project.json @@ -10,9 +10,11 @@ "options": { "outputPath": "dist/libs/json-rpc/nestjs-json-rpc", "tsConfig": "libs/json-rpc/nestjs-json-rpc/tsconfig.lib.json", - "packageJson": "libs/json-rpc/nestjs-json-rpc/package.json", "main": "libs/json-rpc/nestjs-json-rpc/src/index.ts", - "assets": ["libs/json-rpc/nestjs-json-rpc/*.md"] + "assets": ["libs/json-rpc/nestjs-json-rpc/*.md"], + "updateBuildableProjectDepsInPackageJson": true, + "buildableProjectDepsInPackageJsonType": "peerDependencies", + "generateExportsField": true } }, "publish": { 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 index 87b19b47..07199f0d 100644 --- 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 @@ -1,4 +1,4 @@ -import { ValueProvider } from '@angular/core'; +import { ValueProvider } from '@nestjs/common'; import { MAP_HANDLER } from '../constants'; export const mapHandlerStoreProvider: ValueProvider = { 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 index 5e4ce734..e7de6bd0 100644 --- 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 @@ -1,4 +1,4 @@ -import { ValueProvider } from '@angular/core'; +import { ValueProvider } from '@nestjs/common'; import { ZOD_INPUT_DATA } from '../constants'; import { ZPayloadRpc } from '../types'; diff --git a/tools/scripts/preparation-hybrid-npm-package.mjs b/tools/scripts/preparation-hybrid-npm-package.mjs index 35f53f17..c999903f 100644 --- a/tools/scripts/preparation-hybrid-npm-package.mjs +++ b/tools/scripts/preparation-hybrid-npm-package.mjs @@ -87,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) { + +}