From fbd7a2229ba1b829f2fad823c912b822b361d89c Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Sat, 31 Aug 2019 00:31:06 +0700 Subject: [PATCH] Do not use proxy in createLocalClient, add bulk methods --- packages/jsonrpc/src/index.ts | 1 + packages/jsonrpc/src/local.ts | 27 +++++--- packages/jsonrpc/src/util.test.ts | 107 ++++++++++++++++++++++++++++++ packages/jsonrpc/src/util.ts | 48 ++++++++++++++ 4 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 packages/jsonrpc/src/util.test.ts create mode 100644 packages/jsonrpc/src/util.ts diff --git a/packages/jsonrpc/src/index.ts b/packages/jsonrpc/src/index.ts index 7b86b52..f5b70ac 100644 --- a/packages/jsonrpc/src/index.ts +++ b/packages/jsonrpc/src/index.ts @@ -6,3 +6,4 @@ export * from './local' export * from './redux' export * from './remote' export * from './types' +export * from './util' diff --git a/packages/jsonrpc/src/local.ts b/packages/jsonrpc/src/local.ts index 9155c4d..8272486 100644 --- a/packages/jsonrpc/src/local.ts +++ b/packages/jsonrpc/src/local.ts @@ -1,23 +1,30 @@ import {TAsyncified, Contextual, ReverseContextual} from './types' import {Request} from 'express' import {TGetContext} from './express' +import {getAllMethods} from './jsonrpc' + +export type LocalClient = TAsyncified> /** * 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. + * + * The service argument is expected to be a class implementing the + * Contextual type. The first (context) argument will be + * automatically removed from all methods in the service, and the supplied + * context argument will be used instead. */ export function createLocalClient( service: T, context: Context, -): TAsyncified> { - const proxy = new Proxy({}, { - get(obj, prop) { - return async function makeRequest(...args: any[]) { - const result = (service as any)[prop](context, ...args) - return result - } - }, - }) - return proxy as any +): LocalClient { + return getAllMethods(service) + .filter(prop => typeof service[prop] === 'function') + .reduce((obj, prop) => { + obj[prop] = function makeRequest(...args: any[]) { + return (service as any)[prop](context, ...args) + } + return obj + }, {} as any) } diff --git a/packages/jsonrpc/src/util.test.ts b/packages/jsonrpc/src/util.test.ts new file mode 100644 index 0000000..f3a3dc3 --- /dev/null +++ b/packages/jsonrpc/src/util.test.ts @@ -0,0 +1,107 @@ +import * as util from './util' +import express from 'express' +import {Contextual} from './types' +import {jsonrpc} from './express' +import {noopLogger} from './test-utils' +import {createClient} from './supertest' +import {json} from 'body-parser' + +describe('util', () => { + + interface IS1 { + add(a: number, b: number): number + } + + interface IS2 { + mul(a: number, b: number): number + concat(...str: string[]): string + } + + interface IContext { + userId: number + } + + class Service1 implements Contextual { + add(cx: IContext, a: number, b: number) { + return a + b + cx.userId + } + } + + class Service2 implements Contextual { + mul(cx: IContext, a: number, b: number) { + return a * b + cx.userId + } + concat(cx: IContext, ...str: string[]) { + return str.join('') + cx.userId + } + } + + const services = { + s1: new Service1(), + s2: new Service2(), + } + + describe('bulkCreateLocalClient', () => { + it('creates a typed local client', async () => { + const client = util.bulkCreateLocalClient(services, {userId: 10}) + + const r1: number = await client.s1.add(1, 2) + expect(r1).toBe(13) + + const r2: number = await client.s2.mul(2, 3) + expect(r2).toBe(16) + + const r3: string = await client.s2.concat('a', 'b') + expect(r3).toBe('ab10') + }) + }) + + describe('bulkCreateActions', () => { + it('creates typed actions', async () => { + const client = util.bulkCreateLocalClient(services, {userId: 10}) + const actions = util.bulkCreateActions(client) + + const r1 = actions.s1.add(1, 2) + const method1: 'add' = r1.method + const status1: 'pending' = r1.status + const type1: 's1' = r1.type + const payload1: Promise = r1.payload + expect(type1).toBe('s1') + expect(status1).toBe('pending') + expect(method1).toBe('add') + expect(await payload1).toBe(13) + + const r2 = actions.s2.mul(2, 3) + const method2: 'mul' = r2.method + const status2: 'pending' = r2.status + const type2: 's2' = r2.type + const payload2: Promise = r2.payload + expect(type2).toBe('s2') + expect(status2).toBe('pending') + expect(method2).toBe('mul') + expect(await payload2).toBe(16) + }) + }) + + describe('bulkJSONRPC', () => { + const getContext = () => ({userId: 10}) + function createApp(router: express.Router) { + const app = express() + app.use(json()) + app.use('/rpc', router) + return app + } + + it('creates JSON RPC services', async () => { + const router = util.bulkjsonrpc( + jsonrpc(getContext, noopLogger), + services, + ) + const app = createApp(router) + const client = createClient(app, '/rpc/s1') + const result: number = await client.add(1, 3) + expect(result).toBe(14) + }) + }) + +}) diff --git a/packages/jsonrpc/src/util.ts b/packages/jsonrpc/src/util.ts new file mode 100644 index 0000000..7c6ad8b --- /dev/null +++ b/packages/jsonrpc/src/util.ts @@ -0,0 +1,48 @@ +import {IJSONRPCReturnType} from './express' +import {TAsyncified, TReduxed} from './types' +import {createActions} from './redux' +import {createLocalClient, LocalClient} from './local' + +function keys(obj: T): Array { + return Object.keys(obj) as Array +} + +type BulkLocalClient = {[K in keyof T & string]: LocalClient} +type BulkActions = {[K in keyof T & string]: TReduxed} +type BulkRemoteClient = {[K in keyof T & string]: TAsyncified} + +function bulkCreate( + src: T, + mapValue: (key: K, value: T[K]) => any, +) { + return keys(src).reduce((obj, key) => { + const value = mapValue(key, src[key]) + obj[key] = value + return obj + }, {} as any) +} + +export function bulkCreateLocalClient( + src: T, + context: Cx, +): BulkLocalClient { + return bulkCreate(src, (key, value) => createLocalClient(value, context)) +} + +export function bulkCreateActions>>( + src: T, +): BulkActions { + return bulkCreate(src, (key, value) => createActions(value, key)) +} + +export function bulkjsonrpc( + jsonrpc: IJSONRPCReturnType, + services: T, +) { + keys(services).forEach(key => { + const service = services[key] + jsonrpc.addService('/' + key, service) + }) + + return jsonrpc.router() +}