Fix linting errors in packages/jsonrpc

This commit is contained in:
Jerko Steiner 2019-09-15 17:14:31 +07:00
parent 8853c71a0d
commit 50d36f268f
21 changed files with 184 additions and 177 deletions

View File

@ -11,6 +11,10 @@ rules:
- warn - warn
- code: 80 - code: 80
ignorePattern: '^import .* from ' ignorePattern: '^import .* from '
comma-dangle:
- warn
- always-multiline
# semi: # semi:
# - warn # - warn
# - never # - never

View File

@ -0,0 +1,8 @@
extends:
- ../../.eslintrc.yaml
rules:
# due to the nature of this project, writing code becomes much easier
# when any can be used in some places
'@typescript-eslint/no-explicit-any': off
prefer-rest-params: off
prefer-spread: off

View File

@ -8,30 +8,30 @@ import {json} from 'body-parser'
describe('util', () => { describe('util', () => {
interface IS1 { interface S1 {
add(a: number, b: number): number add(a: number, b: number): number
} }
interface IS2 { interface S2 {
mul(a: number, b: number): number mul(a: number, b: number): number
concat(...str: string[]): string concat(...str: string[]): string
} }
interface IContext { interface Context {
userId: number userId: number
} }
class Service1 implements WithContext<IS1, IContext> { class Service1 implements WithContext<S1, Context> {
add(cx: IContext, a: number, b: number) { add(cx: Context, a: number, b: number) {
return a + b + cx.userId return a + b + cx.userId
} }
} }
class Service2 implements WithContext<IS2, IContext> { class Service2 implements WithContext<S2, Context> {
mul(cx: IContext, a: number, b: number) { mul(cx: Context, a: number, b: number) {
return a * b + cx.userId return a * b + cx.userId
} }
concat(cx: IContext, ...str: string[]) { concat(cx: Context, ...str: string[]) {
return str.join('') + cx.userId return str.join('') + cx.userId
} }
} }
@ -98,7 +98,7 @@ describe('util', () => {
services, services,
) )
const app = createApp(router) const app = createApp(router)
const client = createClient<IS1>(app, '/rpc/s1') const client = createClient<S1>(app, '/rpc/s1')
const result: number = await client.add(1, 3) const result: number = await client.add(1, 3)
expect(result).toBe(14) expect(result).toBe(14)
}) })

View File

