Add jsonrpc local, remote and supertest clients
This commit is contained in:
parent
7dd9497514
commit
3787315905
@ -1,4 +1,9 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
compiler: 'ttypescript'
|
||||
}
|
||||
},
|
||||
roots: [
|
||||
'<rootDir>/src'
|
||||
],
|
||||
@ -13,6 +18,5 @@ module.exports = {
|
||||
'jsx'
|
||||
],
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
maxConcurrency: 1,
|
||||
verbose: false
|
||||
}
|
||||
|
||||
2
packages/jsonrpc/src/client.ts
Normal file
2
packages/jsonrpc/src/client.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './local'
|
||||
export * from './remote'
|
||||
@ -1 +1,2 @@
|
||||
export * from './client'
|
||||
export * from './server'
|
||||
|
||||
44
packages/jsonrpc/src/local.test.ts
Normal file
44
packages/jsonrpc/src/local.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
21
packages/jsonrpc/src/local.ts
Normal file
21
packages/jsonrpc/src/local.ts
Normal 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>
|
||||
}
|
||||
60
packages/jsonrpc/src/remote.test.ts
Normal file
60
packages/jsonrpc/src/remote.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
40
packages/jsonrpc/src/remote.ts
Normal file
40
packages/jsonrpc/src/remote.ts
Normal 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>
|
||||
}
|
||||
@ -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> = T extends (...args: infer U) => infer R ? U : never
|
||||
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())
|
||||
const client = createClient<IService>(createApp(), '/myService')
|
||||
|
||||
async function getError(promise: Promise<void>) {
|
||||
let error
|
||||
|
||||
30
packages/jsonrpc/src/supertest.ts
Normal file
30
packages/jsonrpc/src/supertest.ts
Normal 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>
|
||||
}
|
||||
10
packages/jsonrpc/src/types.ts
Normal file
10
packages/jsonrpc/src/types.ts
Normal 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]>
|
||||
}
|
||||
@ -20,6 +20,8 @@
|
||||
"plugins": [{
|
||||
"name": "typescript-tslint-plugin",
|
||||
"suppressWhileTypeErrorsPresent": true
|
||||
}, {
|
||||
"transform": "ts-transformer-keys/transformer"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user