diff --git a/packages/jsonrpc/jest.config.js b/packages/jsonrpc/jest.config.js index 3a4457a..b3171a7 100644 --- a/packages/jsonrpc/jest.config.js +++ b/packages/jsonrpc/jest.config.js @@ -1,4 +1,9 @@ module.exports = { + globals: { + 'ts-jest': { + compiler: 'ttypescript' + } + }, roots: [ '/src' ], @@ -13,6 +18,5 @@ module.exports = { 'jsx' ], setupFiles: ['/jest.setup.js'], - maxConcurrency: 1, verbose: false } diff --git a/packages/jsonrpc/src/client.ts b/packages/jsonrpc/src/client.ts new file mode 100644 index 0000000..1fdc768 --- /dev/null +++ b/packages/jsonrpc/src/client.ts @@ -0,0 +1,2 @@ +export * from './local' +export * from './remote' diff --git a/packages/jsonrpc/src/index.ts b/packages/jsonrpc/src/index.ts index 3718365..7636a1f 100644 --- a/packages/jsonrpc/src/index.ts +++ b/packages/jsonrpc/src/index.ts @@ -1 +1,2 @@ +export * from './client' export * from './server' diff --git a/packages/jsonrpc/src/local.test.ts b/packages/jsonrpc/src/local.test.ts new file mode 100644 index 0000000..52c2677 --- /dev/null +++ b/packages/jsonrpc/src/local.test.ts @@ -0,0 +1,44 @@ +import {createLocalClient} from './local' +import {keys} from 'ts-transformer-keys' + +describe('local', () => { + + interface IService { + add(a: number, b: number): number + addWithContext(a: number, b: number): (context: IContext) => number + } + const IServiceKeys = keys() + + interface IContext { + userId: 1000 + } + + class Service implements IService { + add(a: number, b: number) { + return a + b + } + addWithContext = (a: number, b: number) => (context: IContext) => { + return a + b + context.userId + } + } + + const service = new Service() + const proxy = createLocalClient(service, { + userId: 1000, + }) + + describe('add', () => { + it('should add two numbers', async () => { + const result = await proxy.add(8, 9) + expect(result).toBe(8 + 9) + }) + }) + + describe('addWithContext', () => { + it('should add two numbers with context', async () => { + const result = await proxy.addWithContext(8, 9) + expect(result).toBe(1000 + 8 + 9) + }) + }) + +}) diff --git a/packages/jsonrpc/src/local.ts b/packages/jsonrpc/src/local.ts new file mode 100644 index 0000000..c917e4c --- /dev/null +++ b/packages/jsonrpc/src/local.ts @@ -0,0 +1,21 @@ +import {Asyncified} from './types' + +/** + * Creates a local client for a specific service instance. The actual service + * will be invoked as if it would be remotely. This helps keep the API similar + * on the client- and server-side. + */ +export function createLocalClient(service: T, context: any) { + const proxy = new Proxy({}, { + get(obj, prop) { + return async function makeRequest(...args: any[]) { + const result = (service as any)[prop](...args) + if (typeof result === 'function') { + return result(context) + } + return result + } + }, + }) + return proxy as Asyncified +} diff --git a/packages/jsonrpc/src/remote.test.ts b/packages/jsonrpc/src/remote.test.ts new file mode 100644 index 0000000..1394485 --- /dev/null +++ b/packages/jsonrpc/src/remote.test.ts @@ -0,0 +1,60 @@ +/** + * @jest-environment node + */ +import bodyParser from 'body-parser' +import express from 'express' +import {AddressInfo} from 'net' +import {Server} from 'http' +import {createRemoteClient} from './remote' +import {jsonrpc} from './server' +import {keys} from 'ts-transformer-keys' + +describe('remote', () => { + + interface IService { + add(a: number, b: number): number + } + const IServiceKeys = keys() + + class Service implements IService { + add(a: number, b: number) { + return a + b + } + } + + const service = new Service() + + function createApp() { + const a = express() + a.use(bodyParser.json()) + a.use('/myService', jsonrpc(service, IServiceKeys, () => ({}))) + return a + } + + const app = createApp() + + let server: Server + let baseUrl: string + beforeEach(async () => { + await new Promise(resolve => { + server = app.listen(0, '127.0.0.1', resolve) + }) + const addr = server.address() as AddressInfo + baseUrl = `http://${addr.address}:${addr.port}` + }) + + afterEach(() => { + return new Promise(resolve => { + server.close(resolve) + }) + }) + + describe('method invocation', () => { + it('creates a proxy for remote service', async () => { + const s = createRemoteClient( + baseUrl, '/myService', IServiceKeys) + const result = await s.add(3, 7) + expect(result).toBe(3 + 7) + }) + }) +}) diff --git a/packages/jsonrpc/src/remote.ts b/packages/jsonrpc/src/remote.ts new file mode 100644 index 0000000..d405387 --- /dev/null +++ b/packages/jsonrpc/src/remote.ts @@ -0,0 +1,40 @@ +import Axios from 'axios' +import {Asyncified} from './types' + +export function createRemoteClient( + baseUrl: string, + url: string, + methods: Array, +) { + const axios = Axios.create({ + baseURL: baseUrl, + }) + + let id = 0 + const service = methods.reduce((obj, method) => { + obj[method] = async function makeRequest(...args: any[]) { + id++ + const response = await axios({ + method: 'post', + url, + headers: { + 'content-type': 'application/json', + }, + data: { + id, + jsonrpc: '2.0', + method, + params: args, + }, + }) + if (response.data.error) { + // TODO create an actual error + throw response.data.error + } + return response.data.result + } + return obj + }, {} as any) + + return service as Asyncified +} diff --git a/packages/jsonrpc/src/server.test.ts b/packages/jsonrpc/src/server.test.ts index 3097e0a..7074457 100644 --- a/packages/jsonrpc/src/server.test.ts +++ b/packages/jsonrpc/src/server.test.ts @@ -1,7 +1,8 @@ -import {jsonrpc} from './server' -import request from 'supertest' -import express from 'express' import bodyParser from 'body-parser' +import express from 'express' +import request from 'supertest' +import {createClient} from './supertest' +import {jsonrpc} from './server' describe('jsonrpc', () => { @@ -65,42 +66,7 @@ describe('jsonrpc', () => { return app } - type ArgumentTypes = T extends (...args: infer U) => infer R ? U : never - type RetType = T extends (...args: any[]) => infer R ? R : never - type UnwrapHOC = T extends (...args: any[]) => infer R ? R : T - type RetProm = T extends Promise ? T : Promise - type PromisifyReturnType = (...a: ArgumentTypes) => - RetProm>> - type Asyncified = { - [K in keyof T]: PromisifyReturnType - } - - function createClient(app: express.Application) { - let id = 0 - const proxy = new Proxy({}, { - get(obj, prop) { - id++ - return async function makeRequest(...args: any[]) { - const result = await request(app) - .post('/myService') - .send({ - jsonrpc: '2.0', - id, - method: prop, - params: args, - }) - const {body} = result - if (body.error) { - throw body.error - } - return body.result - } - }, - }) - return proxy as Asyncified - } - - const client = createClient(createApp()) + const client = createClient(createApp(), '/myService') async function getError(promise: Promise) { let error diff --git a/packages/jsonrpc/src/supertest.ts b/packages/jsonrpc/src/supertest.ts new file mode 100644 index 0000000..be88b78 --- /dev/null +++ b/packages/jsonrpc/src/supertest.ts @@ -0,0 +1,30 @@ +import request from 'supertest' +import {Application} from 'express' +import {Asyncified} from './types' + +export function createClient( + app: Application, path: string, +) { + let id = 0 + const proxy = new Proxy({}, { + get(obj, prop) { + id++ + return async function makeRequest(...args: any[]) { + const result = await request(app) + .post(path) + .send({ + jsonrpc: '2.0', + id, + method: prop, + params: args, + }) + const {body} = result + if (body.error) { + throw body.error + } + return body.result + } + }, + }) + return proxy as Asyncified +} diff --git a/packages/jsonrpc/src/types.ts b/packages/jsonrpc/src/types.ts new file mode 100644 index 0000000..794cbe5 --- /dev/null +++ b/packages/jsonrpc/src/types.ts @@ -0,0 +1,10 @@ +export type ArgumentTypes = + T extends (...args: infer U) => infer R ? U : never +export type RetType = T extends (...args: any[]) => infer R ? R : never +export type UnwrapHOC = T extends (...args: any[]) => infer R ? R : T +export type RetProm = T extends Promise ? T : Promise +export type PromisifyReturnType = (...a: ArgumentTypes) => + RetProm>> +export type Asyncified = { + [K in keyof T]: PromisifyReturnType +} diff --git a/packages/tsconfig.common.json b/packages/tsconfig.common.json index 44cf39d..f19b9ca 100644 --- a/packages/tsconfig.common.json +++ b/packages/tsconfig.common.json @@ -20,6 +20,8 @@ "plugins": [{ "name": "typescript-tslint-plugin", "suppressWhileTypeErrorsPresent": true + }, { + "transform": "ts-transformer-keys/transformer" }] } }