167 lines
4.6 KiB
TypeScript
167 lines
4.6 KiB
TypeScript
import { Logger } from '@rondo.dev/logger'
|
|
import express, { ErrorRequestHandler, Request, Response, NextFunction, RequestHandler } from 'express'
|
|
import { createError, ErrorResponse, isRPCError } from './error'
|
|
import { IDEMPOTENT_METHOD_REGEX } from './idempotent'
|
|
import { createRpcService, ERROR_METHOD_NOT_FOUND, ERROR_SERVER, Request as RPCRequest, SuccessResponse } from './jsonrpc'
|
|
import { FunctionPropertyNames } from './types'
|
|
|
|
export type GetContext<Context> = (req: Request) => Promise<Context> | Context
|
|
|
|
export interface RPCReturnType {
|
|
addService<T, F extends FunctionPropertyNames<T>>(
|
|
path: string,
|
|
service: T,
|
|
methods?: F[],
|
|
): RPCReturnType
|
|
router(): Array<RequestHandler | ErrorRequestHandler>
|
|
}
|
|
|
|
export interface InvocationDetails<A extends RPCRequest, Context> {
|
|
context: Context
|
|
path: string
|
|
request: A
|
|
}
|
|
|
|
async function defaultHook<A extends RPCRequest, R, Context>(
|
|
details: InvocationDetails<A, Context>,
|
|
invoke: () => Promise<R>,
|
|
): Promise<R> {
|
|
const result = await invoke()
|
|
return result
|
|
}
|
|
|
|
export function jsonrpc<Context>(
|
|
getContext: GetContext<Context>,
|
|
logger: Logger,
|
|
hook: <A extends RPCRequest, R>(
|
|
details: InvocationDetails<A, Context>,
|
|
invoke: (request?: A) => Promise<R>) => Promise<R> = defaultHook,
|
|
idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX,
|
|
): RPCReturnType {
|
|
|
|
const router: Array<RequestHandler | ErrorRequestHandler> = []
|
|
|
|
const self = {
|
|
/**
|
|
* Adds middleware for handling JSON RPC requests. Expects JSON middleware to
|
|
* already be configured.
|
|
*/
|
|
addService<T, F extends FunctionPropertyNames<T>>(
|
|
path: string,
|
|
service: T,
|
|
methods?: F[],
|
|
) {
|
|
const rpcService = createRpcService(service, methods)
|
|
|
|
const handleError: ErrorRequestHandler = (err, req, res, next) => {
|
|
if (req.path !== path) {
|
|
next(err)
|
|
return
|
|
}
|
|
|
|
logger.error('JSON-RPC Error: %s', err.stack)
|
|
|
|
if (isRPCError(err)) {
|
|
res.status(err.statusCode)
|
|
res.json(err.response)
|
|
return
|
|
}
|
|
|
|
const id = getRequestId(req)
|
|
const statusCode: number = err.statusCode || 500
|
|
const errorResponse: ErrorResponse<unknown> = {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
result: null,
|
|
error: {
|
|
code: ERROR_SERVER.code,
|
|
message: statusCode >= 500 ? ERROR_SERVER.message : err.message,
|
|
data: 'errors' in err ? err.errors : null,
|
|
},
|
|
}
|
|
res.status(statusCode)
|
|
res.json(errorResponse)
|
|
}
|
|
|
|
function handleResponse(
|
|
response: SuccessResponse<unknown> | null,
|
|
res: Response,
|
|
) {
|
|
if (response === null) {
|
|
// notification
|
|
res.status(204).send()
|
|
} else {
|
|
res.json(response)
|
|
}
|
|
}
|
|
|
|
function handle(req: Request, res: Response, next: NextFunction) {
|
|
if (req.path !== path) {
|
|
next()
|
|
return
|
|
}
|
|
|
|
switch(req.method) {
|
|
case 'GET':
|
|
handleGet(req, res, next)
|
|
break
|
|
case 'POST':
|
|
handlePost(req, res, next)
|
|
break
|
|
default:
|
|
next()
|
|
}
|
|
}
|
|
|
|
const handleGet: RequestHandler = (req, res, next) => {
|
|
if (!idempotentMethodRegex.test(req.query.method)) {
|
|
// TODO fix status code and error type
|
|
const err = createError(ERROR_METHOD_NOT_FOUND, {
|
|
id: req.query.id,
|
|
data: null,
|
|
statusCode: 405,
|
|
})
|
|
throw err
|
|
}
|
|
const request = {
|
|
id: req.query.id,
|
|
jsonrpc: req.query.jsonrpc,
|
|
method: req.query.method,
|
|
params: JSON.parse(req.query.params),
|
|
}
|
|
Promise.resolve(getContext(req))
|
|
.then(context =>
|
|
hook(
|
|
{path, request, context},
|
|
(body = request) => rpcService.invoke(body, context)))
|
|
.then(response => handleResponse(response, res))
|
|
.catch(next)
|
|
}
|
|
|
|
const handlePost: RequestHandler = (req, res, next) => {
|
|
Promise.resolve(getContext(req))
|
|
.then(context =>
|
|
hook(
|
|
{path, request: req.body, context},
|
|
(body = req.body) => rpcService.invoke(body, context)))
|
|
.then(response => handleResponse(response, res))
|
|
.catch(next)
|
|
}
|
|
|
|
router.push(handle)
|
|
router.push(handleError)
|
|
return self
|
|
},
|
|
router() {
|
|
return router
|
|
},
|
|
}
|
|
|
|
return self
|
|
}
|
|
|
|
function getRequestId(req: express.Request) {
|
|
const id = req.method === 'POST' ? req.body.id : req.query.id
|
|
return id !== undefined ? id : null
|
|
}
|