Skip to content

Commit

Permalink
feat(@kuai/io): support params
Browse files Browse the repository at this point in the history
  • Loading branch information
PainterPuppets committed Feb 2, 2023
1 parent 79024b7 commit 05cc1ff
Show file tree
Hide file tree
Showing 8 changed files with 1,756 additions and 333 deletions.
1,912 changes: 1,626 additions & 286 deletions package-lock.json

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions packages/io/__tests__/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { CoR } from '../src/cor'
import { KuaiRouter } from '../src/router'
import { KoaRouterAdapter } from '../src/adapter'
import Koa from 'koa'
import { koaBody } from 'koa-body'

describe('test KoaRouterAdapter', () => {
const koaServer = new Koa()
koaServer.use(koaBody())

const cor = new CoR()
const kuaiRouter = new KuaiRouter()
Expand All @@ -21,6 +23,14 @@ describe('test KoaRouterAdapter', () => {
ctx.ok('hello children')
})

kuaiRouter.get('/test-query', async (ctx) => {
ctx.ok(`queries: ${JSON.stringify(ctx.payload.query)}`)
})

kuaiRouter.post('/test-body', async (ctx) => {
ctx.ok(`body: ${JSON.stringify(ctx.payload.body)}`)
})

cor.use(kuaiRouter.middleware())

const koaRouterAdapter = new KoaRouterAdapter(cor)
Expand Down Expand Up @@ -53,4 +63,23 @@ describe('test KoaRouterAdapter', () => {
const body = await res.text()
expect(body).toEqual('hello children')
})

it(`support query`, async () => {
const res = await fetch('http://localhost:4004/test-query?fieldA=a&filedB=b', { method: 'GET' })
expect(res.status).toEqual(200)
const body = await res.text()
expect(body).toEqual('queries: {"fieldA":"a","filedB":"b"}')
})

it(`support body`, async () => {
const res = await fetch('http://localhost:4004/test-body', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fieldA: 'a', filedB: 'b' }),
})

expect(res.status).toEqual(200)
const body = await res.text()
expect(body).toEqual('body: {"fieldA":"a","filedB":"b"}')
})
})
14 changes: 14 additions & 0 deletions packages/io/__tests__/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,18 @@ describe('test KuaiRouter', () => {
const childrenResult = await cor.dispatch({ method: 'GET', path: '/parent/children' })
expect(childrenResult).toMatch('hello children')
})

