Comparing version 0.1.0-alpha.d43b984e to 1.0.0-alpha.5
@@ -0,3 +1,23 @@ | ||
/// <reference types="koa__cors" /> | ||
import Cors from "@koa/cors"; | ||
import { ActionResult, Application, Class, Configuration, DefaultFacility, Facility, Invocation, KoaMiddleware, Middleware, PlumierApplication, PlumierConfiguration, RouteInfo, ValidatorFunction } from "@plumier/core"; | ||
import Koa, { Context } from "koa"; | ||
import { ActionResult, Application, Configuration, Facility, Invocation, KoaMiddleware, PlumierApplication, PlumierConfiguration, Middleware } from "./framework"; | ||
export interface RouteContext extends Koa.Context { | ||
route: Readonly<RouteInfo>; | ||
parameters: any[]; | ||
} | ||
export interface BodyParserOption { | ||
enableTypes?: string[]; | ||
encode?: string; | ||
formLimit?: string; | ||
jsonLimit?: string; | ||
strict?: boolean; | ||
detectJSON?: (ctx: Koa.Context) => boolean; | ||
extendTypes?: { | ||
json?: string[]; | ||
form?: string[]; | ||
text?: string[]; | ||
}; | ||
onerror?: (err: Error, ctx: Koa.Context) => void; | ||
} | ||
export declare class MiddlewareInvocation implements Invocation { | ||
@@ -11,10 +31,42 @@ private middleware; | ||
export declare class ActionInvocation implements Invocation { | ||
context: Context; | ||
constructor(context: Context); | ||
context: RouteContext; | ||
constructor(context: RouteContext); | ||
proceed(): Promise<ActionResult>; | ||
} | ||
export declare function pipe(middleware: Middleware[], context: Context, invocation: Invocation): Invocation; | ||
/** | ||
* Preset configuration for building web api. This facility contains: | ||
* | ||
* body parser: koa-bodyparser | ||
* | ||
* cors: @koa/cors | ||
*/ | ||
export declare class WebApiFacility extends DefaultFacility { | ||
private opt?; | ||
constructor(opt?: { | ||
controller?: string | Class | Class[] | undefined; | ||
bodyParser?: BodyParserOption | undefined; | ||
cors?: Cors.Options | undefined; | ||
validators?: { | ||
[key: string]: ValidatorFunction; | ||
} | undefined; | ||
} | undefined); | ||
setup(app: Readonly<PlumierApplication>): void; | ||
} | ||
/** | ||
* Preset configuration for building restful style api. This facility contains: | ||
* | ||
* body parser: koa-bodyparser | ||
* | ||
* cors: @koa/cors | ||
* | ||
* default response status: { get: 200, post: 201, put: 204, delete: 204 } | ||
*/ | ||
export declare class RestfulApiFacility extends WebApiFacility { | ||
setup(app: Readonly<PlumierApplication>): void; | ||
} | ||
export declare class Plumier implements PlumierApplication { | ||
readonly config: Readonly<PlumierConfiguration>; | ||
readonly koa: Koa; | ||
private globalMiddleware; | ||
constructor(); | ||
@@ -25,3 +77,4 @@ use(option: KoaMiddleware): Application; | ||
set(config: Partial<Configuration>): Application; | ||
private createRoutes; | ||
initialize(): Promise<Koa>; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tslib_1 = require("tslib"); | ||
const debug_1 = tslib_1.__importDefault(require("debug")); | ||
const cors_1 = tslib_1.__importDefault(require("@koa/cors")); | ||
const core_1 = require("@plumier/core"); | ||
const validator_1 = require("@plumier/validator"); | ||
const fs_1 = require("fs"); | ||
const koa_1 = tslib_1.__importDefault(require("koa")); | ||
const koa_bodyparser_1 = tslib_1.__importDefault(require("koa-bodyparser")); | ||
const path_1 = require("path"); | ||
const util_1 = require("util"); | ||
const binder_1 = require("./binder"); | ||
const framework_1 = require("./framework"); | ||
const router_1 = require("./router"); | ||
const log = debug_1.default("plum:app"); | ||
/* ------------------------------------------------------------------------------- */ | ||
@@ -27,2 +26,10 @@ /* ------------------------------- INVOCATIONS ----------------------------------- */ | ||
exports.MiddlewareInvocation = MiddlewareInvocation; | ||
class NotFoundActionInvocation { | ||
constructor(context) { | ||
this.context = context; | ||
} | ||
proceed() { | ||
throw new core_1.HttpStatusError(404); | ||
} | ||
} | ||
class ActionInvocation { | ||
@@ -33,16 +40,23 @@ constructor(context) { | ||
async proceed() { | ||
const { request, route, config } = this.context; | ||
const controller = config.dependencyResolver.resolve(route.controller.object); | ||
const parameters = binder_1.bindParameter(request, route.action, config.converters); | ||
const result = controller[route.action.name].apply(controller, parameters); | ||
const { route, config } = this.context; | ||
const controller = config.dependencyResolver.resolve(route.controller.type); | ||
//check validation | ||
if (config.validator) { | ||
const param = (i) => route.action.parameters[i]; | ||
const validate = (value, i) => config.validator(value, param(i), this.context, this.context.config.validators); | ||
const result = await Promise.all(this.context.parameters.map((value, index) => validate(value, index))); | ||
const issues = result.flatten(); | ||
if (issues.length > 0) | ||
throw new core_1.ValidationError(issues); | ||
} | ||
const result = controller[route.action.name].apply(controller, this.context.parameters); | ||
const awaitedResult = await Promise.resolve(result); | ||
const status = config.responseStatus && config.responseStatus[route.method] || 200; | ||
if (result instanceof framework_1.ActionResult) { | ||
result.status = result.status || status; | ||
log(`[Action Invocation] Method: ${framework_1.b(route.method)} Status config: ${framework_1.b(util_1.inspect(config.responseStatus, false, null))} Status: ${framework_1.b(result.status)} `); | ||
return Promise.resolve(result); | ||
//if instance of action result, return immediately | ||
if (awaitedResult && awaitedResult.execute) { | ||
awaitedResult.status = awaitedResult.status || status; | ||
return awaitedResult; | ||
} | ||
else { | ||
const awaitedResult = await Promise.resolve(result); | ||
log(`[Action Invocation] Method: ${route.method} Status config: ${framework_1.b(util_1.inspect(config.responseStatus, false, null))} Status: ${framework_1.b(status)} `); | ||
return new framework_1.ActionResult(awaitedResult, status); | ||
return new core_1.ActionResult(awaitedResult, status); | ||
} | ||
@@ -52,5 +66,2 @@ } | ||
exports.ActionInvocation = ActionInvocation; | ||
/* ------------------------------------------------------------------------------- */ | ||
/* ------------------------- MIDDLEWARE PIPELINE --------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function pipe(middleware, context, invocation) { | ||
@@ -61,14 +72,64 @@ return middleware.reverse().reduce((prev, cur) => new MiddlewareInvocation(cur, context, prev), invocation); | ||
/* ------------------------------------------------------------------------------- */ | ||
/* --------------------------- REQUEST HANDLER ----------------------------------- */ | ||
/* -------------------------------- FACILITIES ----------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
async function requestHandler(ctx) { | ||
const controllerMiddleware = router_1.extractDecorators(ctx.route); | ||
const pipeline = pipe(controllerMiddleware, ctx, new ActionInvocation(ctx)); | ||
const result = await pipeline.proceed(); | ||
result.execute(ctx); | ||
log(`[Request Handler] ${framework_1.b(ctx.path)} -> ${framework_1.b(ctx.route.controller.name)}.${framework_1.b(ctx.route.action.name)}`); | ||
log(`[Request Handler] Request Query: ${framework_1.b(util_1.inspect(ctx.query, false, null))}`); | ||
log(`[Request Handler] Request Header: ${framework_1.b(util_1.inspect(ctx.headers, false, null))}`); | ||
log(`[Request Handler] Request Body: ${framework_1.b(util_1.inspect(result.body, false, null))}`); | ||
/** | ||
* Preset configuration for building web api. This facility contains: | ||
* | ||
* body parser: koa-bodyparser | ||
* | ||
* cors: @koa/cors | ||
*/ | ||
class WebApiFacility extends core_1.DefaultFacility { | ||
constructor(opt) { | ||
super(); | ||
this.opt = opt; | ||
} | ||
setup(app) { | ||
app.koa.use(async (ctx, next) => { | ||
try { | ||
await next(); | ||
} | ||
catch (e) { | ||
if (e instanceof core_1.ValidationError) { | ||
ctx.body = e.issues; | ||
ctx.status = e.status; | ||
} | ||
else if (e instanceof core_1.ConversionError) { | ||
ctx.body = [e.issues]; | ||
ctx.status = e.status; | ||
} | ||
else if (e instanceof core_1.HttpStatusError) | ||
ctx.throw(e.status, e); | ||
else | ||
ctx.throw(500, e); | ||
} | ||
}); | ||
app.koa.use(koa_bodyparser_1.default(this.opt && this.opt.bodyParser)); | ||
app.koa.use(cors_1.default(this.opt && this.opt.cors)); | ||
if (this.opt && this.opt.controller) | ||
app.set({ controller: this.opt.controller }); | ||
if (this.opt && this.opt.validators) | ||
app.set({ validators: this.opt.validators }); | ||
app.set({ | ||
validator: (value, meta, ctx, validators) => validator_1.validate(value, meta.decorators, [meta.name], ctx, validators) | ||
}); | ||
} | ||
} | ||
exports.WebApiFacility = WebApiFacility; | ||
/** | ||
* Preset configuration for building restful style api. This facility contains: | ||
* | ||
* body parser: koa-bodyparser | ||
* | ||
* cors: @koa/cors | ||
* | ||
* default response status: { get: 200, post: 201, put: 204, delete: 204 } | ||
*/ | ||
class RestfulApiFacility extends WebApiFacility { | ||
setup(app) { | ||
super.setup(app); | ||
app.set({ responseStatus: { post: 201, put: 204, delete: 204 } }); | ||
} | ||
} | ||
exports.RestfulApiFacility = RestfulApiFacility; | ||
/* ------------------------------------------------------------------------------- */ | ||
@@ -79,17 +140,12 @@ /* --------------------------- MAIN APPLICATION ---------------------------------- */ | ||
constructor() { | ||
this.globalMiddleware = []; | ||
this.koa = new koa_1.default(); | ||
this.config = { | ||
mode: "debug", | ||
middleware: [], | ||
facilities: [], | ||
controller: path_1.join(process.cwd(), "./controller"), | ||
dependencyResolver: new framework_1.DefaultDependencyResolver() | ||
}; | ||
this.config = Object.assign({}, core_1.DefaultConfiguration, { middleware: [], facilities: [] }); | ||
} | ||
use(option) { | ||
if (typeof option === "function") { | ||
this.koa.use(option); | ||
this.globalMiddleware.push(core_1.MiddlewareUtil.fromKoa(option)); | ||
} | ||
else { | ||
this.koa.use(framework_1.MiddlewareUtil.toKoa(option)); | ||
this.globalMiddleware.push(option); | ||
} | ||
@@ -99,4 +155,6 @@ return this; | ||
set(config) { | ||
if (framework_1.hasKeyOf(config, "setup")) | ||
if (core_1.hasKeyOf(config, "setup")) { | ||
config.setup(this); | ||
this.config.facilities.push(config); | ||
} | ||
else | ||
@@ -106,18 +164,44 @@ Object.assign(this.config, config); | ||
} | ||
createRoutes(executionPath) { | ||
let routes = []; | ||
if (typeof this.config.controller === "string") { | ||
const path = path_1.isAbsolute(this.config.controller) ? this.config.controller : | ||
path_1.join(executionPath, this.config.controller); | ||
if (!fs_1.existsSync(path)) | ||
throw new Error(core_1.errorMessage.ControllerPathNotFound.format(path)); | ||
routes = core_1.routeGenerator.transformModule(path); | ||
} | ||
else if (Array.isArray(this.config.controller)) { | ||
routes = this.config.controller.map(x => core_1.routeGenerator.transformController(x)) | ||
.flatten(); | ||
} | ||
else { | ||
routes = core_1.routeGenerator.transformController(this.config.controller); | ||
} | ||
return routes; | ||
} | ||
async initialize() { | ||
try { | ||
let routes = []; | ||
if (typeof this.config.controller === "string") { | ||
if (!fs_1.existsSync(this.config.controller)) | ||
throw new Error(framework_1.StringUtil.format(framework_1.errorMessage.ControllerPathNotFound, this.config.controller)); | ||
routes = await router_1.transformModule(this.config.controller); | ||
if (process.env["NODE_ENV"] === "production") | ||
Object.assign(this.config, { mode: "production" }); | ||
//get file location of script who initialized the application to calculate the controller path | ||
//module.parent.parent.filename -> because Plumier app also exported in plumier/src/index.ts | ||
let routes = this.createRoutes(path_1.dirname(module.parent.parent.filename)); | ||
for (const facility of this.config.facilities) { | ||
await facility.initialize(this, routes); | ||
} | ||
else { | ||
routes = this.config.controller.map(x => router_1.transformController(x)) | ||
.reduce((a, b) => a.concat(b), []); | ||
} | ||
if (this.config.mode === "debug") | ||
router_1.printAnalysis(router_1.analyzeRoutes(routes)); | ||
await Promise.all(this.config.facilities.map(x => x.setup(this))); | ||
this.koa.use(router_1.router(routes, this.config, requestHandler)); | ||
core_1.routeGenerator.printAnalysis(core_1.routeGenerator.analyzeRoutes(routes)); | ||
const decorators = {}; | ||
this.koa.use(router_1.router(routes, this.config, ctx => { | ||
if (ctx.route && ctx.parameters) { | ||
//execute global middleware and controller | ||
const middleware = decorators[ctx.route.url] || (decorators[ctx.route.url] = this.globalMiddleware.concat(core_1.middleware.extractDecorators(ctx.route))); | ||
return pipe(middleware, ctx, new ActionInvocation(ctx)); | ||
} | ||
else { | ||
//execute global middleware only | ||
return pipe(this.globalMiddleware.slice(0), ctx, new NotFoundActionInvocation(ctx)); | ||
} | ||
})); | ||
return this.koa; | ||
@@ -124,0 +208,0 @@ } |
@@ -1,2 +0,5 @@ | ||
export { ActionResult, Application, bind, Configuration, DependencyResolver, Facility, HeaderPart, HttpMethod, HttpStatusError, Invocation, KoaMiddleware, middleware, Middleware, PlumierApplication, PlumierConfiguration, RequestPart, route, RouteInfo, TypeConverter, WebApiFacility } from "./framework"; | ||
export { Plumier } from "./application"; | ||
export { authorize, ActionResult, Application, bind, Configuration, Class, DependencyResolver, Facility, FileUploadInfo, FileParser, HeaderPart, HttpMethod, HttpStatusError, Invocation, KoaMiddleware, middleware, Middleware, domain, PlumierApplication, PlumierConfiguration, RequestPart, route, RouteInfo, ConversionError, ValidationError, ValidationIssue, ValidatorStore, ValidatorFunction, ValidatorId, DefaultFacility, converter, binder, routeGenerator, security, response } from "@plumier/core"; | ||
export { WebApiFacility, RestfulApiFacility } from "./application"; | ||
export { val } from "@plumier/validator"; | ||
import { Plumier } from "./application"; | ||
export default Plumier; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var framework_1 = require("./framework"); | ||
exports.ActionResult = framework_1.ActionResult; | ||
exports.bind = framework_1.bind; | ||
exports.HttpStatusError = framework_1.HttpStatusError; | ||
exports.middleware = framework_1.middleware; | ||
exports.route = framework_1.route; | ||
exports.WebApiFacility = framework_1.WebApiFacility; | ||
var core_1 = require("@plumier/core"); | ||
exports.authorize = core_1.authorize; | ||
exports.ActionResult = core_1.ActionResult; | ||
exports.bind = core_1.bind; | ||
exports.HttpStatusError = core_1.HttpStatusError; | ||
exports.middleware = core_1.middleware; | ||
exports.domain = core_1.domain; | ||
exports.route = core_1.route; | ||
exports.ConversionError = core_1.ConversionError; | ||
exports.ValidationError = core_1.ValidationError; | ||
exports.ValidatorId = core_1.ValidatorId; | ||
exports.DefaultFacility = core_1.DefaultFacility; | ||
exports.converter = core_1.converter; | ||
exports.binder = core_1.binder; | ||
exports.routeGenerator = core_1.routeGenerator; | ||
exports.security = core_1.security; | ||
exports.response = core_1.response; | ||
var application_1 = require("./application"); | ||
exports.Plumier = application_1.Plumier; | ||
exports.WebApiFacility = application_1.WebApiFacility; | ||
exports.RestfulApiFacility = application_1.RestfulApiFacility; | ||
var validator_1 = require("@plumier/validator"); | ||
exports.val = validator_1.val; | ||
const application_2 = require("./application"); | ||
exports.default = application_2.Plumier; |
@@ -0,20 +1,3 @@ | ||
import { Configuration, Invocation, RouteInfo } from "@plumier/core"; | ||
import { Context } from "koa"; | ||
import { RouteInfo, Class, Configuration, Middleware } from "./framework"; | ||
import { ClassReflection } from "./libs/reflect"; | ||
interface Issue { | ||
type: "error" | "warning" | "success"; | ||
message?: string; | ||
} | ||
interface TestResult { | ||
route: RouteInfo; | ||
issues: Issue[]; | ||
} | ||
export declare function striveController(name: string): string; | ||
export declare function getControllerRoute(controller: ClassReflection): string; | ||
export declare function extractDecorators(route: RouteInfo): Middleware[]; | ||
export declare function transformController(object: ClassReflection | Class): RouteInfo[]; | ||
export declare function transformModule(path: string): Promise<RouteInfo[]>; | ||
export declare function router(infos: RouteInfo[], config: Configuration, handler: (ctx: Context) => Promise<void>): (ctx: Context, next: () => Promise<void>) => Promise<void>; | ||
export declare function analyzeRoutes(routes: RouteInfo[]): TestResult[]; | ||
export declare function printAnalysis(results: TestResult[]): void; | ||
export {}; | ||
export declare function router(infos: RouteInfo[], config: Configuration, handler: (ctx: Context) => Invocation): (ctx: Context, next: () => Promise<void>) => Promise<void>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tslib_1 = require("tslib"); | ||
const debug_1 = tslib_1.__importDefault(require("debug")); | ||
const Fs = tslib_1.__importStar(require("fs")); | ||
const Path = tslib_1.__importStar(require("path")); | ||
const core_1 = require("@plumier/core"); | ||
const path_to_regexp_1 = tslib_1.__importDefault(require("path-to-regexp")); | ||
const framework_1 = require("./framework"); | ||
const reflect_1 = require("./libs/reflect"); | ||
const util_1 = require("util"); | ||
const chalk_1 = tslib_1.__importDefault(require("chalk")); | ||
const log = debug_1.default("plum:router"); | ||
/* ------------------------------------------------------------------------------- */ | ||
/* ------------------------------- HELPERS --------------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function striveController(name) { | ||
return name.substring(0, name.lastIndexOf("Controller")).toLowerCase(); | ||
} | ||
exports.striveController = striveController; | ||
function getControllerRoute(controller) { | ||
const root = controller.decorators.find((x) => x.name == "Root"); | ||
return (root && root.url) || `/${striveController(controller.name)}`; | ||
} | ||
exports.getControllerRoute = getControllerRoute; | ||
function extractDecorators(route) { | ||
const classDecorator = route.controller.decorators.filter(x => x.name == "Middleware"); | ||
const methodDecorator = route.action.decorators.filter(x => x.name == "Middleware"); | ||
const extract = (d) => d.map(x => x.value).reduce((a, b) => a.concat(b), []); | ||
return extract(classDecorator) | ||
.concat(extract(methodDecorator)) | ||
.reverse(); | ||
} | ||
exports.extractDecorators = extractDecorators; | ||
function getActionName(route) { | ||
return `${route.controller.name}.${route.action.name}(${route.action.parameters.map(x => x.name).join(", ")})`; | ||
} | ||
function resolvePath(path) { | ||
//resolve provided path directory or file | ||
if (Fs.lstatSync(path).isDirectory()) | ||
return Fs.readdirSync(path) | ||
//take only *.js | ||
.filter(x => Path.extname(x) === ".js") | ||
//add root path + file name | ||
.map(x => Path.join(path, x)); | ||
else | ||
return [path]; | ||
} | ||
/* ------------------------------------------------------------------------------- */ | ||
/* ---------------------------------- TRANSFORMER -------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function transformRouteDecorator(controller, method) { | ||
if (method.decorators.some((x) => x.name == "Ignore")) | ||
return; | ||
const root = getControllerRoute(controller); | ||
const decorator = method.decorators.find((x) => x.name == "Route"); | ||
const result = { action: method, method: decorator.method, controller: controller }; | ||
//absolute route | ||
if (decorator.url && decorator.url.startsWith("/")) | ||
return Object.assign({}, result, { url: decorator.url }); | ||
//empty string | ||
else if (decorator.url === "") | ||
return Object.assign({}, result, { url: root }); | ||
//relative route | ||
else { | ||
const actionUrl = decorator.url || method.name.toLowerCase(); | ||
return Object.assign({}, result, { url: [root, actionUrl].join("/") }); | ||
} | ||
} | ||
function transformRegular(controller, method) { | ||
return { | ||
method: "get", | ||
url: `${getControllerRoute(controller)}/${method.name.toLowerCase()}`, | ||
action: method, | ||
controller: controller, | ||
}; | ||
} | ||
function transformController(object) { | ||
const controller = typeof object === "function" ? reflect_1.reflect(object) : object; | ||
if (!controller.name.toLowerCase().endsWith("controller")) | ||
return []; | ||
return controller.methods.map(method => { | ||
//first priority is decorator | ||
if (method.decorators.some((x) => x.name == "Ignore" || x.name == "Route")) | ||
return transformRouteDecorator(controller, method); | ||
else | ||
return transformRegular(controller, method); | ||
}) | ||
//ignore undefined | ||
.filter(x => Boolean(x)); | ||
} | ||
exports.transformController = transformController; | ||
async function transformModule(path) { | ||
//read all files and get module reflection | ||
const modules = await Promise.all(resolvePath(path) | ||
//reflect the file | ||
.map(x => reflect_1.reflect(x))); | ||
//get all module.members and combine into one array | ||
return modules.reduce((a, b) => a.concat(b.members), []) | ||
//take only the controller class | ||
.filter(x => x.type === "Class" && x.name.toLowerCase().endsWith("controller")) | ||
//traverse and change into route | ||
.map(x => transformController(x)) | ||
//flatten the result | ||
.reduce((a, b) => a.concat(b), []); | ||
} | ||
exports.transformModule = transformModule; | ||
/* ------------------------------------------------------------------------------- */ | ||
/* ------------------------------- ROUTER ---------------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function checkUrlMatch(route, ctx) { | ||
function toRegExp(route, path) { | ||
const keys = []; | ||
const regexp = path_to_regexp_1.default(route.url, keys); | ||
const match = regexp.exec(ctx.path); | ||
return { keys, match, method: route.method.toUpperCase(), route }; | ||
const match = regexp.exec(path); | ||
const query = !match ? {} : keys.reduce((a, b, i) => { | ||
a[b.name] = match[i + 1]; | ||
return a; | ||
}, {}); | ||
return { match, query, method: route.method.toUpperCase(), route }; | ||
} | ||
function router(infos, config, handler) { | ||
const matchers = {}; | ||
const getMatcher = (path, method) => infos.map(x => toRegExp(x, path)).find(x => Boolean(x.match) && x.method == method); | ||
return async (ctx, next) => { | ||
const match = infos.map(x => checkUrlMatch(x, ctx)) | ||
.find(x => Boolean(x.match) && x.method == ctx.method); | ||
const key = `${ctx.method}${ctx.path}`; | ||
const match = matchers[key] || (matchers[key] = getMatcher(ctx.path, ctx.method)); | ||
ctx.config = config; | ||
if (match) { | ||
log(`[Router] Match route ${framework_1.b(match.route.method)} ${framework_1.b(match.route.url)} with ${framework_1.b(ctx.method)} ${framework_1.b(ctx.path)}`); | ||
//assign config and route to context | ||
Object.assign(ctx, { config, route: match.route }); | ||
//add query | ||
const query = match.keys.reduce((a, b, i) => { | ||
a[b.name.toString().toLowerCase()] = match.match[i + 1]; | ||
return a; | ||
}, {}); | ||
log(`[Router] Extracted parameter from url ${framework_1.b(util_1.inspect(query, false, null))}`); | ||
Object.assign(ctx.query, query); | ||
await handler(ctx); | ||
Object.assign(ctx.request.query, match.query); | ||
const parameters = core_1.binder.bindParameter(ctx, match.route.action, config.converters); | ||
ctx.route = match.route; | ||
ctx.parameters = parameters; | ||
} | ||
else { | ||
log(`[Router] Not route match ${framework_1.b(ctx.method)} ${framework_1.b(ctx.url)}`); | ||
await next(); | ||
} | ||
const invocation = handler(ctx); | ||
const result = await invocation.proceed(); | ||
await result.execute(ctx); | ||
}; | ||
} | ||
exports.router = router; | ||
/* ------------------------------------------------------------------------------- */ | ||
/* --------------------------- ANALYZER FUNCTION --------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function backingParameterTest(route, allRoutes) { | ||
const ids = route.url.split("/") | ||
.filter(x => x.startsWith(":")) | ||
.map(x => x.substring(1)); | ||
const missing = ids.filter(id => route.action.parameters.map(x => x.name).indexOf(id) === -1); | ||
if (missing.length > 0) { | ||
return { | ||
type: "error", | ||
message: framework_1.StringUtil.format(framework_1.errorMessage.RouteDoesNotHaveBackingParam, missing.join(", ")) | ||
}; | ||
} | ||
else | ||
return { type: "success" }; | ||
} | ||
function metadataTypeTest(route, allRoutes) { | ||
const hasTypeInfo = route.action | ||
.parameters.some(x => Boolean(x.typeAnnotation)); | ||
if (!hasTypeInfo) { | ||
return { | ||
type: "warning", | ||
message: framework_1.errorMessage.ActionDoesNotHaveTypeInfo | ||
}; | ||
} | ||
else | ||
return { type: "success" }; | ||
} | ||
function multipleDecoratorTest(route, allRoutes) { | ||
const decorator = route.action.decorators.filter(x => x.name == "Route"); | ||
if (decorator.length > 1) { | ||
return { | ||
type: "error", | ||
message: framework_1.errorMessage.MultipleDecoratorNotSupported | ||
}; | ||
} | ||
else | ||
return { type: "success" }; | ||
} | ||
function duplicateRouteTest(route, allRoutes) { | ||
const dup = allRoutes.filter(x => x.url == route.url && x.method == route.method); | ||
if (dup.length > 1) { | ||
return { | ||
type: "error", | ||
message: framework_1.StringUtil.format(framework_1.errorMessage.DuplicateRouteFound, dup.map(x => getActionName(x)).join(" ")) | ||
}; | ||
} | ||
else | ||
return { type: "success" }; | ||
} | ||
/* ------------------------------------------------------------------------------- */ | ||
/* -------------------------------- ANALYZER ------------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function analyzeRoute(route, tests, allRoutes) { | ||
const issues = tests.map(test => test(route, allRoutes)) | ||
.filter(x => x.type != "success"); | ||
return { route, issues }; | ||
} | ||
function analyzeRoutes(routes) { | ||
const tests = [ | ||
backingParameterTest, metadataTypeTest, multipleDecoratorTest, | ||
duplicateRouteTest | ||
]; | ||
return routes.map(x => analyzeRoute(x, tests, routes)); | ||
} | ||
exports.analyzeRoutes = analyzeRoutes; | ||
function printAnalysis(results) { | ||
const data = results.map(x => { | ||
const method = framework_1.StringUtil.padRight(x.route.method.toUpperCase(), 5); | ||
const action = getActionName(x.route); | ||
const issues = x.issues.map(issue => ` - ${issue.type} ${issue.message}`); | ||
return { method, url: x.route.url, action, issues }; | ||
}); | ||
data.forEach((x, i) => { | ||
const action = framework_1.StringUtil.padRight(x.action, Math.max(...data.map(x => x.action.length))); | ||
const method = framework_1.StringUtil.padRight(x.method, Math.max(...data.map(x => x.method.length))); | ||
const url = framework_1.StringUtil.padRight(x.url, Math.max(...data.map(x => x.url.length))); | ||
const issueColor = (issue) => issue.startsWith(" - warning") ? chalk_1.default.yellow(issue) : chalk_1.default.red(issue); | ||
const color = x.issues.length == 0 ? (x) => x : | ||
x.issues.some(x => x.startsWith(" - warning")) ? chalk_1.default.yellow : chalk_1.default.red; | ||
console.log(color(`${i + 1}. ${action} -> ${method} ${url}`)); | ||
x.issues.forEach(issue => console.log(issueColor(issue))); | ||
}); | ||
} | ||
exports.printAnalysis = printAnalysis; |
{ | ||
"name": "plumier", | ||
"version": "0.1.0-alpha.d43b984e", | ||
"description": "A testing friendly Web API framework", | ||
"version": "1.0.0-alpha.5+5f97250", | ||
"description": "Delightful Node.js Rest API Framework powered by Koa and TypeScript", | ||
"main": "lib/index.js", | ||
@@ -9,3 +9,3 @@ "types": "lib/index.d.ts", | ||
"MVC", | ||
"Express", | ||
"Koa", | ||
"IoC", | ||
@@ -18,29 +18,64 @@ "Dependency Injection", | ||
"scripts": { | ||
"compile": "tsc -p tsconfig.build.json" | ||
"compile": "tsc -p tsconfig.build.json", | ||
"benchmark": "ts-node test/benchmark/run" | ||
}, | ||
"author": "Ketut Sandiarsa", | ||
"license": "ISC", | ||
"license": "MIT", | ||
"dependencies": { | ||
"@koa/cors": "^2.2.1", | ||
"@types/chalk": "^2.2.0", | ||
"@types/debug": "^0.0.30", | ||
"@types/koa": "^2.0.46", | ||
"@koa/cors": "^3.0.0", | ||
"@plumier/core": "1.0.0-alpha.5+5f97250", | ||
"@plumier/validator": "1.0.0-alpha.5+5f97250", | ||
"@types/glob": "^7.1.1", | ||
"@types/koa": "^2.0.48", | ||
"@types/koa-bodyparser": "^5.0.0", | ||
"@types/koa__cors": "^2.2.2", | ||
"chalk": "^2.4.1", | ||
"debug": "^3.1.0", | ||
"koa": "^2.5.1", | ||
"@types/koa__cors": "^2.2.3", | ||
"@types/validator": "^10.9.0", | ||
"chalk": "^2.4.2", | ||
"glob": "^7.1.3", | ||
"koa": "^2.7.0", | ||
"koa-bodyparser": "^4.2.1", | ||
"path-to-regexp": "^2.2.1", | ||
"tslib": "^1.9.2" | ||
"path-to-regexp": "^3.0.0", | ||
"tinspector": "^2.2.0", | ||
"tslib": "^1.9.3" | ||
}, | ||
"peerDependencies": { | ||
"reflect-metadata": "^0.1.12" | ||
}, | ||
"devDependencies": { | ||
"@types/supertest": "^2.0.4", | ||
"@plumier/jwt": "1.0.0-alpha.5+5f97250", | ||
"@plumier/multipart": "1.0.0-alpha.5+5f97250", | ||
"@plumier/serve-static": "1.0.0-alpha.5+5f97250", | ||
"@types/body-parser": "^1.17.0", | ||
"@types/cors": "^2.8.4", | ||
"@types/express": "^4.16.1", | ||
"@types/express-jwt": "^0.0.42", | ||
"@types/fs-extra": "^5.0.5", | ||
"@types/joi": "^14.3.1", | ||
"@types/jsonwebtoken": "^8.3.0", | ||
"@types/koa-router": "^7.0.39", | ||
"@types/rimraf": "^2.0.2", | ||
"@types/supertest": "^2.0.7", | ||
"autocannon": "^3.2.0", | ||
"benalu": "^2.0.0-beta-1", | ||
"supertest": "^3.1.0", | ||
"typescript": "^2.9.2" | ||
} | ||
"body-parser": "^1.18.3", | ||
"cors": "^2.8.5", | ||
"express": "^4.16.4", | ||
"express-jwt": "^5.3.1", | ||
"fs-extra": "^7.0.1", | ||
"joi": "^14.3.1", | ||
"jsonwebtoken": "^8.4.0", | ||
"koa-router": "^7.4.0", | ||
"rimraf": "^2.6.3", | ||
"supertest": "^4.0.0", | ||
"ts-node": "^8.0.2", | ||
"typescript": "^3.3.3333" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/plumier/plumier/issues" | ||
}, | ||
"repository": { | ||
"url": "https://github.com/plumier/plumier" | ||
}, | ||
"homepage": "https://plumierjs.com", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"gitHead": "5f97250cf3b0e9378c65d0dcd14b1a7642402643" | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
0
1
180
3
26810
15
27
9
357
2
+ Added@types/glob@^7.1.1
+ Added@types/validator@^10.9.0
+ Addedglob@^7.1.3
+ Addedtinspector@^2.2.0
+ Added@koa/cors@3.4.3(transitive)
+ Added@types/acorn@4.0.5(transitive)
+ Added@types/estree@1.0.6(transitive)
+ Added@types/glob@7.2.0(transitive)
+ Added@types/minimatch@5.1.2(transitive)
+ Added@types/node@22.13.4(transitive)
+ Added@types/validator@10.11.3(transitive)
+ Addedacorn@7.1.1(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedglob@7.2.3(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedminimatch@3.1.2(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedpath-to-regexp@3.3.0(transitive)
+ Addedtinspector@2.3.1(transitive)
+ Addedwrappy@1.0.2(transitive)
- Removed@types/chalk@^2.2.0
- Removed@types/debug@^0.0.30
- Removeddebug@^3.1.0
- Removed@koa/cors@2.2.3(transitive)
- Removed@types/chalk@2.2.4(transitive)
- Removed@types/debug@0.0.30(transitive)
- Removed@types/node@22.13.1(transitive)
- Removeddebug@3.2.7(transitive)
- Removedpath-to-regexp@2.4.0(transitive)
Updated@koa/cors@^3.0.0
Updated@types/koa@^2.0.48
Updated@types/koa__cors@^2.2.3
Updatedchalk@^2.4.2
Updatedkoa@^2.7.0
Updatedpath-to-regexp@^3.0.0
Updatedtslib@^1.9.3