Remove express module dependency from jsonrpc

This commit is contained in:
Jerko Steiner 2019-09-25 13:46:26 +07:00
parent 1c4d600450
commit 173b7cf66e
3 changed files with 179 additions and 55 deletions

View File

@ -1,10 +1,11 @@
import * as util from './bulk' import { json } from 'body-parser'
import express from 'express' import express from 'express'
import {WithContext} from './types' import { RequestHandlerParams } from 'express-serve-static-core'
import {jsonrpc} from './express' import * as util from './bulk'
import {noopLogger} from './test-utils' import { jsonrpc } from './express'
import {createClient} from './supertest' import { createClient } from './supertest'
import {json} from 'body-parser' import { noopLogger } from './test-utils'
import { WithContext } from './types'
describe('util', () => { describe('util', () => {
@ -85,7 +86,7 @@ describe('util', () => {
describe('bulkJSONRPC', () => { describe('bulkJSONRPC', () => {
const getContext = () => ({userId: 10}) const getContext = () => ({userId: 10})
function createApp(router: express.Router) { function createApp(router: RequestHandlerParams) {
const app = express() const app = express()
app.use(json()) app.use(json())
app.use('/rpc', router) app.use('/rpc', router)

View File

@ -27,6 +27,7 @@ describe('jsonrpc', () => {
const ensureLoggedIn = ensure<Context>(c => !!c.userId) const ensureLoggedIn = ensure<Context>(c => !!c.userId)
@ensureLoggedIn
class MyService implements WithContext<Service, Context> { class MyService implements WithContext<Service, Context> {
constructor(readonly time: number) {} constructor(readonly time: number) {}
add(context: Context, a: number, b: number) { add(context: Context, a: number, b: number) {
@ -73,7 +74,7 @@ describe('jsonrpc', () => {
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use('/', app.use('/',
jsonrpc(() => ({userId}), noopLogger) jsonrpc(() => ({userId}), noopLogger)
.addService('/myService', new MyService(5), [ .addService('/app/myService', new MyService(5), [
'add', 'add',
'delay', 'delay',
'syncError', 'syncError',
@ -87,7 +88,7 @@ describe('jsonrpc', () => {
return app return app
} }
const client = createClient<Service>(createApp(), '/myService') const client = createClient<Service>(createApp(), '/app/myService')
async function getError(promise: Promise<unknown>) { async function getError(promise: Promise<unknown>) {
let error let error
@ -103,7 +104,7 @@ describe('jsonrpc', () => {
describe('errors', () => { describe('errors', () => {
it('handles sync errors', async () => { it('handles sync errors', async () => {
const response = await request(createApp()) const response = await request(createApp())
.post('/myService') .post('/app/myService')
.send({ .send({
id: 1, id: 1,
jsonrpc: '2.0', jsonrpc: '2.0',
@ -129,14 +130,14 @@ describe('jsonrpc', () => {
}) })
it('returns an error when message is not readable', async () => { it('returns an error when message is not readable', async () => {
const result = await request(createApp()) const result = await request(createApp())
.post('/myService') .post('/app/myService')
.send('a=1') .send('a=1')
.expect(400) .expect(400)
expect(result.body.error.message).toEqual('Invalid request') 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('/app/myService')
.send({}) .send({})
.expect(400) .expect(400)
expect(result.body.error.message).toEqual('Invalid request') expect(result.body.error.message).toEqual('Invalid request')
@ -171,7 +172,7 @@ describe('jsonrpc', () => {
}) })
it('handles synchronous notifications', async () => { it('handles synchronous notifications', async () => {
await request(createApp()) await request(createApp())
.post('/myService') .post('/app/myService')
.send({ .send({
jsonrpc: '2.0', jsonrpc: '2.0',
method: 'add', method: 'add',
@ -181,7 +182,7 @@ describe('jsonrpc', () => {
.expect('') .expect('')
await request(createApp()) await request(createApp())
.post('/myService') .post('/app/myService')
.send({ .send({
jsonrpc: '2.0', jsonrpc: '2.0',
id: null, id: null,
@ -193,10 +194,110 @@ describe('jsonrpc', () => {
}) })
}) })
describe('invalid requests', () => {
it('returns 404 when invalid request method used', async () => {
await request(createApp())
.put('/app/myService')
.send({
id: 123,
jsonrpc: '2.0',
method: 'toString',
params: [],
})
.expect(404)
})
it('returns 404 when service url is invalid', async () => {
await request(createApp())
.post('/app/nonExistingService')
.send({
id: 123,
jsonrpc: '2.0',
method: 'toString',
params: [],
})
.expect(404)
})
})
describe('multiple services', () => {
interface S1 {
add(a: number, b: number): number
}
class Test1 implements WithContext<S1, Context> {
add(c: Context, a: number, b: number) {
return a + b
}
}
class Test2 implements WithContext<S1, Context> {
add(c: Context, a: number, b: number): number {
throw new Error('Not implemented')
}
}
const app = express()
app.use(bodyParser.json())
app.get('/app/s3', (req, res) => {
throw new Error('test s3')
})
app.use('/app',
jsonrpc(() => ({userId: 1}), noopLogger)
.addService('/s1', new Test1(), ['add'])
.addService('/s2', new Test2(), ['add'])
.router(),
)
it('invokes the first service', async () => {
await request(app)
.post('/app/s1')
.send({
id: 123,
jsonrpc: '2.0',
method: 'add',
params: [1, 2],
})
.expect(200)
.expect({
jsonrpc: '2.0',
id: 123,
result: 3,
error: null,
})
})
it('invokes the second service', async () => {
await request(app)
.post('/app/s2')
.send({
id: 123,
jsonrpc: '2.0',
method: 'add',
params: [1, 2],
})
.expect(500)
.expect({
jsonrpc: '2.0',
id: 123,
result: null,
error: {
code: -32000,
message: 'Server error',
data: null,
},
})
})
it('invokes the second service', async () => {
await request(app)
.get('/app/s3')
.expect(500)
.expect(/Error: test s3/)
})
})
describe('security', () => { describe('security', () => {
it('cannot call toString method', async () => { it('cannot call toString method', async () => {
await request(createApp()) await request(createApp())
.post('/myService') .post('/app/myService')
.send({ .send({
id: 123, id: 123,
jsonrpc: '2.0', jsonrpc: '2.0',
@ -218,7 +319,7 @@ describe('jsonrpc', () => {
it('cannot call private _-prefixed methods', async () => { it('cannot call private _-prefixed methods', async () => {
await request(createApp()) await request(createApp())
.post('/myService') .post('/app/myService')
.send({ .send({
id: 123, id: 123,
jsonrpc: '2.0', jsonrpc: '2.0',
@ -231,7 +332,7 @@ describe('jsonrpc', () => {
it('cannot call any other methods in objects prototype', async () => { it('cannot call any other methods in objects prototype', async () => {
await request(createApp()) await request(createApp())
.post('/myService') .post('/app/myService')
.send({ .send({
id: 123, id: 123,
jsonrpc: '2.0', jsonrpc: '2.0',
@ -254,7 +355,7 @@ describe('jsonrpc', () => {
it('cannot call non-idempotent methods using GET request', async () => { it('cannot call non-idempotent methods using GET request', async () => {
const params = encodeURIComponent(JSON.stringify([1, 2])) const params = encodeURIComponent(JSON.stringify([1, 2]))
await request(createApp()) await request(createApp())
.get(`/myService?jsonrpc=2.0&id=1&method=add&params=${params}`) .get(`/app/myService?jsonrpc=2.0&id=1&method=add&params=${params}`)
.expect(405) .expect(405)
}) })
@ -269,9 +370,8 @@ describe('jsonrpc', () => {
userId = 1000 userId = 1000
const app = express() const app = express()
const myService = new MyService(5) const myService = new MyService(5)
// console.log('service', myService, Object.
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use('/', app.use('/app',
jsonrpc( jsonrpc(
() => Promise.resolve({userId}), () => Promise.resolve({userId}),
noopLogger, noopLogger,
@ -296,7 +396,7 @@ describe('jsonrpc', () => {
params: [1, 2], params: [1, 2],
} }
const response = await request(create()) const response = await request(create())
.post('/myService') .post('/app/myService')
.send(req) .send(req)
expect(response.body.result).toEqual(3) expect(response.body.result).toEqual(3)

View File

@ -1,5 +1,5 @@
import { Logger } from '@rondo.dev/logger' import { Logger } from '@rondo.dev/logger'
import express, { ErrorRequestHandler, Request, Response, Router } from 'express' import express, { ErrorRequestHandler, Request, Response, NextFunction, RequestHandler } from 'express'
import { createError, ErrorResponse, isRPCError } from './error' import { createError, ErrorResponse, isRPCError } from './error'
import { IDEMPOTENT_METHOD_REGEX } from './idempotent' import { IDEMPOTENT_METHOD_REGEX } from './idempotent'
import { createRpcService, ERROR_METHOD_NOT_FOUND, ERROR_SERVER, Request as RPCRequest, SuccessResponse } from './jsonrpc' import { createRpcService, ERROR_METHOD_NOT_FOUND, ERROR_SERVER, Request as RPCRequest, SuccessResponse } from './jsonrpc'
@ -13,7 +13,7 @@ export interface RPCReturnType {
service: T, service: T,
methods?: F[], methods?: F[],
): RPCReturnType ): RPCReturnType
router(): Router router(): Array<RequestHandler | ErrorRequestHandler>
} }
export interface InvocationDetails<A extends RPCRequest, Context> { export interface InvocationDetails<A extends RPCRequest, Context> {
@ -39,33 +39,7 @@ export function jsonrpc<Context>(
idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX, idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX,
): RPCReturnType { ): RPCReturnType {
/* eslint @typescript-eslint/no-unused-vars: 0 */ const router: Array<RequestHandler | ErrorRequestHandler> = []
const handleError: ErrorRequestHandler = (err, req, res, next) => {
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)
}
const router = Router()
const self = { const self = {
/** /**
@ -79,6 +53,36 @@ export function jsonrpc<Context>(
) { ) {
const rpcService = createRpcService(service, methods) 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( function handleResponse(
response: SuccessResponse<unknown> | null, response: SuccessResponse<unknown> | null,
res: Response, res: Response,
@ -91,7 +95,25 @@ export function jsonrpc<Context>(
} }
} }
router.get(path, (req, res, next) => { 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)) { if (!idempotentMethodRegex.test(req.query.method)) {
// TODO fix status code and error type // TODO fix status code and error type
const err = createError(ERROR_METHOD_NOT_FOUND, { const err = createError(ERROR_METHOD_NOT_FOUND, {
@ -114,9 +136,9 @@ export function jsonrpc<Context>(
(body = request) => rpcService.invoke(body, context))) (body = request) => rpcService.invoke(body, context)))
.then(response => handleResponse(response, res)) .then(response => handleResponse(response, res))
.catch(next) .catch(next)
}) }
router.post(path, (req, res, next) => { const handlePost: RequestHandler = (req, res, next) => {
Promise.resolve(getContext(req)) Promise.resolve(getContext(req))
.then(context => .then(context =>
hook( hook(
@ -124,9 +146,10 @@ export function jsonrpc<Context>(
(body = req.body) => rpcService.invoke(body, context))) (body = req.body) => rpcService.invoke(body, context)))
.then(response => handleResponse(response, res)) .then(response => handleResponse(response, res))
.catch(next) .catch(next)
}) }
router.use(path, handleError) router.push(handle)
router.push(handleError)
return self return self
}, },
router() { router() {