it(`support params`, async () => {
const cor = new CoR()
const kuaiRouter = new KuaiRouter()

kuaiRouter.get('/:username', async (ctx) => {
ctx.ok(`hello ${ctx.payload.params?.username}`)
})

cor.use(kuaiRouter.middleware())

const result = await cor.dispatch({ method: 'GET', path: '/alice' })
expect(result).toMatch('hello alice')
})
})
2 changes: 2 additions & 0 deletions packages/io/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
},
"dependencies": {
"@ckb-lumos/rpc": "0.19.0",
"koa-body": "6.0.1",
"koa-compose": "4.1.0",
"koa-router": "12.0.0",
"path-to-regexp": "6.2.1",
"rxjs": "7.8.0"
},
"devDependencies": {
Expand Down
15 changes: 11 additions & 4 deletions packages/io/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import KoaRouter from 'koa-router'
import * as Koa from 'koa'
import { CoR } from './cor'
import type { JsonValue } from './types'

declare module 'koa' {
interface Request extends Koa.BaseRequest {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: any
}
}

export class KoaRouterAdapter extends KoaRouter {
constructor(private readonly cor: CoR) {
Expand All @@ -15,10 +24,8 @@ export class KoaRouterAdapter extends KoaRouter {
ctx.body = await this.cor.dispatch({
method: ctx.method,
path: ctx.path,
params: ctx.params,
// todo: support body & query
// query: ctx.query,
// body: ctx.request.body,
query: ctx.query as JsonValue,
body: ctx.request.body,
})
await next()
}
Expand Down
14 changes: 7 additions & 7 deletions packages/io/src/cor.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { CoR as ICoR, Middleware, Context, JsonValue } from './types'
import compose from 'koa-compose'

export class CoR implements ICoR {
private _middlewares: Middleware[] = []
export class CoR<ContextT extends object = Record<string, never>> implements ICoR<ContextT> {
private _middlewares: Middleware<ContextT>[] = []

public use(plugin: Middleware): CoR {
this._middlewares = [...this._middlewares, plugin]
public use<NewContext = unknown>(plugin: Middleware<NewContext & ContextT>): CoR<NewContext & ContextT> {
this._middlewares = [...this._middlewares, plugin] as Middleware<ContextT>[]
return this
}

public async dispatch<Payload extends JsonValue, Ok>(payload: Payload): Promise<Ok | void> {
public async dispatch<Ok>(payload: JsonValue): Promise<Ok | void> {
return new Promise((resolve, rej) => {
const ctx: Context = {
const ctx: Context<ContextT> = {
payload,
ok: (ok?: Ok) => (ok !== undefined ? resolve(ok) : resolve()),
err: rej,
}
} as Context<ContextT>

compose(this._middlewares)(ctx)
})
Expand Down
61 changes: 41 additions & 20 deletions packages/io/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Middleware, Context, Route, Path, RoutePayload } from './types'
import { Middleware, Route, Path, RouterContext, RoutePayload, Method } from './types'
import type { Key } from 'path-to-regexp'
import { pathToRegexp } from 'path-to-regexp'

function isRoutePayload(x: unknown): x is RoutePayload<unknown, unknown> {
function isRoutePayload(x: unknown): x is RoutePayload {
return typeof x === 'object' && x !== null && 'path' in x && 'method' in x
}

Expand All @@ -9,50 +11,63 @@ export function isRoute(x: unknown): x is Route {
}

function matchPath(path: Path, route: Route): boolean {
// todo: add more match rule
return path === route.path
return route.regexp.test(path)
}

function createRoute(options: { path: Path; middleware: Middleware<RouterContext>; method: Method }): Route {
const { path, middleware, method } = options
const keys: Key[] = []
const regexp = pathToRegexp(path, keys)

return {
path,
method,
middleware,
regexp,
paramKeys: keys,
}
}

export class KuaiRouter {
private routes: Route[] = []

public get(path: Path, middleware: Middleware): this {
this.routes.push({ path, method: 'GET', middleware })
public get(path: Path, middleware: Middleware<RouterContext>): this {
this.routes.push(createRoute({ path, method: 'GET', middleware }))
return this
}

public post(path: Path, middleware: Middleware): this {
this.routes.push({ path, method: 'POST', middleware })
public post(path: Path, middleware: Middleware<RouterContext>): this {
this.routes.push(createRoute({ path, method: 'POST', middleware }))
return this
}

public put(path: Path, middleware: Middleware): this {
this.routes.push({ path, method: 'PUT', middleware })
public put(path: Path, middleware: Middleware<RouterContext>): this {
this.routes.push(createRoute({ path, method: 'PUT', middleware }))
return this
}

public delete(path: Path, middleware: Middleware): this {
this.routes.push({ path, method: 'DELETE', middleware })
public delete(path: Path, middleware: Middleware<RouterContext>): this {
this.routes.push(createRoute({ path, method: 'DELETE', middleware }))
return this
}

public patch(path: Path, middleware: Middleware): this {
this.routes.push({ path, method: 'PATCH', middleware })
public patch(path: Path, middleware: Middleware<RouterContext>): this {
this.routes.push(createRoute({ path, method: 'PATCH', middleware }))
return this
}

public head(path: Path, middleware: Middleware): this {
this.routes.push({ path, method: 'HEAD', middleware })
public head(path: Path, middleware: Middleware<RouterContext>): this {
this.routes.push(createRoute({ path, method: 'HEAD', middleware }))
return this
}

public options(path: Path, middleware: Middleware): this {
this.routes.push({ path, method: 'OPTIONS', middleware })
public options(path: Path, middleware: Middleware<RouterContext>): this {
this.routes.push(createRoute({ path, method: 'OPTIONS', middleware }))
return this
}

public middleware(): Middleware {
return async (ctx: Context, next: () => Promise<void>) => {
public middleware(): Middleware<RouterContext> {
return async (ctx, next) => {
const payload = ctx.payload

if (!isRoutePayload(payload)) {
Expand All @@ -61,6 +76,12 @@ export class KuaiRouter {

const route = this.routes.find((route) => route.method === payload.method && matchPath(payload.path, route))
if (route) {
const result = route.regexp.exec(ctx.payload.path)

if (result) {
ctx.payload.params = route.paramKeys.reduce((a, b, index) => ({ ...a, [b.name]: result[index + 1] }), {})
}

return route.middleware(ctx, next)
} else {
return next()
Expand Down
42 changes: 26 additions & 16 deletions packages/io/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import type { CKBComponents } from '@ckb-lumos/rpc/lib/types/api'

import type { Key } from 'path-to-regexp'
export interface Listener<T> {
on(listen: (obj: T) => void): void
}

export type Middleware = (ctx: Context, next: () => Promise<void>) => Promise<void>
export type JsonValue = null | number | string | { [key: string]: JsonValue } | JsonValue[]

export interface Context {
payload: JsonValue
type Next = () => Awaited<void>

ok(): void
export type DefaultContext = object

export type Context<CustomT extends DefaultContext = DefaultContext> = {
payload: JsonValue
ok(): void
ok<OK>(x: OK): void

err(): void

err<Err>(x: Err): void
}
} & CustomT

export type JsonValue = null | number | string | { [key: string]: JsonValue } | JsonValue[]
export type Middleware<CustomT extends DefaultContext = DefaultContext> = (
context: Context<CustomT>,
next: Next,
) => ReturnType<Next>

// `CoR` (Chain of Responsibility), abbr for chain of responsibility
// a module that strings middleware together in order
export interface CoR {
use(plugin: Middleware): void
export interface CoR<ContextT extends DefaultContext = Record<string, never>> {
use<NewContextT = unknown>(plugin: Middleware<NewContextT & ContextT>): CoR<NewContextT & ContextT>

dispatch<Payload extends JsonValue, Ok>(payload: Payload): Promise<Ok | void>
dispatch<Ok>(payload: JsonValue): Promise<Ok | void>
}

export interface Listener<T> {
Expand All @@ -42,15 +45,22 @@ export interface ChainSource {
export type Path = string
export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'

export type RoutePayload<Body, Params> = {
body: Body
params: Params
export type RoutePayload<Query = Record<string, string>, Params = Record<string, string>, Body = unknown> = {
query?: Query
body?: Body
params?: Params
path: Path
method: Method
}

export interface RouterContext {
payload: RoutePayload
}

export interface Route {
path: Path
method: Method
middleware: Middleware
middleware: Middleware<RouterContext>
paramKeys: Key[]
regexp: RegExp
}

0 comments on commit 05cc1ff

Please sign in to comment.