Add ability to validate context via @ensure

This commit is contained in:
Jerko Steiner 2019-08-27 16:17:05 +07:00
parent 22f0b15e3a
commit c7ab4fe387
5 changed files with 64 additions and 5 deletions

View File

@ -0,0 +1,31 @@
import 'reflect-metadata'
export const ensureKey = Symbol('ensure')
export type Validate<Context> = (context: Context) => boolean
export function ensure<Context>(
validate: Validate<Context>,
message: string = 'Validation failed',
) {
return (
target: any,
propertyKey: string,
// tslint:disable-next-line
descriptor: PropertyDescriptor,
) => {
const validators: Array<Validate<Context>> =
getValidatorsForMethod<Context>(target, propertyKey)
validators.push(validate)
Reflect.defineMetadata(ensureKey, validators, target, propertyKey)
}
}
export function getValidatorsForMethod<Context>(
target: any,
method: string,
): Array<Validate<Context>> {
return Reflect.getOwnMetadata(ensureKey, target, method) || []
}

View File

@ -4,6 +4,7 @@ import request from 'supertest'
import {createClient} from './supertest'
import {jsonrpc} from './express'
import {noopLogger} from './test-utils'
import {ensure} from './ensure'
describe('jsonrpc', () => {
@ -22,6 +23,8 @@ describe('jsonrpc', () => {
addWithContext2(a: number, b: number): Promise<number>
}
const ensureLoggedIn = ensure<IContext>(c => !!c.userId)
class Service implements IService {
constructor(readonly time: number) {}
add(a: number, b: number) {
@ -52,16 +55,20 @@ describe('jsonrpc', () => {
addWithContext = (a: number, b: number) => (ctx: IContext): number => {
return a + b + ctx.userId
}
@ensureLoggedIn
addWithContext2(a: number, b: number, ctx?: IContext) {
return Promise.resolve(a + b + ctx!.userId)
}
}
let userId: number | undefined = 1000
function createApp() {
userId = 1000
const app = express()
app.use(bodyParser.json())
app.use('/',
jsonrpc(req => ({userId: 1000}), noopLogger)
jsonrpc(req => ({userId}), noopLogger)
.addService('/myService', new Service(5), [
'add',
'delay',
@ -78,7 +85,7 @@ describe('jsonrpc', () => {
const client = createClient<IService>(createApp(), '/myService')
async function getError(promise: Promise<void>) {
async function getError(promise: Promise<unknown>) {
let error
try {
await promise
@ -153,6 +160,11 @@ describe('jsonrpc', () => {
const response = await client.addWithContext2(5, 7)
expect(response).toEqual(1000 + 5 + 7)
})
it('can validate context using @ensure decorator', async () => {
userId = undefined
const err = await getError(client.addWithContext2(5, 7))
expect(err.message).toMatch(/Invalid request/)
})
it('handles synchronous notifications', async () => {
await request(createApp())
.post('/myService')

View File

@ -2,7 +2,7 @@ import express, {ErrorRequestHandler} from 'express'
import {FunctionPropertyNames} from './types'
import {IDEMPOTENT_METHOD_REGEX} from './idempotent'
import {IErrorResponse} from './error'
import {ILogger} from '@rondo.dev/common'
import {ILogger} from '@rondo.dev/logger'
import {ISuccessResponse} from './jsonrpc'
import {NextFunction, Request, Response, Router} from 'express'
import {createError, isJSONRPCError, IJSONRPCError, IError} from './error'

View File

@ -2,6 +2,7 @@ export type TId = number | string
import {ArgumentTypes, FunctionPropertyNames, RetType} from './types'
import {isPromise} from './isPromise'
import {createError, IErrorResponse, IErrorWithData} from './error'
import {getValidatorsForMethod} from './ensure'
export const ERROR_PARSE = {
code: -32700,
@ -73,6 +74,7 @@ export const createRpcService = <T, M extends FunctionPropertyNames<T>>(
service: T,
methods: M[],
) => {
const rpcService = pick(service, methods)
return {
async invoke<Context>(
req: IRequest<M, ArgumentTypes<T[M]>>,
@ -94,8 +96,6 @@ export const createRpcService = <T, M extends FunctionPropertyNames<T>>(
const isNotification = req.id === null || req.id === undefined
const rpcService = pick(service, methods)
if (
!rpcService.hasOwnProperty(method) ||
typeof rpcService[method] !== 'function'
@ -107,6 +107,20 @@ export const createRpcService = <T, M extends FunctionPropertyNames<T>>(
})
}
const validators = getValidatorsForMethod<Context>(
(service as any).__proto__, method)
validators.forEach(v => {
const success = v(context)
if (!success) {
throw createError(ERROR_INVALID_REQUEST, {
id,
data: null,
statusCode: 400,
})
}
})
let retValue = (rpcService[method] as any)(...params, context)
if (typeof retValue === 'function') {

View File

@ -1,9 +1,11 @@
{
"extends": "../tsconfig.common.json",
"compilerOptions": {
"target": "es5",
"outDir": "lib",
"rootDir": "src"
},
"references": [
{"path": "../logger"}
]
}