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 {jsonrpc} from './express'
import {noopLogger} from './test-utils'
import {Contextual} from './types'
describe('jsonrpc', () => {
@ -20,15 +21,15 @@ describe('jsonrpc', () => {
asyncError(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>
}
const ensureLoggedIn = ensure<IContext>(c => !!c.userId)
class Service implements IService {
class Service implements Contextual<IService, IContext> {
constructor(readonly time: number) {}
add(a: number, b: number) {
add(context: IContext, a: number, b: number) {
return a + b
}
multiply(...numbers: number[]) {
@ -39,13 +40,13 @@ describe('jsonrpc', () => {
setTimeout(resolve, this.time)
})
}
syncError(message: string) {
syncError(context: IContext, message: string) {
throw new Error(message)
}
async asyncError(message: string) {
async asyncError(context: IContext, message: string) {
throw new Error(message)
}
async httpError(statusCode: number, message: string) {
async httpError(context: IContext, statusCode: number, message: string) {
const err: any = new Error(message)
err.statusCode = statusCode
err.errors = [{
@ -53,12 +54,12 @@ describe('jsonrpc', () => {
}]
throw err
}
addWithContext = (a: number, b: number) => (ctx: IContext): number => {
addWithContext = (ctx: IContext, a: number, b: number) => {
return a + b + ctx.userId
}
@ensureLoggedIn
addWithContext2(a: number, b: number, ctx?: IContext) {
addWithContext2(ctx: IContext, a: number, b: number) {
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)
// FIXME TODO if user specified too many parameters in the request,
// 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)
}
let retValue = (rpcService[method] as any)(context, ...params)
if (!isPromise(retValue) && isNotification) {
return null

View File

@ -1,11 +1,12 @@
import {createLocalClient} from './local'
import {keys} from 'ts-transformer-keys'
import {Contextual, ReverseContextual, TAsyncified} from './types'
describe('local', () => {
interface IService {
add(a: number, b: number): number
addWithContext(a: number, b: number): (context: IContext) => number
addWithContext(a: number, b: number): number
}
const IServiceKeys = keys<IService>()
@ -13,19 +14,18 @@ describe('local', () => {
userId: 1000
}
class Service implements IService {
add(a: number, b: number) {
class Service implements Contextual<IService, IContext> {
add(cx: IContext, a: number, b: number) {
return a + b
}
addWithContext = (a: number, b: number) => (context: IContext) => {
return a + b + context.userId
addWithContext = (cx: IContext, a: number, b: number) => {
return a + b + cx.userId
}
}
const service = new Service()
const proxy = createLocalClient<IService>(service, {
userId: 1000,
})
const proxy = createLocalClient(service, () => ({userId: 1000}))
describe('add', () => {
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
* will be invoked as if it would be remotely. This helps keep the API similar
* 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({}, {
get(obj, prop) {
const context = getContext()
return async function makeRequest(...args: any[]) {
const result = (service as any)[prop](...args)
if (typeof result === 'function') {
return result(context)
}
const result = (service as any)[prop](context, ...args)
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 {AddressInfo} from 'net'
import {Server} from 'http'
import {TPendingActions, TAllActions} from './types'
import {Contextual, TPendingActions, TAllActions} from './types'
import {combineReducers} from 'redux'
import {createActions, createReducer} from './redux'
import {createRemoteClient} from './remote'
@ -21,9 +21,8 @@ describe('createActions', () => {
add(a: number, b: number): number
addAsync(a: number, b: number): Promise<number>
addStringsAsync(a: string, b: string): Promise<string>
addWithContext(a: number, b: number): (ctx: IContext) => number
addAsyncWithContext(a: number, b: number): (ctx: IContext) =>
Promise<number>
addWithContext(a: number, b: number): number
addAsyncWithContext(a: number, b: number): Promise<number>
throwError(bool: boolean): boolean
}
@ -31,21 +30,21 @@ describe('createActions', () => {
userId: number
}
class Service implements IService {
add(a: number, b: number) {
class Service implements Contextual<IService, IContext> {
add(cx: IContext, a: number, b: number) {
return a + b
}
addAsync(a: number, b: number) {
addAsync(cx: IContext, a: number, b: number) {
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))
}
addWithContext = (a: number, b: number) => (ctx: IContext) =>
a + b + ctx.userId
addAsyncWithContext = (a: number, b: number) => (ctx: IContext) =>
new Promise<number>(resolve => resolve(a + b + ctx.userId))
throwError(bool: boolean) {
addWithContext = (cx: IContext, a: number, b: number) =>
a + b + cx.userId
addAsyncWithContext = (cx: IContext, a: number, b: number) =>
new Promise<number>(resolve => resolve(a + b + cx.userId))
throwError(cx: IContext, bool: boolean) {
if (bool) {
throw new Error('test')
}

View File

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

View File

@ -3,11 +3,10 @@ import {IPendingAction, IResolvedAction, IRejectedAction} from '@rondo.dev/clien
export type ArgumentTypes<T> =
T extends (...args: infer U) => infer R ? U : 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 RetProm<T> = T extends Promise<any> ? T : Promise<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
@ -15,16 +14,15 @@ type PromisifyReturnType<T> = (...a: ArgumentTypes<T>) =>
*/
export type Contextual<T, Cx> = {
[K in keyof T]:
T[K] extends () => infer R
? (cx: Cx) => R :
T[K] extends (a: infer A) => infer R
? (a: A, cx: Cx) => R :
T[K] extends (a: infer A, b: infer B) => infer R
? (a: A, b: B, cx: Cx) => R :
T[K] extends (a: infer A, b: infer B, c: infer C) => infer R
? (a: A, b: B, c: C, cx: Cx) => R :
T[K] extends (a: infer A, b: infer B, c: infer C, d: infer D) => infer R
? (a: A, b: B, c: C, d: D, cx: Cx) => R :
T[K] extends (...args: infer A) => infer R
? (cx: Cx, ...args: A) => R :
never
}
export type ReverseContextual<T> = {
[K in keyof T]:
T[K] extends (cx: any, ...args: infer A) => infer R
? (...args: A) => R :
never
}
@ -46,7 +44,7 @@ export interface IReduxed<ActionType extends string> {
export type TReduxed<T, ActionType extends string> = {
[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> = {