Skip to content

Commit

Permalink
Added context and user parameter binding. Closes plumier#25
Browse files Browse the repository at this point in the history
  • Loading branch information
ktutnik committed Aug 4, 2018
1 parent a1d8df9 commit e8eb024
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 88 deletions.
43 changes: 43 additions & 0 deletions .docs/parameter-binding.md
Expand Up @@ -174,6 +174,36 @@ Result:
}]
```

### Ctx Binding
Bind Koa context to action's parameter

```typescript
export class AnimalController {
@route.post()
save(@bind.ctx() model:Koa.Context){

}
}
```

Part of context can be issued by providing path

```typescript
export class AnimalController {
@route.post()
save(@bind.ctx("request.body") model:any){

}
}
```

Above code will be the same as access `ctx.request.body`.

Allowed path example:
* Using dot to access child property `request.headers.ip` etc
* Using array notation `request.body[0]`


### Request Binding
Bind request to action's parameter

Expand Down Expand Up @@ -288,6 +318,19 @@ class AnimalController {
}
```

## Login User Binding

Bind login user to action's parameter. This functionalities require `JwtAuthFacility` see how to setup user authorization [here](./authorization.md)

```typescript
export class AnimalController {
@route.get()
save(@bind.user() user:User){

}
}
```

## Custom Error Message
By default if value conversion failed Plumier will throw `ConversionError` with status 400 with message `Unable to convert "<value>" into <Type> in parameter <parameter path>`

Expand Down
35 changes: 35 additions & 0 deletions appveyor.yml
@@ -0,0 +1,35 @@
# AppVeyor file
# http://www.appveyor.com/docs/appveyor-yml

# Build version format
version: "{build}"

# Test against this version of Node.js
environment:
NODE_ENV: test

matrix:
- nodejs_version: "stable"
- nodejs_version: "8"

build: off

branches:
only:
- master

install:
- ps: Install-Product node $env:nodejs_version
- npm install -g npm
- npm config set progress=false
- npm install

# Fix line endings on Windows
init:
- git config --global core.autocrlf true

test_script:
# Output useful info for debugging.
- node --version && npm --version
- ps: "npm --version # PowerShell" # Pass comment to PS for easier debugging
- cmd: npm run test
11 changes: 9 additions & 2 deletions packages/core/src/common.ts
Expand Up @@ -12,15 +12,15 @@ declare global {
}

interface Array<T> {
flatten():T
flatten(): T
}
}

String.prototype.format = function (this: string, ...args: any[]) {
return this.replace(/{(\d+)}/g, (m, i) => typeof args[i] != 'undefined' ? args[i] : m)
}

Array.prototype.flatten = function<T>(this:Array<T>){
Array.prototype.flatten = function <T>(this: Array<T>) {
return this.reduce((a, b) => a.concat(b), <T[]>[])
}

Expand Down Expand Up @@ -82,4 +82,11 @@ export namespace consoleLog {
export function clearMock() {
console.log = log
}
}

export function getChildValue(object: any, path: string, defaultValue?: any) {
return path
.split(/[\.\[\]\'\"]/)
.filter(p => p)
.reduce((o, p) => o ? o[p] : defaultValue, object)
}
57 changes: 31 additions & 26 deletions packages/core/src/core.ts
Expand Up @@ -34,8 +34,7 @@ export interface ParameterPropertiesType<T> {

export interface BindingDecorator {
type: "ParameterBinding",
name: "Request" | "Body" | "Header" | "Query",
part?: RequestPart
part?: string
}

export interface RouteDecorator { name: "Route", method: HttpMethod, url?: string }
Expand Down Expand Up @@ -236,26 +235,7 @@ export namespace MiddlewareUtil {
return ActionResult.fromContext(x.context)
}
}
}/*
export function toKoa(middleware: Middleware): KoaMiddleware {
return async (context: Context, next: () => Promise<any>) => {
try {
const result = await middleware.execute({
context, proceed: async () => {
await next()
return ActionResult.fromContext(context)
}
})
result.execute(context)
}
catch (e) {
if (e instanceof HttpStatusError)
context.throw(e.status, e)
else
context.throw(500, e)
}
}
}*/
}
}


