Remove express module dependency from jsonrpc
This commit is contained in:
parent
1c4d600450
commit
173b7cf66e
@ -1,10 +1,11 @@
|
|||||||
import * as util from './bulk'
|
|
||||||
import express from 'express'
|
|
||||||
import {WithContext} from './types'
|
|
||||||
import {jsonrpc} from './express'
|
|
||||||
import {noopLogger} from './test-utils'
|
|
||||||
import {createClient} from './supertest'
|
|
||||||
import { json } from 'body-parser'
|
import { json } from 'body-parser'
|
||||||
|
import express from 'express'
|
||||||
|
import { RequestHandlerParams } from 'express-serve-static-core'
|
||||||
|
import * as util from './bulk'
|
||||||
|
import { jsonrpc } from './express'
|
||||||
|
import { createClient } from './supertest'
|
||||||
|
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)
|
||||||
|
|||||||
@ -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¶ms=${params}`)
|
.get(`/app/myService?jsonrpc=2.0&id=1&method=add¶ms=${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)
|
||||||
|
|||||||
@ -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,8 +39,26 @@ 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 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) => {
|
const handleError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
|
if (req.path !== path) {
|
||||||
|
next(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.error('JSON-RPC Error: %s', err.stack)
|
logger.error('JSON-RPC Error: %s', err.stack)
|
||||||
|
|
||||||
if (isRPCError(err)) {
|
if (isRPCError(err)) {
|
||||||
@ -65,20 +83,6 @@ export function jsonrpc<Context>(
|
|||||||
res.json(errorResponse)
|
res.json(errorResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = Router()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user