Do not use proxy in createLocalClient, add bulk methods

This commit is contained in:
Jerko Steiner 2019-08-31 00:31:06 +07:00
parent d2a5a35543
commit fbd7a2229b
4 changed files with 173 additions and 10 deletions

View File

@ -6,3 +6,4 @@ export * from './local'
export * from './redux'
export * from './remote'
export * from './types'
export * from './util'

View File

@ -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<T> = TAsyncified<ReverseContextual<T>>
/**
* 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<Service, Context> 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<T extends {}, Context>(
service: T,
context: Context,
): TAsyncified<ReverseContextual<T>> {
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<T> {
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)
}

View File

@ -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<IS1, IContext> {
add(cx: IContext, a: number, b: number) {
return a + b + cx.userId
}
}
class Service2 implements Contextual<IS2, IContext> {
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<number> = 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<number> = 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<IS1>(app, '/rpc/s1')
const result: number = await client.add(1, 3)
expect(result).toBe(14)
})
})
})

View File

@ -0,0 +1,48 @@
import {IJSONRPCReturnType} from './express'
import {TAsyncified, TReduxed} from './types'
import {createActions} from './redux'
import {createLocalClient, LocalClient} from './local'
function keys<T>(obj: T): Array<keyof T & string> {
return Object.keys(obj) as Array<keyof T & string>
}
type BulkLocalClient<T> = {[K in keyof T & string]: LocalClient<T[K]>}
type BulkActions<T> = {[K in keyof T & string]: TReduxed<T[K], K>}
type BulkRemoteClient<T> = {[K in keyof T & string]: TAsyncified<T[K]>}
function bulkCreate<T, R>(
src: T,
mapValue: <K extends keyof T & string>(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<T, Cx>(
src: T,
context: Cx,
): BulkLocalClient<T> {
return bulkCreate(src, (key, value) => createLocalClient(value, context))
}
export function bulkCreateActions<T extends Record<string, TAsyncified<any>>>(
src: T,
): BulkActions<T> {
return bulkCreate(src, (key, value) => createActions(value, key))
}
export function bulkjsonrpc<T>(
jsonrpc: IJSONRPCReturnType,
services: T,
) {
keys(services).forEach(key => {
const service = services[key]
jsonrpc.addService('/' + key, service)
})
return jsonrpc.router()
}