Add jsonrpc local, remote and supertest clients

This commit is contained in:
Jerko Steiner 2019-07-31 08:44:50 +08:00
parent 7dd9497514
commit 3787315905
11 changed files with 220 additions and 40 deletions

View File

@ -1,4 +1,9 @@
module.exports = { module.exports = {
globals: {
'ts-jest': {
compiler: 'ttypescript'
}
},
roots: [ roots: [
'<rootDir>/src' '<rootDir>/src'
], ],
@ -13,6 +18,5 @@ module.exports = {
'jsx' 'jsx'
], ],
setupFiles: ['<rootDir>/jest.setup.js'], setupFiles: ['<rootDir>/jest.setup.js'],
maxConcurrency: 1,
verbose: false verbose: false
} }

View File

@ -0,0 +1,2 @@
export * from './local'
export * from './remote'

View File

@ -1 +1,2 @@
export * from './client'
export * from './server' export * from './server'

View File

@ -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<IService>()
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<IService>(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)
})
})
})

View File

@ -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<T>(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<T>
}

View File

@ -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<IService>()
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<IService>(
baseUrl, '/myService', IServiceKeys)
const result = await s.add(3, 7)
expect(result).toBe(3 + 7)
})
})
})

View File

@ -0,0 +1,40 @@
import Axios from 'axios'
import {Asyncified} from './types'
export function createRemoteClient<T>(
baseUrl: string,
url: string,
methods: Array<keyof T>,
) {
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<T>
}

View File

@ -1,7 +1,8 @@
import {jsonrpc} from './server'
import request from 'supertest'
import express from 'express'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import express from 'express'
import request from 'supertest'
import {createClient} from './supertest'
import {jsonrpc} from './server'
describe('jsonrpc', () => { describe('jsonrpc', () => {
@ -65,42 +66,7 @@ describe('jsonrpc', () => {
return app return app
} }
type ArgumentTypes<T> = T extends (...args: infer U) => infer R ? U : never const client = createClient<IService>(createApp(), '/myService')
type RetType<T> = T extends (...args: any[]) => infer R ? R : never
type UnwrapHOC<T> = T extends (...args: any[]) => infer R ? R : T
type RetProm<T> = T extends Promise<any> ? T : Promise<T>
type PromisifyReturnType<T> = (...a: ArgumentTypes<T>) =>
RetProm<UnwrapHOC<RetType<T>>>
type Asyncified<T> = {
[K in keyof T]: PromisifyReturnType<T[K]>
}
function createClient<T>(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<T>
}
const client = createClient<IService>(createApp())
async function getError(promise: Promise<void>) { async function getError(promise: Promise<void>) {
let error let error

View File

@ -0,0 +1,30 @@
import request from 'supertest'
import {Application} from 'express'
import {Asyncified} from './types'
export function createClient<T>(
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<T>
}

View File

@ -0,0 +1,10 @@
export type ArgumentTypes<T> =
T extends (...args: infer U) => infer R ? U : never
export type RetType<T> = T extends (...args: any[]) => infer R ? R : never
export type UnwrapHOC<T> = T extends (...args: any[]) => infer R ? R : T
export type RetProm<T> = T extends Promise<any> ? T : Promise<T>
export type PromisifyReturnType<T> = (...a: ArgumentTypes<T>) =>
RetProm<UnwrapHOC<RetType<T>>>
export type Asyncified<T> = {
[K in keyof T]: PromisifyReturnType<T[K]>
}

View File

@ -20,6 +20,8 @@
"plugins": [{ "plugins": [{
"name": "typescript-tslint-plugin", "name": "typescript-tslint-plugin",
"suppressWhileTypeErrorsPresent": true "suppressWhileTypeErrorsPresent": true
}, {
"transform": "ts-transformer-keys/transformer"
}] }]
} }
} }