Add ability to validate context via @ensure
This commit is contained in:
parent
22f0b15e3a
commit
c7ab4fe387
31
packages/jsonrpc/src/ensure.ts
Normal file
31
packages/jsonrpc/src/ensure.ts
Normal 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) || []
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import request from 'supertest'
|
|||||||
import {createClient} from './supertest'
|
import {createClient} from './supertest'
|
||||||
import {jsonrpc} from './express'
|
import {jsonrpc} from './express'
|
||||||
import {noopLogger} from './test-utils'
|
import {noopLogger} from './test-utils'
|
||||||
|
import {ensure} from './ensure'
|
||||||
|
|
||||||
describe('jsonrpc', () => {
|
describe('jsonrpc', () => {
|
||||||
|
|
||||||
@ -22,6 +23,8 @@ describe('jsonrpc', () => {
|
|||||||
addWithContext2(a: number, b: number): Promise<number>
|
addWithContext2(a: number, b: number): Promise<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensureLoggedIn = ensure<IContext>(c => !!c.userId)
|
||||||
|
|
||||||
class Service implements IService {
|
class Service implements IService {
|
||||||
constructor(readonly time: number) {}
|
constructor(readonly time: number) {}
|
||||||
add(a: number, b: number) {
|
add(a: number, b: number) {
|
||||||
@ -52,16 +55,20 @@ describe('jsonrpc', () => {
|
|||||||
addWithContext = (a: number, b: number) => (ctx: IContext): number => {
|
addWithContext = (a: number, b: number) => (ctx: IContext): number => {
|
||||||
return a + b + ctx.userId
|
return a + b + ctx.userId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ensureLoggedIn
|
||||||
addWithContext2(a: number, b: number, ctx?: IContext) {
|
addWithContext2(a: number, b: number, ctx?: IContext) {
|
||||||
return Promise.resolve(a + b + ctx!.userId)
|
return Promise.resolve(a + b + ctx!.userId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let userId: number | undefined = 1000
|
||||||
function createApp() {
|
function createApp() {
|
||||||
|
userId = 1000
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json())
|
||||||
app.use('/',
|
app.use('/',
|
||||||
jsonrpc(req => ({userId: 1000}), noopLogger)
|
jsonrpc(req => ({userId}), noopLogger)
|
||||||
.addService('/myService', new Service(5), [
|
.addService('/myService', new Service(5), [
|
||||||
'add',
|
'add',
|
||||||
'delay',
|
'delay',
|
||||||
@ -78,7 +85,7 @@ describe('jsonrpc', () => {
|
|||||||
|
|
||||||
const client = createClient<IService>(createApp(), '/myService')
|
const client = createClient<IService>(createApp(), '/myService')
|
||||||
|
|
||||||
async function getError(promise: Promise<void>) {
|
async function getError(promise: Promise<unknown>) {
|
||||||
let error
|
let error
|
||||||
try {
|
try {
|
||||||
await promise
|
await promise
|
||||||
@ -153,6 +160,11 @@ describe('jsonrpc', () => {
|
|||||||
const response = await client.addWithContext2(5, 7)
|
const response = await client.addWithContext2(5, 7)
|
||||||
expect(response).toEqual(1000 + 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 () => {
|
it('handles synchronous notifications', async () => {
|
||||||
await request(createApp())
|
await request(createApp())
|
||||||
.post('/myService')
|
.post('/myService')
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import express, {ErrorRequestHandler} from 'express'
|
|||||||
import {FunctionPropertyNames} from './types'
|
import {FunctionPropertyNames} from './types'
|
||||||
import {IDEMPOTENT_METHOD_REGEX} from './idempotent'
|
import {IDEMPOTENT_METHOD_REGEX} from './idempotent'
|
||||||
import {IErrorResponse} from './error'
|
import {IErrorResponse} from './error'
|
||||||
import {ILogger} from '@rondo.dev/common'
|
import {ILogger} from '@rondo.dev/logger'
|
||||||
import {ISuccessResponse} from './jsonrpc'
|
import {ISuccessResponse} from './jsonrpc'
|
||||||
import {NextFunction, Request, Response, Router} from 'express'
|
import {NextFunction, Request, Response, Router} from 'express'
|
||||||
import {createError, isJSONRPCError, IJSONRPCError, IError} from './error'
|
import {createError, isJSONRPCError, IJSONRPCError, IError} from './error'
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export type TId = number | string
|
|||||||
import {ArgumentTypes, FunctionPropertyNames, RetType} from './types'
|
import {ArgumentTypes, FunctionPropertyNames, RetType} from './types'
|
||||||
import {isPromise} from './isPromise'
|
import {isPromise} from './isPromise'
|
||||||
import {createError, IErrorResponse, IErrorWithData} from './error'
|
import {createError, IErrorResponse, IErrorWithData} from './error'
|
||||||
|
import {getValidatorsForMethod} from './ensure'
|
||||||
|
|
||||||
export const ERROR_PARSE = {
|
export const ERROR_PARSE = {
|
||||||
code: -32700,
|
code: -32700,
|
||||||
@ -73,6 +74,7 @@ export const createRpcService = <T, M extends FunctionPropertyNames<T>>(
|
|||||||
service: T,
|
service: T,
|
||||||
methods: M[],
|
methods: M[],
|
||||||
) => {
|
) => {
|
||||||
|
const rpcService = pick(service, methods)
|
||||||
return {
|
return {
|
||||||
async invoke<Context>(
|
async invoke<Context>(
|
||||||
req: IRequest<M, ArgumentTypes<T[M]>>,
|
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 isNotification = req.id === null || req.id === undefined
|
||||||
|
|
||||||
const rpcService = pick(service, methods)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!rpcService.hasOwnProperty(method) ||
|
!rpcService.hasOwnProperty(method) ||
|
||||||
typeof rpcService[method] !== 'function'
|
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)
|
let retValue = (rpcService[method] as any)(...params, context)
|
||||||
|
|
||||||
if (typeof retValue === 'function') {
|
if (typeof retValue === 'function') {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.common.json",
|
"extends": "../tsconfig.common.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"rootDir": "src"
|
"rootDir": "src"
|
||||||
},
|
},
|
||||||
"references": [
|
"references": [
|
||||||
|
{"path": "../logger"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user