Breaking change: JSONRPC context is always 1st argument
This commit is contained in:
parent
b8fb7c2eba
commit
0222455cd7
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
@ -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})
|
||||
}
|
||||
|
||||
@ -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> = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user