Extract jsonrpc functionality into a separate file
This commit is contained in:
parent
be7b38b39e
commit
c273c1b914
56
packages/jsonrpc/src/error.ts
Normal file
56
packages/jsonrpc/src/error.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
export interface IError {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IErrorWithData<T> extends IError {
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IErrorResponse<T> {
|
||||||
|
jsonrpc: '2.0'
|
||||||
|
id: string | number | null
|
||||||
|
result: null
|
||||||
|
error: IErrorWithData<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IJSONRPCError<T> extends Error {
|
||||||
|
code: number
|
||||||
|
statusCode: number
|
||||||
|
response: IErrorResponse<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJSONRPCError(err: any): err is IJSONRPCError<unknown> {
|
||||||
|
return err.name === 'IJSONRPCError' &&
|
||||||
|
typeof err.message === 'string' &&
|
||||||
|
err.hasOwnProperty('code') &&
|
||||||
|
err.hasOwnProperty('response')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createError<T = null>(
|
||||||
|
error: IError,
|
||||||
|
info: {
|
||||||
|
id: number | string | null
|
||||||
|
data: T
|
||||||
|
statusCode: number
|
||||||
|
},
|
||||||
|
): IJSONRPCError<T> {
|
||||||
|
|
||||||
|
const err = new Error(error.message) as IJSONRPCError<T>
|
||||||
|
|
||||||
|
err.name = 'IJSONRPCError'
|
||||||
|
err.code = error.code
|
||||||
|
err.statusCode = info.statusCode
|
||||||
|
err.response = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: info.id,
|
||||||
|
error: {
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
data: info.data,
|
||||||
|
},
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -55,14 +55,15 @@ describe('jsonrpc', () => {
|
|||||||
function createApp() {
|
function createApp() {
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json())
|
||||||
app.use('/myService', jsonrpc(new Service(5), [
|
app.use('/myService', jsonrpc(req => ({userId: 1000}))
|
||||||
|
.addService(new Service(5), [
|
||||||
'add',
|
'add',
|
||||||
'delay',
|
'delay',
|
||||||
'syncError',
|
'syncError',
|
||||||
'asyncError',
|
'asyncError',
|
||||||
'httpError',
|
'httpError',
|
||||||
'addWithContext',
|
'addWithContext',
|
||||||
], req => ({userId: 1000})))
|
]))
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +98,7 @@ describe('jsonrpc', () => {
|
|||||||
result: null,
|
result: null,
|
||||||
error: {
|
error: {
|
||||||
code: -32000,
|
code: -32000,
|
||||||
|
data: null,
|
||||||
message: 'Server error',
|
message: 'Server error',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -106,19 +108,19 @@ describe('jsonrpc', () => {
|
|||||||
expect(err.message).toBe('Server error')
|
expect(err.message).toBe('Server error')
|
||||||
expect(err.code).toBe(-32000)
|
expect(err.code).toBe(-32000)
|
||||||
})
|
})
|
||||||
it('returns an error when message is not in json format', async () => {
|
it('returns an error when message is not readable', async () => {
|
||||||
const result = await request(createApp())
|
const result = await request(createApp())
|
||||||
.post('/myService')
|
.post('/myService')
|
||||||
.send('a=1')
|
.send('a=1')
|
||||||
.expect(400)
|
.expect(400)
|
||||||
expect(result.body.error.message).toEqual('Parse error')
|
expect(result.body.error.message).toEqual('Invalid request')
|
||||||
})
|
})
|
||||||
it('returns an error when message is not valid', async () => {
|
it('returns an error when message is not valid', async () => {
|
||||||
const result = await request(createApp())
|
const result = await request(createApp())
|
||||||
.post('/myService')
|
.post('/myService')
|
||||||
.send({})
|
.send({})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
expect(result.body.error.message).toEqual('Invalid Request')
|
expect(result.body.error.message).toEqual('Invalid request')
|
||||||
})
|
})
|
||||||
it('converts http errors into jsonrpc errors', async () => {
|
it('converts http errors into jsonrpc errors', async () => {
|
||||||
const err = await getError(client.httpError(403, 'Unauthorized'))
|
const err = await getError(client.httpError(403, 'Unauthorized'))
|
||||||
|
|||||||
@ -1,182 +1,68 @@
|
|||||||
import {FunctionPropertyNames} from './types'
|
import {FunctionPropertyNames} from './types'
|
||||||
import {NextFunction, Request, Response, Router} from 'express'
|
import {NextFunction, Request, Response, Router} from 'express'
|
||||||
|
import {createRpcService, ERROR_SERVER} from './jsonrpc'
|
||||||
export const ERROR_PARSE = {
|
import {createError, isJSONRPCError, IJSONRPCError} from './error'
|
||||||
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 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'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TGetContext<Context> = (req: Request) => Context
|
export type TGetContext<Context> = (req: Request) => Context
|
||||||
|
|
||||||
/**
|
export function jsonrpc<Context>(
|
||||||
* Adds middleware for handling JSON RPC requests. Expects JSON middleware to
|
|
||||||
* already be configured.
|
|
||||||
*/
|
|
||||||
export function jsonrpc<T, F extends FunctionPropertyNames<T>, Context>(
|
|
||||||
service: T,
|
|
||||||
methods: F[],
|
|
||||||
getContext: TGetContext<Context>,
|
getContext: TGetContext<Context>,
|
||||||
|
idempotentMethodRegex = /^(find|fetch|get)/,
|
||||||
) {
|
) {
|
||||||
const rpcService = pick(service, methods)
|
return {
|
||||||
|
/**
|
||||||
const router = Router()
|
* Adds middleware for handling JSON RPC requests. Expects JSON middleware to
|
||||||
|
* already be configured.
|
||||||
router.post('/', (req, res, next) => {
|
*/
|
||||||
if (req.headers['content-type'] !== 'application/json') {
|
addService<T, F extends FunctionPropertyNames<T>>(
|
||||||
res.status(400)
|
service: T,
|
||||||
return res.json(createErrorResponse(null, ERROR_PARSE))
|
methods: F[],
|
||||||
}
|
|
||||||
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)
|
const callRpcService = createRpcService(service, methods)
|
||||||
return res.json(createErrorResponse(id, ERROR_INVALID_REQUEST))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
const router = Router()
|
||||||
!rpcService.hasOwnProperty(method) ||
|
|
||||||
typeof (rpcService as any)[method] !== 'function'
|
|
||||||
) {
|
|
||||||
res.status(404)
|
|
||||||
return res.json(createErrorResponse(id, ERROR_METHOD_NOT_FOUND))
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (rpcService[method].arguments.length !== params.length) {
|
router.post('/', (req, res, next) => {
|
||||||
// return res.json(createErrorResponse(null, ERROR_INVALID_PARAMS))
|
callRpcService(req.body, getContext(req))
|
||||||
// }
|
.then(response => {
|
||||||
|
if (response === null) {
|
||||||
|
// notification
|
||||||
|
res.status(204).send()
|
||||||
|
} else {
|
||||||
|
res.json(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => handleError(req.body.id, err, req, res, next))
|
||||||
|
})
|
||||||
|
|
||||||
// TODO handle synchronous errors
|
return router
|
||||||
let retValue
|
},
|
||||||
try {
|
}
|
||||||
retValue = (rpcService as any)[method](...params)
|
|
||||||
if (typeof retValue === 'function') {
|
|
||||||
retValue = retValue(getContext(req))
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return handleError(err, req, res, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPromise(retValue)) {
|
|
||||||
if (isNotification) {
|
|
||||||
return res.status(204).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(
|
function handleError(
|
||||||
|
id: number | string | null,
|
||||||
err: any,
|
err: any,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) {
|
) {
|
||||||
const statusCode: number = 'statusCode' in err &&
|
// TODO log error
|
||||||
typeof err.statusCode === 'number'
|
// TODO make this nicer
|
||||||
? err.statusCode
|
|
||||||
: 500
|
|
||||||
|
|
||||||
const message = statusCode >= 400 && statusCode < 500
|
const error: IJSONRPCError<unknown> = isJSONRPCError(err)
|
||||||
? err.message
|
? err
|
||||||
: ERROR_SERVER.message
|
: createError({
|
||||||
|
code: ERROR_SERVER.code,
|
||||||
|
message: err.statusCode >= 400 && err.statusCode < 500
|
||||||
|
? err.message
|
||||||
|
: ERROR_SERVER.message,
|
||||||
|
}, {
|
||||||
|
id,
|
||||||
|
data: null,
|
||||||
|
statusCode: err.statusCode || 500,
|
||||||
|
})
|
||||||
|
|
||||||
res.status(statusCode)
|
res.status(error.statusCode)
|
||||||
res.json(createErrorResponse(req.body.id, {
|
res.json(error.response)
|
||||||
code: ERROR_SERVER.code,
|
|
||||||
message,
|
|
||||||
data: err.errors,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
export * from './error'
|
||||||
export * from './express'
|
export * from './express'
|
||||||
|
export * from './jsonrpc'
|
||||||
export * from './local'
|
export * from './local'
|
||||||
export * from './redux'
|
export * from './redux'
|
||||||
export * from './remote'
|
export * from './remote'
|
||||||
|
|||||||
5
packages/jsonrpc/src/isPromise.ts
Normal file
5
packages/jsonrpc/src/isPromise.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export 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'
|
||||||
|
}
|
||||||
139
packages/jsonrpc/src/jsonrpc.ts
Normal file
139
packages/jsonrpc/src/jsonrpc.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
export type TId = number | string
|
||||||
|
import {ArgumentTypes, FunctionPropertyNames, RetType} from './types'
|
||||||
|
import {isPromise} from './isPromise'
|
||||||
|
import {createError, IErrorWithData} from './error'
|
||||||
|
|
||||||
|
export const ERROR_PARSE = {
|
||||||
|
code: -32700,
|
||||||
|
message: 'Parse error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERROR_INVALID_REQUEST = {
|
||||||
|
code: -32600,
|
||||||
|
message: 'Invalid request',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERROR_METHOD_NOT_FOUND = {
|
||||||
|
code: -32601,
|
||||||
|
message: 'Method not found',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERROR_INVALID_PARAMS = {
|
||||||
|
code: -32602,
|
||||||
|
message: 'Invalid params',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERROR_INTERNAL_ERROR = {
|
||||||
|
code: -32603,
|
||||||
|
message: 'Internal error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERROR_SERVER = {
|
||||||
|
code: -32000,
|
||||||
|
message: 'Server error',
|
||||||
|
}
|
||||||
|
|
||||||
|
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>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest<M extends string | symbol | number = any, A = any[]> {
|
||||||
|
jsonrpc: '2.0'
|
||||||
|
id: TId | null
|
||||||
|
method: M
|
||||||
|
params: A
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISuccessResponse<T> {
|
||||||
|
jsonrpc: '2.0'
|
||||||
|
id: TId
|
||||||
|
result: T
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IErrorResponse<T> {
|
||||||
|
jsonrpc: '2.0'
|
||||||
|
id: TId | null
|
||||||
|
result: null
|
||||||
|
error: IErrorWithData<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IResponse<T = any> = ISuccessResponse<T> | IErrorResponse<T>
|
||||||
|
|
||||||
|
export function createSuccessResponse<T>(id: number | string, result: T)
|
||||||
|
: ISuccessResponse<T> {
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id,
|
||||||
|
result,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createErrorResponse<T>(
|
||||||
|
id: number | string | null, error: IErrorWithData<T>)
|
||||||
|
: IErrorResponse<T> {
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id,
|
||||||
|
result: null,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TGetContext<Context> = (req: Request) => Context
|
||||||
|
|
||||||
|
export const createRpcService =
|
||||||
|
<T, M extends FunctionPropertyNames<T>>(
|
||||||
|
service: T,
|
||||||
|
methods: M[],
|
||||||
|
) => async <Context>(req: IRequest<M, ArgumentTypes<T[M]>>, context: Context)
|
||||||
|
: Promise<ISuccessResponse<RetType<T[M]>> | null> => {
|
||||||
|
const {id, method, params} = req
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.jsonrpc !== '2.0' ||
|
||||||
|
typeof method !== 'string' ||
|
||||||
|
!Array.isArray(params)
|
||||||
|
) {
|
||||||
|
throw createError(ERROR_INVALID_REQUEST, {
|
||||||
|
id,
|
||||||
|
data: null,
|
||||||
|
statusCode: 400,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNotification = req.id === null || req.id === undefined
|
||||||
|
|
||||||
|
const rpcService = pick(service, methods)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!rpcService.hasOwnProperty(method) ||
|
||||||
|
typeof rpcService[method] !== 'function'
|
||||||
|
) {
|
||||||
|
throw createError(ERROR_METHOD_NOT_FOUND, {
|
||||||
|
id,
|
||||||
|
data: null,
|
||||||
|
statusCode: 404,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let retValue = (rpcService[method] as any)(...params)
|
||||||
|
|
||||||
|
if (typeof retValue === 'function') {
|
||||||
|
retValue = retValue(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPromise(retValue) && isNotification) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
retValue = await retValue
|
||||||
|
return createSuccessResponse(req.id as any, retValue)
|
||||||
|
}
|
||||||
@ -54,9 +54,10 @@ describe('createActions', () => {
|
|||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json())
|
||||||
app.use('/service', jsonrpc(new Service(), keys<IService>(), () => ({
|
app.use('/service', jsonrpc(() => ({userId: 1000})).addService(
|
||||||
userId: 1000,
|
new Service(),
|
||||||
})))
|
keys<IService>(),
|
||||||
|
))
|
||||||
|
|
||||||
let baseUrl: string
|
let baseUrl: string
|
||||||
let server: Server
|
let server: Server
|
||||||
|
|||||||
@ -28,7 +28,7 @@ describe('remote', () => {
|
|||||||
function createApp() {
|
function createApp() {
|
||||||
const a = express()
|
const a = express()
|
||||||
a.use(bodyParser.json())
|
a.use(bodyParser.json())
|
||||||
a.use('/myService', jsonrpc(service, IServiceKeys, () => ({})))
|
a.use('/myService', jsonrpc(() => ({})).addService(service, IServiceKeys))
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import {IPendingAction, IResolvedAction, IRejectedAction} from '@rondo/client'
|
import {IPendingAction, IResolvedAction, IRejectedAction} from '@rondo/client'
|
||||||
|
|
||||||
type ArgumentTypes<T> =
|
export type ArgumentTypes<T> =
|
||||||
T extends (...args: infer U) => infer R ? U : never
|
T extends (...args: infer U) => infer R ? U : never
|
||||||
type RetType<T> = T extends (...args: any[]) => infer R ? R : never
|
export type RetType<T> = T extends (...args: any[]) => infer R ? R : never
|
||||||
type UnwrapHOC<T> = T extends (...args: any[]) => infer R ? R : T
|
type UnwrapHOC<T> = T extends (...args: any[]) => infer R ? R : T
|
||||||
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T
|
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T
|
||||||
type RetProm<T> = T extends Promise<any> ? T : Promise<T>
|
type RetProm<T> = T extends Promise<any> ? T : Promise<T>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user