Add jsonrpc middleware
This commit is contained in:
parent
b5e0c408c2
commit
faf76ec76a
18
packages/jsonrpc/jest.config.js
Normal file
18
packages/jsonrpc/jest.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
roots: [
|
||||
'<rootDir>/src'
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest'
|
||||
},
|
||||
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
|
||||
moduleFileExtensions: [
|
||||
'ts',
|
||||
'tsx',
|
||||
'js',
|
||||
'jsx'
|
||||
],
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
maxConcurrency: 1,
|
||||
verbose: false
|
||||
}
|
||||
0
packages/jsonrpc/jest.setup.js
Normal file
0
packages/jsonrpc/jest.setup.js
Normal file
1
packages/jsonrpc/src/index.ts
Normal file
1
packages/jsonrpc/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './server'
|
||||
108
packages/jsonrpc/src/server.test.ts
Normal file
108
packages/jsonrpc/src/server.test.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import {jsonrpc} from './server'
|
||||
import request from 'supertest'
|
||||
import express from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
|
||||
describe('jsonrpc', () => {
|
||||
|
||||
class Service {
|
||||
constructor(readonly time: number) {}
|
||||
add(a: number, b: number) {
|
||||
return a + b
|
||||
}
|
||||
multiply(...numbers: number[]) {
|
||||
return numbers.reduce((a, b) => a * b, 1)
|
||||
}
|
||||
delay() {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, this.time)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function createApp() {
|
||||
const app = express()
|
||||
app.use(bodyParser.json())
|
||||
app.use('/myService', jsonrpc(new Service(5), ['add', 'delay']))
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
console.log(err)
|
||||
next(err)
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
describe('errors', () => {
|
||||
|
||||
})
|
||||
|
||||
describe('success', () => {
|
||||
it('can call method and receive results', async () => {
|
||||
const response = await request(createApp())
|
||||
.post('/myService')
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'add',
|
||||
params: [1, 2],
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(response.body).toEqual({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
result: 3,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
it('handles promises', async () => {
|
||||
const response = await request(createApp())
|
||||
.post('/myService')
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'delay',
|
||||
params: [],
|
||||
})
|
||||
.expect(200)
|
||||
expect(response.body).toEqual({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
// result: null,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
it('handles synchronous notifications', async () => {
|
||||
await request(createApp())
|
||||
.post('/myService')
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
method: 'add',
|
||||
params: [1, 2],
|
||||
})
|
||||
.expect(200)
|
||||
.expect('')
|
||||
|
||||
await request(createApp())
|
||||
.post('/myService')
|
||||
.send({
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
method: 'add',
|
||||
params: [1, 2],
|
||||
})
|
||||
.expect(200)
|
||||
.expect('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('security', () => {
|
||||
it('cannot call toString method', () => {
|
||||
|
||||
})
|
||||
|
||||
it('cannot call any other methods in objects prototype', () => {
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
174
packages/jsonrpc/src/server.ts
Normal file
174
packages/jsonrpc/src/server.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import {NextFunction, Request, Response, Router} from 'express'
|
||||
|
||||
export const ERROR_PARSE = {
|
||||
code: -32700, message: 'Parse error', data: null}
|
||||
export const ERROR_INVALID_REQUEST = {
|
||||
code: -32600,
|
||||
message: 'Invalid Request',
|
||||
data: null}
|
||||
export const ERROR_METHOD_NOT_FOUND = {
|
||||
code: -32601,
|
||||
message: 'Method not found',
|
||||
data: null,
|
||||
}
|
||||
export const ERROR_INVALID_PARAMS = {
|
||||
code: -32602,
|
||||
message: 'Invalid params',
|
||||
data: null,
|
||||
}
|
||||
export const ERROR_INTERNAL_ERROR = {
|
||||
code: -32603,
|
||||
message: 'Internal error',
|
||||
data: null,
|
||||
}
|
||||
export const ERROR_SERVER = {
|
||||
code: -32000,
|
||||
message: 'Server error',
|
||||
data: null,
|
||||
}
|
||||
|
||||
export interface ISuccessResponse<T> {
|
||||
jsonrpc: '2.0'
|
||||
id: number
|
||||
result: T
|
||||
error: null
|
||||
}
|
||||
|
||||
export interface IJsonRpcError<T> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface IErrorResponse<T> {
|
||||
jsonrpc: '2.0'
|
||||
id: number | null
|
||||
result: null
|
||||
error: IJsonRpcError<T>
|
||||
}
|
||||
|
||||
export type IRpcResponse<T> = ISuccessResponse<T> | IErrorResponse<T>
|
||||
|
||||
export function createSuccessResponse<T>(id: number, result: T)
|
||||
: ISuccessResponse<T> {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function createErrorResponse<T>(
|
||||
id: number | null, error: IJsonRpcError<T>)
|
||||
: IErrorResponse<T> {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: null,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export type FunctionPropertyNames<T> = {
|
||||
// tslint:disable-next-line
|
||||
[K in keyof T]: T[K] extends Function ? K : never
|
||||
}[keyof T]
|
||||
|
||||
export function pick<T, K extends FunctionPropertyNames<T>>(t: T, keys: K[])
|
||||
: Pick<T, K> {
|
||||
return keys.reduce((obj, key) => {
|
||||
// tslint:disable-next-line
|
||||
const fn = t[key] as unknown as Function
|
||||
obj[key] = fn.bind(t)
|
||||
return obj
|
||||
}, {} as Pick<T, K>)
|
||||
}
|
||||
|
||||
function isPromise(value: any): value is Promise<unknown> {
|
||||
return value !== null && typeof value === 'object' &&
|
||||
'then' in value && 'catch' in value &&
|
||||
typeof value.then === 'function' && typeof value.catch === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds middleware for handling JSON RPC requests. Expects JSON middleware to
|
||||
* already be configured.
|
||||
*/
|
||||
export function jsonrpc<T, F extends FunctionPropertyNames<T>>(
|
||||
service: T,
|
||||
functions: F[],
|
||||
) {
|
||||
const rpcService = pick(service, functions)
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post('/', (req, res, next) => {
|
||||
if (req.headers['content-type'] !== 'application/json') {
|
||||
res.status(400)
|
||||
return res.json(createErrorResponse(null, ERROR_PARSE))
|
||||
}
|
||||
const {id, method, params} = req.body
|
||||
const isNotification = id === null || id === undefined
|
||||
if (
|
||||
req.body.jsonrpc !== '2.0' ||
|
||||
typeof method !== 'string' ||
|
||||
!Array.isArray(params)
|
||||
) {
|
||||
res.status(400)
|
||||
return res.json(createErrorResponse(null, ERROR_INVALID_REQUEST))
|
||||
}
|
||||
|
||||
if (
|
||||
!rpcService.hasOwnProperty(method) ||
|
||||
typeof (rpcService as any)[method] !== 'function'
|
||||
) {
|
||||
res.status(404)
|
||||
return res.json(createErrorResponse(null, ERROR_METHOD_NOT_FOUND))
|
||||
}
|
||||
|
||||
// if (rpcService[method].arguments.length !== params.length) {
|
||||
// return res.json(createErrorResponse(null, ERROR_INVALID_PARAMS))
|
||||
// }
|
||||
|
||||
const retValue = (rpcService as any)[method](...params)
|
||||
|
||||
if (!isPromise(retValue)) {
|
||||
if (isNotification) {
|
||||
return res.send()
|
||||
}
|
||||
return res.json(createSuccessResponse(id, retValue))
|
||||
}
|
||||
|
||||
retValue
|
||||
.then(result => {
|
||||
return res.json(createSuccessResponse(id, result))
|
||||
})
|
||||
.catch(err => handleError(err, req, res, next))
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
function handleError(
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
const statusCode: number = 'statusCode' in err &&
|
||||
typeof err.statusCode === 'number'
|
||||
? err.statusCode
|
||||
: 500
|
||||
|
||||
const message = statusCode >= 400 && statusCode < 500
|
||||
? err.message
|
||||
: ERROR_SERVER.message
|
||||
|
||||
res.status(statusCode)
|
||||
res.json(createErrorResponse(req.body.id, {
|
||||
code: ERROR_SERVER.code,
|
||||
message,
|
||||
data: err.errors,
|
||||
}))
|
||||
}
|
||||
7
packages/jsonrpc/tsconfig.esm.json
Normal file
7
packages/jsonrpc/tsconfig.esm.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "esm"
|
||||
},
|
||||
"references": []
|
||||
}
|
||||
9
packages/jsonrpc/tsconfig.json
Normal file
9
packages/jsonrpc/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"references": [
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user