Breaking change: JSONRPC context is always 1st argument

This commit is contained in:
Jerko Steiner 2019-08-30 17:12:46 +07:00
parent b8fb7c2eba
commit 0222455cd7
7 changed files with 55 additions and 61 deletions

View File

@ -6,6 +6,7 @@ import {createClient} from './supertest'
import {ensure} from './ensure' import {ensure} from './ensure'
import {jsonrpc} from './express' import {jsonrpc} from './express'
import {noopLogger} from './test-utils' import {noopLogger} from './test-utils'
import {Contextual} from './types'
describe('jsonrpc', () => { describe('jsonrpc', () => {
@ -20,15 +21,15 @@ describe('jsonrpc', () => {
asyncError(message: string): Promise<void> asyncError(message: string): Promise<void>
httpError(statusCode: number, message: string): Promise<void> httpError(statusCode: number, message: string): Promise<void>
addWithContext(a: number, b: number): (ctx: IContext) => number addWithContext(a: number, b: number): number
addWithContext2(a: number, b: number): Promise<number> addWithContext2(a: number, b: number): Promise<number>
} }
const ensureLoggedIn = ensure<IContext>(c => !!c.userId) const ensureLoggedIn = ensure<IContext>(c => !!c.userId)
class Service implements IService { class Service implements Contextual<IService, IContext> {
constructor(readonly time: number) {} constructor(readonly time: number) {}
add(a: number, b: number) { add(context: IContext, a: number, b: number) {
return a + b return a + b
} }
multiply(...numbers: number[]) { multiply(...numbers: number[]) {
@ -39,13 +40,13 @@ describe('jsonrpc', () => {
setTimeout(resolve, this.time) setTimeout(resolve, this.time)
}) })
} }
syncError(message: string) { syncError(context: IContext, message: string) {
throw new Error(message) throw new Error(message)
} }
async asyncError(message: string) { async asyncError(context: IContext, message: string) {
throw new Error(message) throw new Error(message)
} }
async httpError(statusCode: number, message: string) { async httpError(context: IContext, statusCode: number, message: string) {
const err: any = new Error(message) const err: any = new Error(message)
err.statusCode = statusCode err.statusCode = statusCode
err.errors = [{ err.errors = [{
@ -53,12 +54,12 @@ describe('jsonrpc', () => {
}] }]
throw err throw err
} }
addWithContext = (a: number, b: number) => (ctx: IContext): number => { addWithContext = (ctx: IContext, a: number, b: number) => {
return a + b + ctx.userId return a + b + ctx.userId
} }
@ensureLoggedIn @ensureLoggedIn
addWithContext2(a: number, b: number, ctx?: IContext) { addWithContext2(ctx: IContext, a: number, b: number) {
return Promise.resolve(a + b + ctx!.userId) return Promise.resolve(a + b + ctx!.userId)
} }
} }

View File

@ -157,15 +157,7 @@ export const createRpcService = <T, M extends FunctionPropertyNames<T>>(
await validateServiceContext(id, service, method, context) await validateServiceContext(id, service, method, context)
// FIXME TODO if user specified too many parameters in the request, let retValue = (rpcService[method] as any)(context, ...params)
// they might override the context argument! this is dangerous as it
// could allow them to set any userId they would like. We should compare
// method arguments length before invoking this function.
let retValue = (rpcService[method] as any)(...params, context)
if (typeof retValue === 'function') {
retValue = retValue(context)
}
if (!isPromise(retValue) && isNotification) { if (!isPromise(retValue) && isNotification) {
return null return null

View File

@ -1,11 +1,12 @@
import {createLocalClient} from './local' import {createLocalClient} from './local'
import {keys} from 'ts-transformer-keys' import {keys} from 'ts-transformer-keys'
import {Contextual, ReverseContextual, TAsyncified} from './types'
describe('local', () => { describe('local', () => {
interface IService { interface IService {
add(a: number, b: number): number add(a: number, b: number): number
addWithContext(a: number, b: number): (context: IContext) => number addWithContext(a: number, b: number): number
} }
const IServiceKeys = keys<IService>() const IServiceKeys = keys<IService>()
@ -13,19 +14,18 @@ describe('local', () => {
userId: 1000 userId: 1000
} }
class Service implements IService { class Service implements Contextual<IService, IContext> {
add(a: number, b: number) { add(cx: IContext, a: number, b: number) {
return a + b return a + b
} }
addWithContext = (a: number, b: number) => (context: IContext) => { addWithContext = (cx: IContext, a: number, b: number) => {
return a + b + context.userId return a + b + cx.userId
} }
} }
const service = new Service() const service = new Service()
const proxy = createLocalClient<IService>(service, {
userId: 1000, const proxy = createLocalClient(service, () => ({userId: 1000}))
})
describe('add', () => { describe('add', () => {
it('should add two numbers', async () => { it('should add two numbers', async () => {

View File

@ -1,21 +1,24 @@
import {TAsyncified} from './types' import {TAsyncified, Contextual, ReverseContextual} from './types'
import {Request} from 'express'
import {TGetContext} from './express'
/** /**
* Creates a local client for a specific service instance. The actual service * Creates a local client for a specific service instance. The actual service
* will be invoked as if it would be remotely. This helps keep the API similar * will be invoked as if it would be remotely. This helps keep the API similar
* on the client- and server-side. * on the client- and server-side.
*/ */
export function createLocalClient<T>(service: T, context: any) { export function createLocalClient<T, Context>(
service: T,
getContext: () => Context,
): TAsyncified<ReverseContextual<T>> {
const proxy = new Proxy({}, { const proxy = new Proxy({}, {
get(obj, prop) { get(obj, prop) {
const context = getContext()
return async function makeRequest(...args: any[]) { return async function makeRequest(...args: any[]) {
const result = (service as any)[prop](...args) const result = (service as any)[prop](context, ...args)
if (typeof result === 'function') {
return result(context)
}
return result return result
} }
}, },
}) })
return proxy as TAsyncified<T> return proxy as any
} }

View File

@ -6,7 +6,7 @@ import bodyParser from 'body-parser'
import express from 'express' import express from 'express'
import {AddressInfo} from 'net' import {AddressInfo} from 'net'
import {Server} from 'http' import {Server} from 'http'
import {TPendingActions, TAllActions} from './types' import {Contextual, TPendingActions, TAllActions} from './types'
import {combineReducers} from 'redux' import {combineReducers} from 'redux'
import {createActions, createReducer} from './redux' import {createActions, createReducer} from './redux'
import {createRemoteClient} from './remote' import {createRemoteClient} from './remote'
@ -21,9 +21,8 @@ describe('createActions', () => {
add(a: number, b: number): number add(a: number, b: number): number
addAsync(a: number, b: number): Promise<number> addAsync(a: number, b: number): Promise<number>
addStringsAsync(a: string, b: string): Promise<string> addStringsAsync(a: string, b: string): Promise<string>
addWithContext(a: number, b: number): (ctx: IContext) => number addWithContext(a: number, b: number): number
addAsyncWithContext(a: number, b: number): (ctx: IContext) => addAsyncWithContext(a: number, b: number): Promise<number>
Promise<number>
throwError(bool: boolean): boolean throwError(bool: boolean): boolean
} }
@ -31,21 +30,21 @@ describe('createActions', () => {
userId: number userId: number
} }
class Service implements IService { class Service implements Contextual<IService, IContext> {
add(a: number, b: number) { add(cx: IContext, a: number, b: number) {
return a + b return a + b
} }
addAsync(a: number, b: number) { addAsync(cx: IContext, a: number, b: number) {
return new Promise<number>(resolve => resolve(a + b)) return new Promise<number>(resolve => resolve(a + b))
} }
addStringsAsync(a: string, b: string) { addStringsAsync(cx: IContext, a: string, b: string) {
return new Promise<string>(resolve => resolve(a + b)) return new Promise<string>(resolve => resolve(a + b))
} }
addWithContext = (a: number, b: number) => (ctx: IContext) => addWithContext = (cx: IContext, a: number, b: number) =>
a + b + ctx.userId a + b + cx.userId
addAsyncWithContext = (a: number, b: number) => (ctx: IContext) => addAsyncWithContext = (cx: IContext, a: number, b: number) =>
new Promise<number>(resolve => resolve(a + b + ctx.userId)) new Promise<number>(resolve => resolve(a + b + cx.userId))
throwError(bool: boolean) { throwError(cx: IContext, bool: boolean) {
if (bool) { if (bool) {
throw new Error('test') throw new Error('test')
} }

View File

@ -10,6 +10,7 @@ import {createRemoteClient} from './remote'
import {jsonrpc} from './express' import {jsonrpc} from './express'
import {keys} from 'ts-transformer-keys' import {keys} from 'ts-transformer-keys'
import {noopLogger} from './test-utils' import {noopLogger} from './test-utils'
import {Contextual} from './types'
describe('remote', () => { describe('remote', () => {
@ -20,11 +21,11 @@ describe('remote', () => {
} }
const IServiceKeys = keys<IService>() const IServiceKeys = keys<IService>()
class Service implements IService { class Service implements Contextual<IService, {}> {
add(a: number, b: number) { add(ctx: {}, a: number, b: number) {
return a + b return a + b
} }
async fetchItem(obj1: {a: number}, obj2: {b: number}) async fetchItem(ctx: {}, obj1: {a: number}, obj2: {b: number})
: Promise<{a: number, b: number}> { : Promise<{a: number, b: number}> {
return Promise.resolve({...obj1, ...obj2}) return Promise.resolve({...obj1, ...obj2})
} }

View File

@ -3,11 +3,10 @@ import {IPendingAction, IResolvedAction, IRejectedAction} from '@rondo.dev/clien
export type ArgumentTypes<T> = export type ArgumentTypes<T> =
T extends (...args: infer U) => infer R ? U : never T extends (...args: infer U) => infer R ? U : never
export type RetType<T> = T extends (...args: any[]) => infer R ? R : never export type RetType<T> = T extends (...args: any[]) => infer R ? R : never
type UnwrapHOC<T> = T extends (...args: any[]) => infer R ? R : T
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T type UnwrapPromise<T> = T extends Promise<infer V> ? V : T
type RetProm<T> = T extends Promise<any> ? T : Promise<T> type RetProm<T> = T extends Promise<any> ? T : Promise<T>
type PromisifyReturnType<T> = (...a: ArgumentTypes<T>) => type PromisifyReturnType<T> = (...a: ArgumentTypes<T>) =>
RetProm<UnwrapHOC<RetType<T>>> RetProm<RetType<T>>
/** /**
* Helps implement a service from a service definiton that has a context as a * Helps implement a service from a service definiton that has a context as a
@ -15,16 +14,15 @@ type PromisifyReturnType<T> = (...a: ArgumentTypes<T>) =>
*/ */
export type Contextual<T, Cx> = { export type Contextual<T, Cx> = {
[K in keyof T]: [K in keyof T]:
T[K] extends () => infer R T[K] extends (...args: infer A) => infer R
? (cx: Cx) => R : ? (cx: Cx, ...args: A) => R :
T[K] extends (a: infer A) => infer R never
? (a: A, cx: Cx) => R : }
T[K] extends (a: infer A, b: infer B) => infer R
? (a: A, b: B, cx: Cx) => R : export type ReverseContextual<T> = {
T[K] extends (a: infer A, b: infer B, c: infer C) => infer R [K in keyof T]:
? (a: A, b: B, c: C, cx: Cx) => R : T[K] extends (cx: any, ...args: infer A) => infer R
T[K] extends (a: infer A, b: infer B, c: infer C, d: infer D) => infer R ? (...args: A) => R :
? (a: A, b: B, c: C, d: D, cx: Cx) => R :
never never
} }
@ -46,7 +44,7 @@ export interface IReduxed<ActionType extends string> {
export type TReduxed<T, ActionType extends string> = { export type TReduxed<T, ActionType extends string> = {
[K in keyof T]: (...a: ArgumentTypes<T[K]>) => [K in keyof T]: (...a: ArgumentTypes<T[K]>) =>
IRPCPendingAction<UnwrapPromise<UnwrapHOC<RetType<T[K]>>>, ActionType, K> IRPCPendingAction<UnwrapPromise<RetType<T[K]>>, ActionType, K>
} }
export type TReduxHandlers<T, State> = { export type TReduxHandlers<T, State> = {