185 lines
4.2 KiB
TypeScript
185 lines
4.2 KiB
TypeScript
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>, Ctx>(
|
|
service: T,
|
|
functions: F[],
|
|
getContext: (req: Request) => Ctx,
|
|
) {
|
|
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(id, ERROR_INVALID_REQUEST))
|
|
}
|
|
|
|
if (
|
|
!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) {
|
|
// return res.json(createErrorResponse(null, ERROR_INVALID_PARAMS))
|
|
// }
|
|
|
|
// TODO handle synchronous errors
|
|
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(
|
|
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,
|
|
}))
|
|
}
|