@ -1,4 +1,5 @@
import {IJSONRPCReturnType} from './express' /* eslint @typescript-eslint/no-explicit-any: off */
import {RPCReturnType} from './express'
import {WithContext, RPCClient, RPCActions} from './types' import {WithContext, RPCClient, RPCActions} from './types'
import {createActions} from './redux' import {createActions} from './redux'
import {createLocalClient, LocalClient} from './local' import {createLocalClient, LocalClient} from './local'
@ -39,7 +40,7 @@ export function bulkCreateActions<T extends Record<string, RPCClient<any>>>(
} }
export function bulkjsonrpc<T>( export function bulkjsonrpc<T>(
jsonrpc: IJSONRPCReturnType, jsonrpc: RPCReturnType,
services: T, services: T,
) { ) {
keys(services).forEach(key => { keys(services).forEach(key => {

View File

@ -2,13 +2,13 @@ import createClientMock from './createClientMock'
describe('createClientMock', () => { describe('createClientMock', () => {
interface IService { interface Service {
add(a: number, b: number): number add(a: number, b: number): number
concat(a: string, b: string): string concat(a: string, b: string): string
} }
it('creates a mock for all methods', async () => { it('creates a mock for all methods', async () => {
const [client, mock] = createClientMock<IService>(['add', 'concat']) const [client, mock] = createClientMock<Service>(['add', 'concat'])
mock.add.mockReturnValue(Promise.resolve(3)) mock.add.mockReturnValue(Promise.resolve(3))
mock.concat.mockImplementation((a, b) => Promise.resolve(a + b)) mock.concat.mockImplementation((a, b) => Promise.resolve(a + b))
expect(await client.add(4, 5)).toBe(3) expect(await client.add(4, 5)).toBe(3)

View File

@ -1,6 +1,6 @@
import { FunctionPropertyNames, RPCClient } from './types' import { FunctionPropertyNames, RPCClient } from './types'
export type TMocked<T> = { export type RPCClientMock<T> = {
[K in keyof T]: [K in keyof T]:
T[K] extends (...args: infer A) => infer R T[K] extends (...args: infer A) => infer R
? jest.Mock<R, A> ? jest.Mock<R, A>
@ -20,7 +20,7 @@ export type TMocked<T> = {
*/ */
export default function createClientMock<T extends object>( export default function createClientMock<T extends object>(
methods: Array<FunctionPropertyNames<T>>, methods: Array<FunctionPropertyNames<T>>,
): [RPCClient<T>, TMocked<RPCClient<T>>] { ): [RPCClient<T>, RPCClientMock<RPCClient<T>>] {
const client = methods const client = methods
.reduce((obj, prop) => { .reduce((obj, prop) => {
obj[prop] = jest.fn() obj[prop] = jest.fn()
@ -29,6 +29,6 @@ export default function createClientMock<T extends object>(
return [ return [
client as RPCClient<T>, client as RPCClient<T>,
client as TMocked<RPCClient<T>>, client as RPCClientMock<RPCClient<T>>,
] ]
} }

View File

@ -7,15 +7,15 @@ import {
describe('ensure', () => { describe('ensure', () => {
interface IContext { interface Context {
userId: number userId: number
} }
const validate: Validate<IContext> = c => c.userId > 0 const validate: Validate<Context> = c => c.userId > 0
it('decorates class methods', () => { it('decorates class methods', () => {
class Service { class Service {
@ensure<IContext>(validate) @ensure<Context>(validate)
fetchData() { fetchData() {
return 1 return 1
} }
@ -32,7 +32,7 @@ describe('ensure', () => {
it('works with properties/instance method definitions', () => { it('works with properties/instance method definitions', () => {
class Service { class Service {
@ensure<IContext>(validate) @ensure<Context>(validate)
fetchData = () => 1 fetchData = () => 1
} }
const s = new Service() const s = new Service()

View File

@ -1,3 +1,4 @@
/* eslint @typescript-eslint/no-unused-vars: 0 */
import 'reflect-metadata' import 'reflect-metadata'
export const ensureKey = Symbol('ensure') export const ensureKey = Symbol('ensure')
@ -7,10 +8,11 @@ export type Validate<Context> = (context: Context) => boolean | Promise<boolean>
export function ensure<Context>( export function ensure<Context>(
validate: Validate<Context>, validate: Validate<Context>,
message: string = 'Validation failed', message = 'Validation failed',
) { ) {
return function ensureImpl( return function ensureImpl(
target: any, key?: string, descriptor?: PropertyDescriptor) { target: any, key?: string, descriptor?: PropertyDescriptor,
) {
switch (arguments.length) { switch (arguments.length) {
case 1: case 1:
return ensureClass(validate, message).apply(null, arguments as any) return ensureClass(validate, message).apply(null, arguments as any)
@ -25,7 +27,7 @@ export function ensure<Context>(
function ensureClass<Context>( function ensureClass<Context>(
validate: Validate<Context>, validate: Validate<Context>,
message: string = 'Validation failed', message = 'Validation failed',
) { ) {
// tslint:disable-next-line // tslint:disable-next-line
return (target: any) => { return (target: any) => {
@ -40,7 +42,7 @@ function ensureClass<Context>(
function ensureMethod<Context>( function ensureMethod<Context>(
validate: Validate<Context>, validate: Validate<Context>,
message: string = 'Validation failed', message = 'Validation failed',
) { ) {
return ( return (
target: any, target: any,

View File

@ -1,44 +1,44 @@
export interface IError { export interface ErrorWithCode {
code: number code: number
message: string message: string
} }
export interface IErrorWithData<T> extends IError { export interface ErrorWithData<T> extends ErrorWithCode {
data: T data: T
} }
export interface IErrorResponse<T> { export interface ErrorResponse<T> {
jsonrpc: '2.0' jsonrpc: '2.0'
id: string | number | null id: string | number | null
result: null result: null
error: IErrorWithData<T> error: ErrorWithData<T>
} }
export interface IJSONRPCError<T> extends Error { export interface RPCError<T> extends Error {
code: number code: number
statusCode: number statusCode: number
response: IErrorResponse<T> response: ErrorResponse<T>
} }
export function isJSONRPCError(err: any): err is IJSONRPCError<unknown> { export function isRPCError(err: any): err is RPCError<unknown> {
return err.name === 'IJSONRPCError' && return err.name === 'RPCError' &&
typeof err.message === 'string' && typeof err.message === 'string' &&
err.hasOwnProperty('code') && Object.prototype.hasOwnProperty.call(err, 'code') &&
err.hasOwnProperty('response') Object.prototype.hasOwnProperty.call(err, 'response')
} }
export function createError<T = null>( export function createError<T = null>(
error: IError, error: ErrorWithCode,
info: { info: {
id: number | string | null id: number | string | null
data: T data: T
statusCode: number statusCode: number
}, },
): IJSONRPCError<T> { ): RPCError<T> {
const err = new Error(error.message) as IJSONRPCError<T> const err = new Error(error.message) as RPCError<T>
err.name = 'IJSONRPCError' err.name = 'RPCError'
err.code = error.code err.code = error.code
err.statusCode = info.statusCode err.statusCode = info.statusCode
err.response = { err.response = {

View File

@ -1,7 +1,7 @@
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import express from 'express' import express from 'express'
import request from 'supertest' import request from 'supertest'
import {IRequest} from './jsonrpc' import {Request} from './jsonrpc'
import {createClient} from './supertest' import {createClient} from './supertest'
import {ensure} from './ensure' import {ensure} from './ensure'
import {jsonrpc} from './express' import {jsonrpc} from './express'
@ -10,11 +10,11 @@ import {WithContext} from './types'
describe('jsonrpc', () => { describe('jsonrpc', () => {
interface IContext { interface Context {
userId: number userId: number
} }
interface IService { interface Service {
add(a: number, b: number): number add(a: number, b: number): number
delay(): Promise<void> delay(): Promise<void>
syncError(message: string): void syncError(message: string): void
@ -25,11 +25,11 @@ describe('jsonrpc', () => {
addWithContext2(a: number, b: number): Promise<number> addWithContext2(a: number, b: number): Promise<number>
} }
const ensureLoggedIn = ensure<IContext>(c => !!c.userId) const ensureLoggedIn = ensure<Context>(c => !!c.userId)
class Service implements WithContext<IService, IContext> { class MyService implements WithContext<Service, Context> {
constructor(readonly time: number) {} constructor(readonly time: number) {}
add(context: IContext, a: number, b: number) { add(context: Context, a: number, b: number) {
return a + b return a + b
} }
multiply(...numbers: number[]) { multiply(...numbers: number[]) {
@ -40,13 +40,13 @@ describe('jsonrpc', () => {
setTimeout(resolve, this.time) setTimeout(resolve, this.time)
}) })
} }
syncError(context: IContext, message: string) { syncError(context: Context, message: string) {
throw new Error(message) throw new Error(message)
} }
async asyncError(context: IContext, message: string) { async asyncError(context: Context, message: string) {
throw new Error(message) throw new Error(message)
} }
async httpError(context: IContext, statusCode: number, message: string) { async httpError(context: Context, 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 = [{
@ -54,14 +54,14 @@ describe('jsonrpc', () => {
}] }]
throw err throw err
} }
addWithContext = (ctx: IContext, a: number, b: number) => { addWithContext = (ctx: Context, a: number, b: number) => {
return a + b + ctx.userId return a + b + ctx.userId
} }
_private = () => { _private = () => {
return 1 return 1
} }
@ensureLoggedIn @ensureLoggedIn
addWithContext2(ctx: IContext, a: number, b: number) { addWithContext2(ctx: Context, a: number, b: number) {
return Promise.resolve(a + b + ctx!.userId) return Promise.resolve(a + b + ctx!.userId)
} }
} }
@ -72,8 +72,8 @@ describe('jsonrpc', () => {
const app = express() const app = express()
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use('/', app.use('/',
jsonrpc(req => ({userId}), noopLogger) jsonrpc(() => ({userId}), noopLogger)
.addService('/myService', new Service(5), [ .addService('/myService', new MyService(5), [
'add', 'add',
'delay', 'delay',
'syncError', 'syncError',
@ -87,7 +87,7 @@ describe('jsonrpc', () => {
return app return app
} }
const client = createClient<IService>(createApp(), '/myService') const client = createClient<Service>(createApp(), '/myService')
async function getError(promise: Promise<unknown>) { async function getError(promise: Promise<unknown>) {
let error let error
@ -260,7 +260,7 @@ describe('jsonrpc', () => {
describe('hook', () => { describe('hook', () => {
let requests: IRequest[] = [] let requests: Request[] = []
let results: any[] = [] let results: any[] = []
function create() { function create() {
requests = [] requests = []
@ -268,12 +268,12 @@ describe('jsonrpc', () => {
userId = 1000 userId = 1000
const app = express() const app = express()
const myService = new Service(5) const myService = new MyService(5)
// console.log('service', myService, Object. // console.log('service', myService, Object.
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use('/', app.use('/',
jsonrpc( jsonrpc(
req => Promise.resolve({userId}), () => Promise.resolve({userId}),
noopLogger, noopLogger,
async (details, makeRequest) => { async (details, makeRequest) => {
requests.push(details.request) requests.push(details.request)

View File

@ -1,35 +1,29 @@
import express, {ErrorRequestHandler} from 'express' import { Logger } from '@rondo.dev/logger'
import {FunctionPropertyNames} from './types' import express, { ErrorRequestHandler, Request, Response, Router } from 'express'
import { createError, ErrorResponse, isRPCError } from './error'
import { IDEMPOTENT_METHOD_REGEX } from './idempotent' import { IDEMPOTENT_METHOD_REGEX } from './idempotent'
import {IErrorResponse} from './error' import { createRpcService, ERROR_METHOD_NOT_FOUND, ERROR_SERVER, IRequest, SuccessResponse } from './jsonrpc'
import {ILogger} from '@rondo.dev/logger' import { FunctionPropertyNames } from './types'
import {ISuccessResponse} from './jsonrpc'
import {NextFunction, Request, Response, Router} from 'express'
import {createError, isJSONRPCError, IJSONRPCError, IError} from './error'
import {
createRpcService, ERROR_SERVER, ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND,
IRequest,
} from './jsonrpc'
export type TGetContext<Context> = (req: Request) => Promise<Context> | Context export type TGetContext<Context> = (req: Request) => Promise<Context> | Context
export interface IJSONRPCReturnType { export interface RPCReturnType {
addService<T, F extends FunctionPropertyNames<T>>( addService<T, F extends FunctionPropertyNames<T>>(
path: string, path: string,
service: T, service: T,
methods?: F[], methods?: F[],
): IJSONRPCReturnType, ): RPCReturnType
router(): Router router(): Router
} }
export interface IInvocationDetails<A extends IRequest, Context> { export interface InvocationDetails<A extends IRequest, Context> {
context: Context context: Context
path: string path: string
request: A request: A
} }
async function defaultHook<A extends IRequest, R, Context>( async function defaultHook<A extends IRequest, R, Context>(
details: IInvocationDetails<A, Context>, details: InvocationDetails<A, Context>,
invoke: () => Promise<R>, invoke: () => Promise<R>,
): Promise<R> { ): Promise<R> {
const result = await invoke() const result = await invoke()
@ -38,17 +32,18 @@ async function defaultHook<A extends IRequest, R, Context>(
export function jsonrpc<Context>( export function jsonrpc<Context>(
getContext: TGetContext<Context>, getContext: TGetContext<Context>,
logger: ILogger, logger: Logger,
hook: <A extends IRequest, R>( hook: <A extends IRequest, R>(
details: IInvocationDetails<A, Context>, details: InvocationDetails<A, Context>,
invoke: (request?: A) => Promise<R>) => Promise<R> = defaultHook, invoke: (request?: A) => Promise<R>) => Promise<R> = defaultHook,
idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX, idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX,
): IJSONRPCReturnType { ): RPCReturnType {
/* eslint @typescript-eslint/no-unused-vars: 0 */
const handleError: ErrorRequestHandler = (err, req, res, next) => { const handleError: ErrorRequestHandler = (err, req, res, next) => {
logger.error('JSON-RPC Error: %s', err.stack) logger.error('JSON-RPC Error: %s', err.stack)
if (isJSONRPCError(err)) { if (isRPCError(err)) {
res.status(err.statusCode) res.status(err.statusCode)
res.json(err.response) res.json(err.response)
return return
@ -56,7 +51,7 @@ export function jsonrpc<Context>(
const id = getRequestId(req) const id = getRequestId(req)
const statusCode: number = err.statusCode || 500 const statusCode: number = err.statusCode || 500
const errorResponse: IErrorResponse<unknown> = { const errorResponse: ErrorResponse<unknown> = {
jsonrpc: '2.0', jsonrpc: '2.0',
id, id,
result: null, result: null,
@ -85,7 +80,7 @@ export function jsonrpc<Context>(
const rpcService = createRpcService(service, methods) const rpcService = createRpcService(service, methods)
function handleResponse( function handleResponse(
response: ISuccessResponse<unknown> | null, response: SuccessResponse<unknown> | null,
res: Response, res: Response,
) { ) {
if (response === null) { if (response === null) {

View File

@ -1,7 +1,7 @@
export type TId = number | string 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, ErrorResponse} from './error'
import {getValidatorsForMethod, getValidatorsForInstance} from './ensure' import {getValidatorsForMethod, getValidatorsForInstance} from './ensure'
import {Validate} from './ensure' import {Validate} from './ensure'
@ -35,8 +35,8 @@ export const ERROR_SERVER = {
message: 'Server error', message: 'Server error',
} }
export function pick<T, K extends FunctionPropertyNames<T>>(t: T, keys: K[]) export function pick<T, K extends FunctionPropertyNames<T>>(
: Pick<T, K> { t: T, keys: K[]): Pick<T, K> {
return keys.reduce((obj, key) => { return keys.reduce((obj, key) => {
// tslint:disable-next-line // tslint:disable-next-line
const fn = t[key] as unknown as Function const fn = t[key] as unknown as Function
@ -48,8 +48,8 @@ export function pick<T, K extends FunctionPropertyNames<T>>(t: T, keys: K[])
export function getAllMethods<T>(obj: T): Array<FunctionPropertyNames<T>> { export function getAllMethods<T>(obj: T): Array<FunctionPropertyNames<T>> {
const props = new Set<string>() const props = new Set<string>()
do { do {
const l = Object.getOwnPropertyNames(obj) Object.getOwnPropertyNames(obj)
.filter((p, i, arr) => { .filter(p => {
return typeof (obj as any)[p] === 'function' && return typeof (obj as any)[p] === 'function' &&
p.startsWith('_') === false && p.startsWith('_') === false &&
p !== 'constructor' p !== 'constructor'
@ -61,24 +61,25 @@ export function getAllMethods<T>(obj: T): Array<FunctionPropertyNames<T>> {
return Array.from(props) as unknown as Array<FunctionPropertyNames<T>> return Array.from(props) as unknown as Array<FunctionPropertyNames<T>>
} }
export interface IRequest<M extends string = any, A = any[]> { export interface Request<M extends string = any, A = any[]> {
jsonrpc: '2.0' jsonrpc: '2.0'
id: TId | null id: TId | null
method: M method: M
params: A params: A
} }
export interface ISuccessResponse<T> { export interface SuccessResponse<T> {
jsonrpc: '2.0' jsonrpc: '2.0'
id: TId id: TId
result: T result: T
error: null error: null
} }
export type IResponse<T = any> = ISuccessResponse<T> | IErrorResponse<T> export type Response<T = any> = SuccessResponse<T> | ErrorResponse<T>
export function createSuccessResponse<T>(id: number | string, result: T) export function createSuccessResponse<T>(
: ISuccessResponse<T> { id: number | string, result: T
): SuccessResponse<T> {
return { return {
jsonrpc: '2.0', jsonrpc: '2.0',
id, id,
@ -125,9 +126,9 @@ export const createRpcService = <T, M extends FunctionPropertyNames<T>>(
pick(service, getAllMethods(service)) pick(service, getAllMethods(service))
return { return {
async invoke<Context>( async invoke<Context>(
req: IRequest<M, ArgumentTypes<T[M]>>, req: Request<M, ArgumentTypes<T[M]>>,
context: Context, context: Context,
): Promise<ISuccessResponse<RetType<T[M]>> | null> { ): Promise<SuccessResponse<RetType<T[M]>> | null> {
const {id, method, params} = req const {id, method, params} = req
if ( if (
@ -145,7 +146,7 @@ export const createRpcService = <T, M extends FunctionPropertyNames<T>>(
const isNotification = req.id === null || req.id === undefined const isNotification = req.id === null || req.id === undefined
if ( if (
!rpcService.hasOwnProperty(method) || !Object.prototype.hasOwnProperty.call(rpcService, method) ||
typeof rpcService[method] !== 'function' typeof rpcService[method] !== 'function'
) { ) {
throw createError(ERROR_METHOD_NOT_FOUND, { throw createError(ERROR_METHOD_NOT_FOUND, {

View File

@ -1,29 +1,27 @@
import { createLocalClient } from './local' import { createLocalClient } from './local'
import {keys} from 'ts-transformer-keys' import { WithContext } from './types'
import {WithContext, WithoutContext, RPCClient} from './types'
describe('local', () => { describe('local', () => {
interface IService { interface Service {
add(a: number, b: number): number add(a: number, b: number): number
addWithContext(a: number, b: number): number addWithContext(a: number, b: number): number
} }
const IServiceKeys = keys<IService>()
interface IContext { interface Context {
userId: 1000 userId: 1000
} }
class Service implements WithContext<IService, IContext> { class MyService implements WithContext<Service, Context> {
add(cx: IContext, a: number, b: number) { add(cx: Context, a: number, b: number) {
return a + b return a + b
} }
addWithContext = (cx: IContext, a: number, b: number) => { addWithContext = (cx: Context, a: number, b: number) => {
return a + b + cx.userId return a + b + cx.userId
} }
} }
const service = new Service() const service = new MyService()
const proxy = createLocalClient(service, {userId: 1000}) const proxy = createLocalClient(service, {userId: 1000})

View File

@ -1,7 +1,5 @@
import {RPCClient, WithContext, WithoutContext} from './types'
import {Request} from 'express'
import {TGetContext} from './express'
import { getAllMethods } from './jsonrpc' import { getAllMethods } from './jsonrpc'
import { RPCClient, WithoutContext } from './types'
export type LocalClient<T> = RPCClient<WithoutContext<T>> export type LocalClient<T> = RPCClient<WithoutContext<T>>

View File

@ -2,22 +2,22 @@
* @jest-environment node * @jest-environment node
*/ */
import { createStore } from '@rondo.dev/redux'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import express from 'express' import express from 'express'
import {AddressInfo} from 'net'
import { Server } from 'http' import { Server } from 'http'
import {WithContext, TPendingActions, TAllActions} from './types' import { AddressInfo } from 'net'
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import { keys } from 'ts-transformer-keys'
import { jsonrpc } from './express'
import { createActions, createReducer } from './redux' import { createActions, createReducer } from './redux'
import { createRemoteClient } from './remote' import { createRemoteClient } from './remote'
import {createStore} from '@rondo.dev/redux'
import {jsonrpc} from './express'
import {keys} from 'ts-transformer-keys'
import { noopLogger } from './test-utils' import { noopLogger } from './test-utils'
import { WithContext } from './types'
describe('createActions', () => { describe('createActions', () => {
interface IService { interface Service {
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>
@ -26,25 +26,25 @@ describe('createActions', () => {
throwError(bool: boolean): boolean throwError(bool: boolean): boolean
} }
interface IContext { interface Context {
userId: number userId: number
} }
class Service implements WithContext<IService, IContext> { class MyService implements WithContext<Service, Context> {
add(cx: IContext, a: number, b: number) { add(cx: Context, a: number, b: number) {
return a + b return a + b
} }
addAsync(cx: IContext, a: number, b: number) { addAsync(cx: Context, a: number, b: number) {
return new Promise<number>(resolve => resolve(a + b)) return new Promise<number>(resolve => resolve(a + b))
} }
addStringsAsync(cx: IContext, a: string, b: string) { addStringsAsync(cx: Context, a: string, b: string) {
return new Promise<string>(resolve => resolve(a + b)) return new Promise<string>(resolve => resolve(a + b))
} }
addWithContext = (cx: IContext, a: number, b: number) => addWithContext = (cx: Context, a: number, b: number) =>
a + b + cx.userId a + b + cx.userId
addAsyncWithContext = (cx: IContext, a: number, b: number) => addAsyncWithContext = (cx: Context, a: number, b: number) =>
new Promise<number>(resolve => resolve(a + b + cx.userId)) new Promise<number>(resolve => resolve(a + b + cx.userId))
throwError(cx: IContext, bool: boolean) { throwError(cx: Context, bool: boolean) {
if (bool) { if (bool) {
throw new Error('test') throw new Error('test')
} }
@ -57,7 +57,7 @@ describe('createActions', () => {
app.use( app.use(
'/', '/',
jsonrpc(() => ({userId: 1000}), noopLogger) jsonrpc(() => ({userId: 1000}), noopLogger)
.addService('/service', new Service(), keys<IService>()) .addService('/service', new MyService(), keys<Service>())
.router(), .router(),
) )
@ -78,8 +78,8 @@ describe('createActions', () => {
}) })
function getClient() { function getClient() {
const remoteClient = createRemoteClient<IService>( const remoteClient = createRemoteClient<Service>(
baseUrl + '/service', keys<IService>()) baseUrl + '/service', keys<Service>())
const client = createActions(remoteClient, 'myService') const client = createActions(remoteClient, 'myService')
const defaultState = { const defaultState = {
@ -96,16 +96,14 @@ describe('createActions', () => {
case 'addAsync': case 'addAsync':
case 'addWithContext': case 'addWithContext':
case 'addAsyncWithContext': case 'addAsyncWithContext':
const r1: number = action.payload
return { return {
...state, ...state,
add: r1, add: action.payload,
} }
case 'addStringsAsync': case 'addStringsAsync':
const r2: string = action.payload
return { return {
...state, ...state,
addStringsAsync: r2, addStringsAsync: action.payload,
} }
default: default:
return state return state
@ -139,7 +137,7 @@ describe('createActions', () => {
add: action.payload, add: action.payload,
} }
}, },
throwError(state, action) { throwError(state) {
return state return state
}, },
}) })

View File

@ -1,10 +1,10 @@
import { import {
IRPCActions,
RPCClient,
RPCActions, RPCActions,
RPCClient,
TResolvedActions, TResolvedActions,
TAllActions, TAllActions,
RPCReduxHandlers, RPCReduxHandlers,
RPCActionsRecord,
} from './types' } from './types'
export function createActions<T, ActionType extends string>( export function createActions<T, ActionType extends string>(
@ -27,18 +27,19 @@ export function createActions<T, ActionType extends string>(
return service as RPCActions<T, ActionType> return service as RPCActions<T, ActionType>
} }
export interface IState { export interface TestState {
loading: number loading: number
error: string error: string
} }
export function createReducer<ActionType extends string, State extends IState>( export function createReducer<
ActionType extends string, State extends TestState>(
actionType: ActionType, actionType: ActionType,
defaultState: State, defaultState: State,
) { ) {
const self = { const self = {
withHandler<R extends IRPCActions<ActionType>>( withHandler<R extends RPCActionsRecord<ActionType>>(
handleAction: (state: State, action: TResolvedActions<R>) => State, handleAction: (state: State, action: TResolvedActions<R>) => State,
): (state: State | undefined, action: TAllActions<R>) => State { ): (state: State | undefined, action: TAllActions<R>) => State {
return (state: State = defaultState, action: TAllActions<R>): State => { return (state: State = defaultState, action: TAllActions<R>): State => {
@ -66,7 +67,7 @@ export function createReducer<ActionType extends string, State extends IState>(
}, action) }, action)
} }
}, },
withMapping<R extends IRPCActions<ActionType>>( withMapping<R extends RPCActionsRecord<ActionType>>(
handlers: RPCReduxHandlers<R, State>, handlers: RPCReduxHandlers<R, State>,
) { ) {
return self.withHandler<R>((state, action) => { return self.withHandler<R>((state, action) => {

View File

@ -14,24 +14,25 @@ import {WithContext} from './types'
describe('remote', () => { describe('remote', () => {
interface IService { interface Service {
add(a: number, b: number): number add(a: number, b: number): number
fetchItem(obj1: {a: number}, obj2: {b: number}) fetchItem(
: Promise<{a: number, b: number}> obj1: {a: number}, obj2: {b: number}): Promise<{a: number, b: number}>
} }
const IServiceKeys = keys<IService>() const IServiceKeys = keys<Service>()
class Service implements WithContext<IService, {}> { class MyService implements WithContext<Service, {}> {
add(ctx: {}, a: number, b: number) { add(ctx: {}, a: number, b: number) {
return a + b return a + b
} }
async fetchItem(ctx: {}, obj1: {a: number}, obj2: {b: number}) async fetchItem(
: Promise<{a: number, b: number}> { ctx: {}, obj1: {a: number}, obj2: {b: number}
): Promise<{a: number, b: number}> {
return Promise.resolve({...obj1, ...obj2}) return Promise.resolve({...obj1, ...obj2})
} }
} }
const service = new Service() const service = new MyService()
function createApp() { function createApp() {
const a = express() const a = express()
@ -65,7 +66,7 @@ describe('remote', () => {
describe('idempotent method invocation (GET)', () => { describe('idempotent method invocation (GET)', () => {
it('creates a proxy for remote service', async () => { it('creates a proxy for remote service', async () => {
const rpc = createRemoteClient<IService>( const rpc = createRemoteClient<Service>(
baseUrl + '/myService', IServiceKeys) baseUrl + '/myService', IServiceKeys)
const result = await rpc.fetchItem({a: 10}, {b: 20}) const result = await rpc.fetchItem({a: 10}, {b: 20})
expect(result).toEqual({a: 10, b: 20}) expect(result).toEqual({a: 10, b: 20})
@ -74,7 +75,7 @@ describe('remote', () => {
describe('method invocation (POST)', () => { describe('method invocation (POST)', () => {
it('creates a proxy for remote service', async () => { it('creates a proxy for remote service', async () => {
const rpc = createRemoteClient<IService>( const rpc = createRemoteClient<Service>(
baseUrl + '/myService', IServiceKeys) baseUrl + '/myService', IServiceKeys)
const result = await rpc.add(3, 7) const result = await rpc.add(3, 7)
expect(result).toBe(3 + 7) expect(result).toBe(3 + 7)

View File

@ -21,7 +21,7 @@ export function createRemoteClient<T>(
method: string, method: string,
params: any[], params: any[],
) { ) {
const reqMethod = IDEMPOTENT_METHOD_REGEX.test(method) ? 'GET' : 'POST' const reqMethod = idempotentMethodRegex.test(method) ? 'GET' : 'POST'
const payloadKey = reqMethod === 'POST' ? 'data' : 'params' const payloadKey = reqMethod === 'POST' ? 'data' : 'params'
const response = await axios({ const response = await axios({

View File

@ -1,8 +1,8 @@
import {ILogger} from '@rondo.dev/logger' import {Logger} from '@rondo.dev/logger'
const noop = () => undefined const noop = () => undefined
export const noopLogger: ILogger = { export const noopLogger: Logger = {
error: noop, error: noop,
warn: noop, warn: noop,
info: noop, info: noop,

View File

@ -1,4 +1,4 @@
import {IPendingAction, IResolvedAction, IRejectedAction} from '@rondo.dev/redux' import {PendingAction, ResolvedAction, RejectedAction} from '@rondo.dev/redux'
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
@ -38,64 +38,64 @@ export type RPCClient<T> = {
[K in keyof T]: PromisifyReturnType<T[K]> [K in keyof T]: PromisifyReturnType<T[K]>
} }
export interface IRPCActions<ActionType extends string> { export interface RPCActionsRecord<ActionType extends string> {
[key: string]: (...a: any[]) => IRPCPendingAction<any, ActionType, typeof key> [key: string]: (...a: any[]) => RPCPendingAction<any, ActionType, typeof key>
} }
export type RPCActions<T, ActionType extends string> = { export type RPCActions<T, ActionType extends string> = {
[K in keyof T]: (...a: ArgumentTypes<T[K]>) => [K in keyof T]: (...a: ArgumentTypes<T[K]>) =>
IRPCPendingAction<UnwrapPromise<RetType<T[K]>>, ActionType, K> RPCPendingAction<UnwrapPromise<RetType<T[K]>>, ActionType, K>
} }
export type RPCReduxHandlers<T, State> = { export type RPCReduxHandlers<T, State> = {
[K in keyof T]: ( [K in keyof T]: (
state: State, state: State,
action: TResolved<TPending<RetType<T[K]>>>, action: GetResolvedAction<GetPendingAction<RetType<T[K]>>>,
) => Partial<State> ) => Partial<State>
} }
export interface IRPCPendingAction< export interface RPCPendingAction<
T, ActionType extends string, Method extends string | number | symbol T, ActionType extends string, Method extends string | number | symbol
> extends IPendingAction<T, ActionType> { > extends PendingAction<T, ActionType> {
method: Method method: Method
} }
export interface IRPCResolvedAction< export interface RPCResolvedAction<
T, ActionType extends string, Method extends string | symbol | number T, ActionType extends string, Method extends string | symbol | number
> extends IResolvedAction<T, ActionType> { > extends ResolvedAction<T, ActionType> {
method: Method method: Method
} }
export interface IRPCRejectedAction< export interface RPCRejectedAction<
ActionType extends string, Method extends string | symbol | number ActionType extends string, Method extends string | symbol | number
> extends IRejectedAction<ActionType> { > extends RejectedAction<ActionType> {
method: Method method: Method
} }
export type TRPCAction< export type TRPCAction<
T, ActionType extends string, Method extends string | number | symbol T, ActionType extends string, Method extends string | number | symbol
> = > =
IRPCPendingAction<T, ActionType, Method> RPCPendingAction<T, ActionType, Method>
| IRPCResolvedAction<T, ActionType, Method> | RPCResolvedAction<T, ActionType, Method>
| IRPCRejectedAction<ActionType, Method> | RPCRejectedAction<ActionType, Method>
export type TResolved<A> = export type GetResolvedAction<A> =
A extends IRPCPendingAction<infer T, infer ActionType, infer Method> A extends RPCPendingAction<infer T, infer ActionType, infer Method>
? IRPCResolvedAction<T, ActionType, Method> ? RPCResolvedAction<T, ActionType, Method>
: never : never
export type TRejected<A> = export type GetRejectedAction<A> =
A extends IRPCPendingAction<infer T, infer ActionType, infer Method> A extends RPCPendingAction<infer T, infer ActionType, infer Method>
? IRPCRejectedAction<ActionType, Method> ? RPCRejectedAction<ActionType, Method>
: never : never
export type TPending<A> = export type GetPendingAction<A> =
A extends IRPCPendingAction<infer T, infer ActionType, infer Method> A extends RPCPendingAction<infer T, infer ActionType, infer Method>
? IRPCPendingAction<T, ActionType, Method> ? RPCPendingAction<T, ActionType, Method>
: never : never
type Values<T> = T[keyof T] type Values<T> = T[keyof T]
export type TPendingActions<T> = TPending<RetType<Values<T>>> export type TPendingActions<T> = GetPendingAction<RetType<Values<T>>>
export type TResolvedActions<T> = TResolved<TPendingActions<T>> export type TResolvedActions<T> = GetResolvedAction<TPendingActions<T>>
export type TAllActions<T> = TPendingActions<T> export type TAllActions<T> = TPendingActions<T>
| TResolvedActions<T> | TRejected<TPendingActions<T>> | TResolvedActions<T> | GetRejectedAction<TPendingActions<T>>

View File

@ -1,4 +1,4 @@
import {PendingAction} from './IPendingAction' import {PendingAction} from './PendingAction'
export function createPendingAction<T, ActionType extends string>( export function createPendingAction<T, ActionType extends string>(
payload: Promise<T>, payload: Promise<T>,