Add jsonrpc local, remote and supertest clients
This commit is contained in:
parent
7dd9497514
commit
3787315905
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
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'
|
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 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
|
||||||
|
|||||||
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": [{
|
"plugins": [{
|
||||||
"name": "typescript-tslint-plugin",
|
"name": "typescript-tslint-plugin",
|
||||||
"suppressWhileTypeErrorsPresent": true
|
"suppressWhileTypeErrorsPresent": true
|
||||||
|
}, {
|
||||||
|
"transform": "ts-transformer-keys/transformer"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user