Expand Down Expand Up @@ -321,6 +301,23 @@ export class DefaultDependencyResolver implements DependencyResolver {
/* ------------------------------------------------------------------------------- */

export namespace bind {
/**
* Bind Koa Context
*
* method(@bind.ctx() ctx:any) {}
*
* Use dot separated string to access child property
*
* method(@bind.ctx("state.user") ctx:User) {}
* method(@bind.ctx("request.headers.ip") ip:string) {}
* method(@bind.ctx("body[0].id") id:string) {}
*
* @param part part of context, use dot separator to access child property
*/
export function ctx(part?:string){
return decorateParameter(<BindingDecorator>{ type: "ParameterBinding", name: "Context", part })
}

/**
* Bind Koa request to parameter
*
Expand All @@ -334,7 +331,7 @@ export namespace bind {
* @param part part of request ex: body, method, query etc
*/
export function request(part?: RequestPart) {
return decorateParameter(<BindingDecorator>{ type: "ParameterBinding", name: "Request", part })
return ctx(["request", part].join("."))
}

/**
Expand All @@ -348,7 +345,7 @@ export namespace bind {
* method(@bind.body("age") age:number){}
*/
export function body(part?: string) {
return decorateParameter(<BindingDecorator>{ type: "ParameterBinding", name: "Body", part })
return ctx(["request", "body", part].join("."))
}

/**
Expand All @@ -362,7 +359,7 @@ export namespace bind {
* method(@bind.header("cookie") age:any){}
*/
export function header(key?: HeaderPart) {
return decorateParameter(<BindingDecorator>{ type: "ParameterBinding", name: "Header", part: key })
return ctx(["request", "headers", key].join("."))
}

/**
Expand All @@ -376,9 +373,17 @@ export namespace bind {
* method(@bind.query("type") type:string){}
*/
export function query(name?: string) {
return decorateParameter(<BindingDecorator>{ type: "ParameterBinding", name: "Query", part: name })
return ctx(["request", "query", name].join("."))
}

/**
* Bind current login user to parameter
*
* method(@bind.user() user:User){}
*/
export function user() {
return ctx("state.user")
}
}

export class RouteDecoratorImpl {
Expand Down
31 changes: 29 additions & 2 deletions packages/core/test/helper.spec.ts
@@ -1,5 +1,5 @@
import "@plumjs/core"
import { resolvePath } from '@plumjs/core';
import { resolvePath, getChildValue } from '@plumjs/core';
import { join } from 'path';
import { unlinkSync, existsSync } from 'fs';

Expand Down Expand Up @@ -34,7 +34,7 @@ describe("resolvePath", () => {

it("Should resolve file if extension not specified", () => {
const jsFile = join(__dirname, "./no-js/no-js.js")
if(existsSync(jsFile)) unlinkSync(jsFile)
if (existsSync(jsFile)) unlinkSync(jsFile)
const result = resolvePath(join(__dirname, "./no-js/no-js"))
expect(result[0]).toBe(join(__dirname, "./no-js/no-js"))
})
Expand All @@ -44,4 +44,31 @@ describe("resolvePath", () => {
expect(result[0]).toBe(join(__dirname, "./resolve-path/my-module.ts"))
})

})

describe("getChildProperty", () => {
it("Should able to get nested child property value", () => {
const result = getChildValue({ a: { b: { c: "Hello" } } }, "a.b")
expect(result).toEqual({c: "Hello"})
})
it("Should able to get nested child property value", () => {
const result = getChildValue({ a: { b: { c: "Hello" } } }, "a.b.c")
expect(result).toBe("Hello")
})
it("Should able to access array", () => {
const result = getChildValue({ a: [true, "Hello", 2] }, "a[1]")
expect(result).toBe("Hello")
})
it("Should work with falsy value", () => {
const result = getChildValue({ a: [false] }, "a[0]")
expect(result).toBe(false)
})
it("Should able to access array", () => {
const result = getChildValue([1, 2, 3], "[0]")
expect(result).toBe(1)
})
it("Should return undefined if property not match", () => {
const result = getChildValue({ a: { b: { c: "Hello" } } }, "a.d.e.f.g[0]")
expect(result).toBeUndefined()
})
})
4 changes: 2 additions & 2 deletions packages/plumier/src/application.ts
Expand Up @@ -60,10 +60,10 @@ export class MiddlewareInvocation implements Invocation {
export class ActionInvocation implements Invocation {
constructor(public context: Context) { }
async proceed(): Promise<ActionResult> {
const { request, route, config } = this.context
const { route, config } = this.context
const controller: any = config.dependencyResolver.resolve(route.controller.object)
//bind parameters
const parameters = bindParameter(request, route.action, config.converters)
const parameters = bindParameter(this.context, route.action, config.converters)
//check validation
if (config.validator) {
const param = (i: number) => route.action.parameters[i]
Expand Down
41 changes: 16 additions & 25 deletions packages/plumier/src/binder.ts
Expand Up @@ -8,9 +8,10 @@ import {
ParameterPropertiesType,
TypeConverter,
ValueConverter,
getChildValue,
} from "@plumjs/core";
import { FunctionReflection, ParameterReflection, reflect } from "@plumjs/reflect";
import { Request } from "koa";
import { Context } from "koa";


function createConversionError(value: any, prop: ParameterProperties) {
Expand Down Expand Up @@ -147,47 +148,37 @@ export function convert(value: any, prop: ParameterProperties) {
/* ------------------------------------------------------------------------------- */


function bindModel(action: FunctionReflection, request: Request, par: ParameterReflection, converter: (value: any) => any): any {
function bindModel(action: FunctionReflection, ctx: Context, par: ParameterReflection, converter: (value: any) => any): any {
if (!par.typeAnnotation) return
if (!isCustomClass(par.typeAnnotation)) return
return converter(request.body)
return converter(ctx.request.body)
}

function bindDecorator(action: FunctionReflection, request: Request, par: ParameterReflection, converter: (value: any) => any): any {
function bindDecorator(action: FunctionReflection, ctx: Context, par: ParameterReflection, converter: (value: any) => any): any {
const decorator: BindingDecorator = par.decorators.find((x: BindingDecorator) => x.type == "ParameterBinding")
if (!decorator) return
const getDataOrPart = (data: any) => decorator.part ? data && data[decorator.part] : data
switch (decorator.name) {
case "Body":
return converter(getDataOrPart(request.body))
case "Query":
return converter(getDataOrPart(request.query))
case "Header":
return converter(getDataOrPart(request.headers))
case "Request":
return converter(getDataOrPart(request))
}
return converter(decorator.part ? ctx && getChildValue(ctx, decorator.part) : ctx)
}

function bindArrayDecorator(action: FunctionReflection, request: Request, par: ParameterReflection, converter: (value: any, type: Class | Class[]) => any): any {
if (!Array.isArray(par.typeAnnotation)) return
return converter(request.body, par.typeAnnotation)
function bindArrayDecorator(action: FunctionReflection, ctx: Context, par: ParameterReflection, converter: (value: any, type: Class | Class[]) => any): any {
if (!Array.isArray(par.typeAnnotation)) return
return converter(ctx.request.body, par.typeAnnotation)
}

function bindRegular(action: FunctionReflection, request: Request, par: ParameterReflection, converter: (value: any) => any): any {
return converter(request.query[par.name.toLowerCase()])
function bindRegular(action: FunctionReflection, ctx: Context, par: ParameterReflection, converter: (value: any) => any): any {
return converter(ctx.request.query[par.name.toLowerCase()])
}

export function bindParameter(request: Request, action: FunctionReflection, converter?: TypeConverter[]) {
export function bindParameter(ctx: Context, action: FunctionReflection, converter?: TypeConverter[]) {
const mergedConverters = flattenConverters(DefaultConverterList.concat(converter || []))
return action.parameters.map(((x, i) => {
const converter = (result: any, type?: Class | Class[]) => convert(result, {
path: [x.name], parameterType: type || x.typeAnnotation,
converters: mergedConverters, decorators: x.decorators
});
return bindArrayDecorator(action, request, x, converter) ||
bindDecorator(action, request, x, converter) ||
bindModel(action, request, x, converter) ||
bindRegular(action, request, x, converter)
return bindArrayDecorator(action, ctx, x, converter) ||
bindDecorator(action, ctx, x, converter) ||
bindModel(action, ctx, x, converter) ||
bindRegular(action, ctx, x, converter)
}))
}

0 comments on commit e8eb024

Please sign in to comment.