@plumier/core
Advanced tools
Comparing version 1.0.0-rc2.59 to 1.0.0
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createPipes = exports.invoke = void 0; | ||
const authorization_1 = require("./authorization"); | ||
const binder_1 = require("./binder"); | ||
const common_1 = require("./common"); | ||
const types_1 = require("./types"); | ||
const binder_1 = require("./binder"); | ||
const validator_1 = require("./validator"); | ||
const authorization_1 = require("./authorization"); | ||
// --------------------------------------------------------------------- // | ||
@@ -94,3 +95,10 @@ // ---------------------------- INVOCATIONS ---------------------------- // | ||
function createPipes(context) { | ||
const globalMdw = context.config.middlewares.map(m => ({ middleware: m })); | ||
const globalMdw = []; | ||
const actionMdw = []; | ||
for (const mdw of context.config.middlewares) { | ||
if (mdw.scope === "Global") | ||
globalMdw.push(mdw); | ||
else | ||
actionMdw.push(mdw); | ||
} | ||
if (common_1.hasKeyOf(context, "route")) { | ||
@@ -107,2 +115,4 @@ const middlewares = [ | ||
// 5. action middlewares | ||
...actionMdw, | ||
// 6. action middlewares from decorators | ||
...types_1.MiddlewareUtil.extractDecorators(context.route) | ||
@@ -109,0 +119,0 @@ ]; |
@@ -1,29 +0,114 @@ | ||
import { ActionContext, ActionResult, Configuration, Invocation, Middleware, RouteInfo, Metadata } from "./types"; | ||
interface AuthorizationContext { | ||
value?: any; | ||
role: string[]; | ||
user: any; | ||
ctx: ActionContext; | ||
metadata: Metadata; | ||
} | ||
declare type AuthorizerFunction = (info: AuthorizationContext, location: "Class" | "Parameter" | "Method") => boolean | Promise<boolean>; | ||
import { Class } from "./common"; | ||
import { AccessModifier, ActionContext, ActionResult, AuthorizationContext, Authorizer, AuthPolicy, Invocation, Middleware, RouteInfo } from "./types"; | ||
declare type AuthorizerFunction = (info: AuthorizationContext) => boolean | Promise<boolean>; | ||
interface AuthorizeDecorator { | ||
type: "plumier-meta:authorize"; | ||
authorize: string | AuthorizerFunction | Authorizer; | ||
policies: string[]; | ||
access: AccessModifier; | ||
tag: string; | ||
location: "Class" | "Parameter" | "Method"; | ||
evaluation: "Static" | "Dynamic"; | ||
appliedClass: Class; | ||
} | ||
interface Authorizer { | ||
authorize(info: AuthorizationContext, location: "Class" | "Parameter" | "Method"): boolean | Promise<boolean>; | ||
} | ||
declare type CustomAuthorizer = Authorizer; | ||
declare type CustomAuthorizerFunction = AuthorizerFunction; | ||
declare type AuthorizerContext = AuthorizationContext; | ||
declare type RoleField = string | ((value: any) => Promise<string[]>); | ||
declare function getAuthorizeDecorators(info: RouteInfo, globalDecorator?: (...args: any[]) => void): AuthorizeDecorator[]; | ||
declare function updateRouteAuthorizationAccess(routes: RouteInfo[], config: Configuration): void; | ||
declare function getRouteAuthorizeDecorators(info: RouteInfo, globalDecorator?: string | string[]): AuthorizeDecorator[]; | ||
declare function createAuthContext(ctx: ActionContext, access: AccessModifier): AuthorizationContext; | ||
declare function throwAuthError(ctx: AuthorizerContext, msg?: string): void; | ||
declare const Public = "Public"; | ||
declare const Authenticated = "Authenticated"; | ||
declare const AuthorizeReadonly = "plumier::readonly"; | ||
declare const AuthorizeWriteonly = "plumier::writeonly"; | ||
declare type EntityProviderQuery<T = any> = (entity: Class, id: any) => Promise<T>; | ||
interface EntityPolicyProviderDecorator { | ||
kind: "plumier-meta:entity-policy-provider"; | ||
entity: Class; | ||
idParam: string; | ||
} | ||
declare type EntityPolicyAuthorizerFunction = (ctx: AuthorizerContext, id: any) => boolean | Promise<boolean>; | ||
declare class PolicyAuthorizer implements Authorizer { | ||
private policies; | ||
private keys; | ||
constructor(policies: Class<AuthPolicy>[], keys: string[]); | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
} | ||
declare class CustomAuthPolicy implements AuthPolicy { | ||
name: string; | ||
private authorizer; | ||
constructor(name: string, authorizer: CustomAuthorizerFunction | CustomAuthorizer); | ||
equals(id: string, ctx: AuthorizationContext): boolean; | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
conflict(other: AuthPolicy): boolean; | ||
friendlyName(): string; | ||
} | ||
declare class PublicAuthPolicy extends CustomAuthPolicy { | ||
constructor(); | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
} | ||
declare class AuthenticatedAuthPolicy extends CustomAuthPolicy { | ||
constructor(); | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
} | ||
declare class ReadonlyAuthPolicy extends CustomAuthPolicy { | ||
constructor(); | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
} | ||
declare class WriteonlyAuthPolicy extends CustomAuthPolicy { | ||
constructor(); | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
} | ||
declare class EntityAuthPolicy<T> implements AuthPolicy { | ||
name: string; | ||
entity: Class<T>; | ||
private authorizer; | ||
constructor(name: string, entity: Class<T>, authorizer: EntityPolicyAuthorizerFunction); | ||
private getEntity; | ||
equals(id: string, ctx: AuthorizationContext): boolean; | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
conflict(other: AuthPolicy): boolean; | ||
friendlyName(): string; | ||
} | ||
declare class AuthPolicyBuilder { | ||
private globalCache; | ||
constructor(globalCache: Class<AuthPolicy>[]); | ||
/** | ||
* Define AuthPolicy class on the fly | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
define(id: string, authorizer: CustomAuthorizerFunction | CustomAuthorizer): Class<AuthPolicy>; | ||
/** | ||
* Register authorization policy into authorization cache | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
register(id: string, authorizer: CustomAuthorizerFunction | CustomAuthorizer): AuthPolicyBuilder; | ||
} | ||
declare class EntityPolicyBuilder<T> { | ||
private entity; | ||
private globalCache; | ||
constructor(entity: Class<T>, globalCache: Class<AuthPolicy>[]); | ||
/** | ||
* Define AuthPolicy class on the fly | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
define(id: string, authorizer: EntityPolicyAuthorizerFunction): Class<AuthPolicy>; | ||
/** | ||
* Register authorization policy into authorization cache | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
register(id: string, authorizer: EntityPolicyAuthorizerFunction): EntityPolicyBuilder<T>; | ||
} | ||
declare const globalPolicies: Class<AuthPolicy>[]; | ||
declare function authPolicy(): AuthPolicyBuilder; | ||
declare function entityPolicy<T>(entity: Class<T>): EntityPolicyBuilder<T>; | ||
declare function executeAuthorizer(decorator: AuthorizeDecorator | AuthorizeDecorator[], info: AuthorizationContext): Promise<boolean>; | ||
declare function checkAuthorize(ctx: ActionContext): Promise<void>; | ||
declare class AuthorizerMiddleware implements Middleware { | ||
constructor(); | ||
execute(invocation: Readonly<Invocation<ActionContext>>): Promise<ActionResult>; | ||
} | ||
export { AuthorizerFunction, RoleField, Authorizer, checkAuthorize, AuthorizeDecorator, getAuthorizeDecorators, updateRouteAuthorizationAccess, AuthorizerMiddleware, CustomAuthorizer, CustomAuthorizerFunction, AuthorizationContext, AuthorizerContext }; | ||
export { AuthorizerFunction, Authorizer, checkAuthorize, AuthorizeDecorator, getRouteAuthorizeDecorators, AuthorizerMiddleware, CustomAuthorizer, CustomAuthorizerFunction, AuthorizationContext, AuthorizerContext, AccessModifier, EntityPolicyProviderDecorator, EntityProviderQuery, PublicAuthPolicy, AuthenticatedAuthPolicy, authPolicy, entityPolicy, EntityPolicyAuthorizerFunction, PolicyAuthorizer, Public, Authenticated, AuthPolicy, CustomAuthPolicy, EntityAuthPolicy, globalPolicies, AuthorizeReadonly, AuthorizeWriteonly, ReadonlyAuthPolicy, WriteonlyAuthPolicy, createAuthContext, executeAuthorizer, throwAuthError }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.throwAuthError = exports.executeAuthorizer = exports.createAuthContext = exports.WriteonlyAuthPolicy = exports.ReadonlyAuthPolicy = exports.AuthorizeWriteonly = exports.AuthorizeReadonly = exports.globalPolicies = exports.EntityAuthPolicy = exports.CustomAuthPolicy = exports.Authenticated = exports.Public = exports.PolicyAuthorizer = exports.entityPolicy = exports.authPolicy = exports.AuthenticatedAuthPolicy = exports.PublicAuthPolicy = exports.AuthorizerMiddleware = exports.getRouteAuthorizeDecorators = exports.checkAuthorize = void 0; | ||
const tslib_1 = require("tslib"); | ||
const tinspector_1 = require("tinspector"); | ||
const reflect_1 = require("@plumier/reflect"); | ||
const debug_1 = tslib_1.__importDefault(require("debug")); | ||
const common_1 = require("./common"); | ||
const http_status_1 = require("./http-status"); | ||
const types_1 = require("./types"); | ||
// --------------------------------------------------------------------- // | ||
// ------------------------------- TYPES ------------------------------- // | ||
// --------------------------------------------------------------------- // | ||
const log = { | ||
debug: debug_1.default("@plumier/core:authorization"), | ||
}; | ||
/* ------------------------------------------------------------------------------- */ | ||
/* ------------------------------- HELPERS --------------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function executeDecorator(decorator, info) { | ||
const authorize = decorator.authorize; | ||
let instance; | ||
if (typeof authorize === "function") | ||
instance = { authorize }; | ||
else if (common_1.hasKeyOf(authorize, "authorize")) | ||
instance = authorize; | ||
else | ||
instance = info.ctx.config.dependencyResolver.resolve(authorize); | ||
return instance.authorize(info, decorator.location); | ||
function createDecoratorFilter(predicate) { | ||
return (x) => x.type === "plumier-meta:authorize" && predicate(x); | ||
} | ||
function isAuthDecorator(decorator) { | ||
return decorator.type === "plumier-meta:authorize"; | ||
} | ||
function getGlobalDecorators(globalDecorator) { | ||
if (globalDecorator) { | ||
let DummyClass = class DummyClass { | ||
}; | ||
DummyClass = tslib_1.__decorate([ | ||
globalDecorator | ||
], DummyClass); | ||
const meta = tinspector_1.reflect(DummyClass); | ||
return meta.decorators.filter((x) => isAuthDecorator(x)); | ||
} | ||
else | ||
return []; | ||
const policies = typeof globalDecorator === "string" ? [globalDecorator] : globalDecorator; | ||
return [{ | ||
type: "plumier-meta:authorize", | ||
policies, | ||
tag: policies.join("|"), | ||
access: "route", | ||
evaluation: "Dynamic", | ||
location: "Method", | ||
appliedClass: Object | ||
}]; | ||
} | ||
function getAuthorizeDecorators(info, globalDecorator) { | ||
const actionDecs = info.action.decorators.filter((x) => isAuthDecorator(x)); | ||
function getRouteAuthorizeDecorators(info, globalDecorator) { | ||
// if action has decorators then return immediately to prioritize the action decorator | ||
const actionDecs = info.action.decorators.filter(createDecoratorFilter(x => x.access === "route")); | ||
if (actionDecs.length > 0) | ||
return actionDecs; | ||
const controllerDecs = info.controller.decorators.filter((x) => isAuthDecorator(x)); | ||
// if controller has decorators then return immediately | ||
const controllerDecs = info.controller.decorators.filter(createDecoratorFilter(x => x.access === "route")); | ||
if (controllerDecs.length > 0) | ||
return controllerDecs; | ||
if (!globalDecorator) | ||
return []; | ||
return getGlobalDecorators(globalDecorator); | ||
} | ||
exports.getAuthorizeDecorators = getAuthorizeDecorators; | ||
async function checkParameter(path, meta, value, info, parent) { | ||
if (value === undefined) | ||
exports.getRouteAuthorizeDecorators = getRouteAuthorizeDecorators; | ||
function createAuthContext(ctx, access) { | ||
const { route, state } = ctx; | ||
return { | ||
user: state.user, route, ctx, access, policyIds: [], | ||
metadata: new types_1.MetadataImpl(ctx.parameters, ctx.route, {}), | ||
}; | ||
} | ||
exports.createAuthContext = createAuthContext; | ||
function throwAuthError(ctx, msg) { | ||
if (!ctx.user) | ||
throw new types_1.HttpStatusError(http_status_1.HttpStatus.Forbidden, msg !== null && msg !== void 0 ? msg : "Forbidden"); | ||
else | ||
throw new types_1.HttpStatusError(http_status_1.HttpStatus.Unauthorized, msg !== null && msg !== void 0 ? msg : "Unauthorized"); | ||
} | ||
exports.throwAuthError = throwAuthError; | ||
function getErrorLocation(metadata) { | ||
const current = metadata.current; | ||
if (current.kind === "Class") | ||
return `class ${current.name}`; | ||
return `${current.kind.toLowerCase()} ${current.parent.name}.${current.name}`; | ||
} | ||
// --------------------------------------------------------------------- // | ||
// ------------------------ AUTHORIZATION POLICY ----------------------- // | ||
// --------------------------------------------------------------------- // | ||
const Public = "Public"; | ||
exports.Public = Public; | ||
const Authenticated = "Authenticated"; | ||
exports.Authenticated = Authenticated; | ||
const AuthorizeReadonly = "plumier::readonly"; | ||
exports.AuthorizeReadonly = AuthorizeReadonly; | ||
const AuthorizeWriteonly = "plumier::writeonly"; | ||
exports.AuthorizeWriteonly = AuthorizeWriteonly; | ||
class PolicyAuthorizer { | ||
constructor(policies, keys) { | ||
this.policies = policies; | ||
this.keys = keys; | ||
} | ||
async authorize(ctx) { | ||
for (const Auth of this.policies.reverse()) { | ||
const authPolicy = new Auth(); | ||
for (const policy of this.keys) { | ||
if (authPolicy.equals(policy, ctx)) { | ||
const authorize = await authPolicy.authorize(ctx); | ||
log.debug("%s by %s", authorize ? "AUTHORIZED" : "FORBIDDEN", authPolicy.friendlyName()); | ||
if (authorize) | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
} | ||
exports.PolicyAuthorizer = PolicyAuthorizer; | ||
class CustomAuthPolicy { | ||
constructor(name, authorizer) { | ||
this.name = name; | ||
this.authorizer = authorizer; | ||
} | ||
equals(id, ctx) { | ||
return id === this.name; | ||
} | ||
async authorize(ctx) { | ||
try { | ||
if (typeof this.authorizer === "function") | ||
return await this.authorizer(ctx); | ||
else | ||
return await this.authorizer.authorize(ctx); | ||
} | ||
catch (e) { | ||
const message = e instanceof Error ? e.stack : e; | ||
const location = getErrorLocation(ctx.metadata); | ||
throw new Error(`Error occur inside authorization policy ${this.name} on ${location} \n ${message}`); | ||
} | ||
} | ||
conflict(other) { | ||
return this.name === other.name; | ||
} | ||
friendlyName() { | ||
return `AuthPolicy { name: "${this.name}" }`; | ||
} | ||
} | ||
exports.CustomAuthPolicy = CustomAuthPolicy; | ||
class PublicAuthPolicy extends CustomAuthPolicy { | ||
constructor() { super(Public, {}); } | ||
async authorize(ctx) { | ||
return true; | ||
} | ||
} | ||
exports.PublicAuthPolicy = PublicAuthPolicy; | ||
class AuthenticatedAuthPolicy extends CustomAuthPolicy { | ||
constructor() { super(Authenticated, {}); } | ||
async authorize(ctx) { | ||
return !!ctx.user; | ||
} | ||
} | ||
exports.AuthenticatedAuthPolicy = AuthenticatedAuthPolicy; | ||
class ReadonlyAuthPolicy extends CustomAuthPolicy { | ||
constructor() { super(AuthorizeReadonly, {}); } | ||
async authorize(ctx) { | ||
return false; | ||
} | ||
} | ||
exports.ReadonlyAuthPolicy = ReadonlyAuthPolicy; | ||
class WriteonlyAuthPolicy extends CustomAuthPolicy { | ||
constructor() { super(AuthorizeWriteonly, {}); } | ||
async authorize(ctx) { | ||
return false; | ||
} | ||
} | ||
exports.WriteonlyAuthPolicy = WriteonlyAuthPolicy; | ||
class EntityAuthPolicy { | ||
constructor(name, entity, authorizer) { | ||
this.name = name; | ||
this.entity = entity; | ||
this.authorizer = authorizer; | ||
} | ||
getEntity(ctx) { | ||
if (ctx.access === "route" || ctx.access === "write") { | ||
// when the entity provider is Route | ||
// take the provided Entity from decorator | ||
// take the entity ID value from the Action Parameter | ||
const dec = ctx.metadata.action.decorators | ||
.find((x) => x.kind === "plumier-meta:entity-policy-provider"); | ||
if (!dec) { | ||
const meta = ctx.metadata; | ||
throw new Error(`Action ${meta.controller.name}.${meta.action.name} doesn't have Entity Policy Provider information`); | ||
} | ||
const id = ctx.metadata.actionParams.get(dec.idParam); | ||
return { entity: dec.entity, id }; | ||
} | ||
else { | ||
// when the entity provider is Read/Write/Filter | ||
// take the provided entity from the parent type from context | ||
// take the entity ID value using @primaryId() decorator | ||
const entity = ctx.metadata.current.parent; | ||
const meta = reflect_1.reflect(entity); | ||
const prop = meta.properties.find(p => p.decorators.some((x) => x.kind === "plumier-meta:entity-id")); | ||
if (!prop) | ||
throw new Error(`Entity ${entity.name} doesn't have primary ID information required for entity policy`); | ||
const id = ctx.parentValue[prop.name]; | ||
return { entity, id }; | ||
} | ||
} | ||
equals(id, ctx) { | ||
if (id === this.name) { | ||
const provider = this.getEntity(ctx); | ||
return this.entity === provider.entity; | ||
} | ||
return false; | ||
} | ||
async authorize(ctx) { | ||
const provider = this.getEntity(ctx); | ||
try { | ||
return await this.authorizer(ctx, provider.id); | ||
} | ||
catch (e) { | ||
const message = e instanceof Error ? e.stack : e; | ||
const location = getErrorLocation(ctx.metadata); | ||
throw new Error(`Error occur inside authorization policy ${this.name} for entity ${this.entity.name} on ${location} \n ${message}`); | ||
} | ||
} | ||
conflict(other) { | ||
if (other instanceof EntityAuthPolicy) | ||
return this.name === other.name && this.entity === other.entity; | ||
else | ||
return this.name === other.name; | ||
} | ||
friendlyName() { | ||
return `EntityPolicy { name: "${this.name}", entity: ${this.entity.name} }`; | ||
} | ||
} | ||
exports.EntityAuthPolicy = EntityAuthPolicy; | ||
class AuthPolicyBuilder { | ||
constructor(globalCache) { | ||
this.globalCache = globalCache; | ||
} | ||
/** | ||
* Define AuthPolicy class on the fly | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
define(id, authorizer) { | ||
class Policy extends CustomAuthPolicy { | ||
constructor() { super(id, authorizer); } | ||
} | ||
return Policy; | ||
} | ||
/** | ||
* Register authorization policy into authorization cache | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
register(id, authorizer) { | ||
const Policy = this.define(id, authorizer); | ||
this.globalCache.push(Policy); | ||
return this; | ||
} | ||
} | ||
class EntityPolicyBuilder { | ||
constructor(entity, globalCache) { | ||
this.entity = entity; | ||
this.globalCache = globalCache; | ||
} | ||
/** | ||
* Define AuthPolicy class on the fly | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
define(id, authorizer) { | ||
const entity = this.entity; | ||
class Policy extends EntityAuthPolicy { | ||
constructor() { super(id, entity, authorizer); } | ||
} | ||
return Policy; | ||
} | ||
/** | ||
* Register authorization policy into authorization cache | ||
* @param id Id of the authorization policy that will be used in @authorize decorator | ||
* @param authorizer Authorization logic, a lambda function return true to authorize otherwise false | ||
*/ | ||
register(id, authorizer) { | ||
const Policy = this.define(id, authorizer); | ||
this.globalCache.push(Policy); | ||
return this; | ||
} | ||
} | ||
const globalPolicies = []; | ||
exports.globalPolicies = globalPolicies; | ||
function authPolicy() { | ||
return new AuthPolicyBuilder(globalPolicies); | ||
} | ||
exports.authPolicy = authPolicy; | ||
function entityPolicy(entity) { | ||
return new EntityPolicyBuilder(entity, globalPolicies); | ||
} | ||
exports.entityPolicy = entityPolicy; | ||
// --------------------------------------------------------------------- // | ||
// ---------------------- MAIN AUTHORIZER FUNCTION --------------------- // | ||
// --------------------------------------------------------------------- // | ||
function executeAuthorizer(decorator, info) { | ||
const policies = Array.isArray(decorator) ? decorator.map(x => x.policies).flatten() : decorator.policies; | ||
const instance = new PolicyAuthorizer(info.ctx.config.authPolicies, policies); | ||
return instance.authorize(info); | ||
} | ||
exports.executeAuthorizer = executeAuthorizer; | ||
// --------------------------------------------------------------------- // | ||
// ----------------- CONTROLLER OR ACTION AUTHORIZATION ---------------- // | ||
// --------------------------------------------------------------------- // | ||
function fixContext(decorator, info) { | ||
if (decorator.location === "Class") { | ||
info.metadata.current = info.ctx.route.controller; | ||
} | ||
if (decorator.location === "Method") { | ||
info.metadata.current = Object.assign(Object.assign({}, info.ctx.route.action), { parent: info.ctx.route.controller.type }); | ||
} | ||
return info; | ||
} | ||
async function checkUserAccessToRoute(decorators, info) { | ||
const conditions = await Promise.all(decorators.map(x => executeAuthorizer(x, fixContext(x, info)))); | ||
// if authorized once then pass | ||
if (conditions.some(x => x === true)) | ||
return; | ||
// if not then throw error accordingly | ||
throwAuthError(info); | ||
} | ||
function createContext(ctx, value, meta) { | ||
const info = Object.assign({}, ctx.info); | ||
const metadata = Object.assign({}, info.metadata); | ||
metadata.current = Object.assign(Object.assign({}, meta), { parent: ctx.parent }); | ||
info.value = value; | ||
info.parentValue = ctx.parentValue; | ||
info.metadata = metadata; | ||
return info; | ||
} | ||
async function checkParameter(meta, value, ctx) { | ||
if (value === undefined || value === null) | ||
return []; | ||
@@ -56,3 +330,3 @@ else if (Array.isArray(meta.type)) { | ||
const val = value[i]; | ||
result.push(...await checkParameter(path.concat(i.toString()), newMeta, val, info, parent)); | ||
result.push(...await checkParameter(newMeta, val, Object.assign(Object.assign({}, ctx), { path: ctx.path.concat(i.toString()) }))); | ||
} | ||
@@ -62,22 +336,28 @@ return result; | ||
else if (common_1.isCustomClass(meta.type)) { | ||
const classMeta = tinspector_1.reflect(meta.type); | ||
const classMeta = reflect_1.reflect(meta.type); | ||
const values = classMeta.properties.map(x => value[x.name]); | ||
return checkParameters(path, classMeta.properties, values, info, meta.type); | ||
return checkParameters(classMeta.properties, values, Object.assign(Object.assign({}, ctx), { parent: meta.type, parentValue: value })); | ||
} | ||
else { | ||
const decorators = meta.decorators.filter((x) => isAuthDecorator(x)); | ||
const result = []; | ||
for (const dec of decorators) { | ||
info.metadata.current = Object.assign(Object.assign({}, meta), { parent }); | ||
if (!await executeDecorator(dec, Object.assign(Object.assign({}, info), { value }))) | ||
result.push(path.join(".")); | ||
} | ||
return result; | ||
// skip check on GET method | ||
if (ctx.info.ctx.method === "GET") | ||
return []; | ||
const decorators = meta.decorators.filter(createDecoratorFilter(x => x.access === "write")); | ||
// if no decorator then just allow, follow route authorization | ||
if (decorators.length === 0) | ||
return []; | ||
const info = createContext(ctx, value, meta); | ||
const allowed = await executeAuthorizer(decorators, info); | ||
return allowed ? [] : [ctx.path.join(".")]; | ||
} | ||
} | ||
async function checkParameters(path, meta, value, info, parent) { | ||
async function checkParameters(meta, value, ctx) { | ||
const result = []; | ||
for (let i = 0; i < meta.length; i++) { | ||
const prop = meta[i]; | ||
const issues = await checkParameter(path.concat(prop.name), prop, value[i], info, parent); | ||
// if the property is a relation property just skip checking, since we allow set relation using ID | ||
const isRelation = prop.decorators.some((x) => x.kind === "plumier-meta:relation"); | ||
if (isRelation) | ||
continue; | ||
const issues = await checkParameter(prop, value[i], Object.assign(Object.assign({}, ctx), { path: ctx.path.concat(prop.name) })); | ||
result.push(...issues); | ||
@@ -87,62 +367,103 @@ } | ||
} | ||
function fixContext(decorator, info) { | ||
if (decorator.location === "Class") { | ||
info.metadata.current = info.ctx.route.controller; | ||
async function checkUserAccessToParameters(meta, values, info) { | ||
const unauthorizedPaths = await checkParameters(meta, values, { info, path: [], parent: info.ctx.route.controller.type }); | ||
if (unauthorizedPaths.length > 0) | ||
throwAuthError(info, `Unauthorized to populate parameter paths (${unauthorizedPaths.join(", ")})`); | ||
} | ||
async function createPropertyNode(prop, info) { | ||
const decorators = prop.decorators.filter(createDecoratorFilter(x => x.access === "read")); | ||
let policies = []; | ||
for (const dec of decorators) { | ||
policies.push(...dec.policies); | ||
} | ||
if (decorator.location === "Method") { | ||
info.metadata.current = Object.assign(Object.assign({}, info.ctx.route.action), { parent: info.ctx.route.controller.type }); | ||
// if no authorize decorator then always allow to access | ||
const authorizer = policies.length === 0 ? true : new PolicyAuthorizer(info.ctx.config.authPolicies, policies); | ||
return { name: prop.name, authorizer }; | ||
} | ||
async function compileType(type, ctx, parentTypes) { | ||
if (Array.isArray(type)) { | ||
return { kind: "Array", child: await compileType(type[0], ctx, parentTypes) }; | ||
} | ||
return info; | ||
else if (common_1.isCustomClass(type)) { | ||
// CIRCULAR: just return basic node if circular dependency happened | ||
if (parentTypes.some(x => x === type)) | ||
return { kind: "Class", properties: [] }; | ||
const meta = reflect_1.reflect(type); | ||
const properties = []; | ||
for (const prop of meta.properties) { | ||
const meta = Object.assign(Object.assign({}, prop), { parent: type }); | ||
const propCtx = Object.assign(Object.assign({}, ctx), { metadata: new types_1.MetadataImpl(ctx.ctx.parameters, ctx.ctx.route, meta) }); | ||
const propNode = await createPropertyNode(prop, propCtx); | ||
properties.push(Object.assign(Object.assign({}, propNode), { meta, type: await compileType(prop.type, propCtx, parentTypes.concat(type)) })); | ||
} | ||
return { kind: "Class", properties }; | ||
} | ||
else | ||
return { kind: "Value" }; | ||
} | ||
async function checkUserAccessToRoute(decorators, info) { | ||
if (decorators.some(x => x.tag === "Public")) | ||
return; | ||
if (info.role.length === 0) | ||
throw new types_1.HttpStatusError(http_status_1.HttpStatus.Forbidden, "Forbidden"); | ||
const conditions = await Promise.all(decorators.map(x => executeDecorator(x, fixContext(x, info)))); | ||
//use OR condition | ||
//if ALL condition doesn't authorize user then throw | ||
if (conditions.length > 0 && conditions.every(x => x === false)) | ||
throw new types_1.HttpStatusError(http_status_1.HttpStatus.Unauthorized, "Unauthorized"); | ||
async function getAuthorize(authorizers, ctx) { | ||
if (typeof authorizers === "boolean") | ||
return authorizers; | ||
return authorizers.authorize(ctx); | ||
} | ||
async function checkUserAccessToParameters(meta, values, info) { | ||
const unauthorizedPaths = await checkParameters([], meta, values, info, info.ctx.route.controller.type); | ||
if (unauthorizedPaths.length > 0) | ||
throw new types_1.HttpStatusError(401, `Unauthorized to populate parameter paths (${unauthorizedPaths.join(", ")})`); | ||
async function filterType(raw, node, ctx) { | ||
var _a; | ||
if (raw === undefined || raw === null) | ||
return undefined; | ||
if (node.kind === "Array") { | ||
const result = []; | ||
for (const item of raw) { | ||
const val = await filterType(item, node.child, ctx); | ||
if (val !== undefined) | ||
result.push(val); | ||
} | ||
return result.length === 0 ? undefined : result; | ||
} | ||
else if (node.kind === "Class") { | ||
const result = {}; | ||
for (const prop of node.properties) { | ||
const value = raw[prop.name]; | ||
const authorized = await getAuthorize(prop.authorizer, Object.assign(Object.assign({}, ctx), { value, parentValue: raw, metadata: Object.assign(Object.assign({}, ctx.metadata), { current: prop.meta }) })); | ||
if (authorized) { | ||
const candidate = await filterType(value, prop.type, ctx); | ||
const transform = (_a = ctx.ctx.config.responseTransformer) !== null && _a !== void 0 ? _a : ((a, b) => b); | ||
const val = transform(prop.meta, candidate); | ||
if (val !== undefined) | ||
result[prop.name] = val; | ||
} | ||
} | ||
return Object.keys(result).length === 0 && result.constructor === Object ? undefined : result; | ||
} | ||
else | ||
return raw; | ||
} | ||
async function getRole(user, roleField) { | ||
if (!user) | ||
return []; | ||
if (typeof roleField === "function") | ||
return await roleField(user); | ||
async function responseAuthorize(raw, ctx) { | ||
var _a; | ||
const getType = (resp) => { | ||
return !!resp ? (reflect_1.reflection.isCallback(resp.type) ? resp.type({}) : resp.type) : undefined; | ||
}; | ||
const responseType = ctx.route.action.decorators.find((x) => x.kind === "plumier-meta:response-type"); | ||
const type = (_a = getType(responseType)) !== null && _a !== void 0 ? _a : ctx.route.action.returnType; | ||
if (type !== Promise && type && raw.status === 200 && raw.body) { | ||
const info = createAuthContext(ctx, "read"); | ||
const node = await compileType(type, info, []); | ||
raw.body = Array.isArray(raw.body) && raw.body.length === 0 ? [] : await filterType(raw.body, node, info); | ||
return raw; | ||
} | ||
else { | ||
const role = user[roleField]; | ||
return Array.isArray(role) ? role : [role]; | ||
return raw; | ||
} | ||
} | ||
/* ------------------------------------------------------------------------------- */ | ||
/* --------------------------- MAIN IMPLEMENTATION ------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function updateRouteAuthorizationAccess(routes, config) { | ||
if (config.enableAuthorization) { | ||
routes.forEach(x => { | ||
const decorators = getAuthorizeDecorators(x, config.globalAuthorizationDecorators); | ||
if (decorators.length > 0) | ||
x.access = decorators.map(x => x.tag).join("|"); | ||
else | ||
x.access = "Authenticated"; | ||
}); | ||
} | ||
} | ||
exports.updateRouteAuthorizationAccess = updateRouteAuthorizationAccess; | ||
// --------------------------------------------------------------------- // | ||
// ----------------------------- MIDDLEWARE ---------------------------- // | ||
// --------------------------------------------------------------------- // | ||
async function checkAuthorize(ctx) { | ||
if (ctx.config.enableAuthorization) { | ||
const { route, parameters, state, config } = ctx; | ||
const decorator = getAuthorizeDecorators(route, config.globalAuthorizationDecorators); | ||
const userRoles = await getRole(state.user, config.roleField); | ||
const info = { role: userRoles, user: state.user, route, ctx, metadata: new types_1.MetadataImpl(ctx.parameters, ctx.route, {}) }; | ||
const { route, parameters, config } = ctx; | ||
const info = createAuthContext(ctx, "route"); | ||
const decorator = getRouteAuthorizeDecorators(route, config.globalAuthorizations); | ||
//check user access | ||
await checkUserAccessToRoute(decorator, info); | ||
//if ok check parameter access | ||
await checkUserAccessToParameters(route.action.parameters, parameters, info); | ||
await checkUserAccessToParameters(route.action.parameters, parameters, Object.assign(Object.assign({}, info), { access: "write" })); | ||
} | ||
@@ -152,7 +473,10 @@ } | ||
class AuthorizerMiddleware { | ||
constructor() { } | ||
async execute(invocation) { | ||
Object.assign(invocation.ctx, { user: invocation.ctx.state.user }); | ||
await checkAuthorize(invocation.ctx); | ||
return invocation.proceed(); | ||
const result = await invocation.proceed(); | ||
return responseAuthorize(result, invocation.ctx); | ||
} | ||
} | ||
exports.AuthorizerMiddleware = AuthorizerMiddleware; |
/// <reference types="node" /> | ||
import { IncomingHttpHeaders } from "http"; | ||
import { Context, Request } from "koa"; | ||
import { MethodReflection } from "@plumier/reflect"; | ||
import { ActionContext, ActionResult, Invocation, Middleware, GlobalMetadata } from "./types"; | ||
@@ -10,2 +11,5 @@ interface BindingDecorator { | ||
} | ||
interface BindActionResult { | ||
kind: "plumier-meta:bind-action-result"; | ||
} | ||
declare type RequestPart = keyof Request; | ||
@@ -19,6 +23,6 @@ declare type HeaderPart = keyof IncomingHttpHeaders; | ||
} | ||
declare function binder(ctx: ActionContext): any[]; | ||
declare function binder(methodReflection: MethodReflection, ctx: ActionContext): Promise<any[]>; | ||
declare class ParameterBinderMiddleware implements Middleware { | ||
execute(invocation: Readonly<Invocation<ActionContext>>): Promise<ActionResult>; | ||
} | ||
export { RequestPart, HeaderPart, BindingDecorator, binder, ParameterBinderMiddleware, CustomBinderFunction }; | ||
export { RequestPart, HeaderPart, BindingDecorator, binder, ParameterBinderMiddleware, CustomBinderFunction, BindActionResult }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ParameterBinderMiddleware = exports.binder = void 0; | ||
const common_1 = require("./common"); | ||
const types_1 = require("./types"); | ||
const NEXT = Symbol("__NEXT"); | ||
function isFile(par) { | ||
const type = Array.isArray(par.type) ? par.type[0] : par.type; | ||
return type === types_1.FormFile; | ||
} | ||
function getProperty(obj, parKey) { | ||
@@ -13,12 +18,15 @@ for (const key in obj) { | ||
function bindBody(ctx, par) { | ||
if (isFile(par)) | ||
return; | ||
return common_1.isCustomClass(par.type) || !!(par.type && par.type[0]) ? ctx.request.body : undefined; | ||
} | ||
function bindDecorator(ctx, par) { | ||
const decorator = par.decorators.find((x) => x.type == "ParameterBinding"); | ||
const decorator = par.decorators.find((x) => x.type === "ParameterBinding"); | ||
if (!decorator) | ||
return NEXT; | ||
return decorator.process(ctx, new types_1.MetadataImpl(undefined, ctx.route, Object.assign(Object.assign({}, par), { parent: ctx.route.controller.type }))); | ||
return decorator.process(ctx, new types_1.MetadataImpl([], ctx.route, Object.assign(Object.assign({}, par), { parent: ctx.route.controller.type }))); | ||
} | ||
function bindByName(ctx, par) { | ||
return getProperty(ctx.request.query, par.name) | ||
const paramName = ctx.route.paramMapper.alias(par.name); | ||
return getProperty(ctx.request.query, paramName) | ||
|| getProperty(ctx.request.body, par.name) | ||
@@ -33,4 +41,4 @@ || getProperty(ctx.request.files, par.name) | ||
const binderChain = chain(bindDecorator, bindByName, bindBody); | ||
function binder(ctx) { | ||
return ctx.route.action.parameters.map(x => binderChain(ctx, x)); | ||
function binder(methodReflection, ctx) { | ||
return Promise.all(methodReflection.parameters.map(x => binderChain(ctx, x))); | ||
} | ||
@@ -40,3 +48,3 @@ exports.binder = binder; | ||
async execute(invocation) { | ||
invocation.ctx.parameters = binder(invocation.ctx); | ||
invocation.ctx.parameters = await binder(invocation.ctx.route.action, invocation.ctx); | ||
return invocation.proceed(); | ||
@@ -43,0 +51,0 @@ } |
@@ -1,3 +0,3 @@ | ||
/// <reference types="jest" /> | ||
declare type Class = new (...args: any[]) => any; | ||
import glob from "glob"; | ||
declare type Class<T = any> = new (...args: any[]) => T; | ||
declare global { | ||
@@ -11,2 +11,3 @@ interface String { | ||
} | ||
declare function ellipsis(str: string, length: number): string; | ||
declare function getChildValue(object: any, path: string): any; | ||
@@ -17,7 +18,4 @@ declare function hasKeyOf<T>(opt: any, key: string): opt is T; | ||
declare function memoize<R, P extends any[]>(fn: (...args: P) => R, getKey: (...args: P) => string): (...args: P) => R; | ||
declare namespace consoleLog { | ||
function startMock(): jest.Mock<void, [any]>; | ||
function clearMock(): void; | ||
} | ||
declare function findFilesRecursive(path: string): string[]; | ||
declare function globAsync(path: string, opts?: glob.IOptions): Promise<string[]>; | ||
declare function findFilesRecursive(path: string): Promise<string[]>; | ||
interface ColumnMeta { | ||
@@ -31,3 +29,31 @@ align?: "left" | "right"; | ||
declare function printTable<T>(meta: (ColumnMeta | string | undefined)[], data: T[], option?: TableOption<T>): void; | ||
declare function cleanupConsole(mocks: string[][]): string[][]; | ||
export { toBoolean, getChildValue, Class, hasKeyOf, isCustomClass, consoleLog, findFilesRecursive, memoize, printTable, cleanupConsole }; | ||
interface TraverseContext<T> { | ||
path: string[]; | ||
parentPath: Class[]; | ||
} | ||
interface AnalysisMessage { | ||
issue: "NoProperties" | "TypeMissing" | "ArrayTypeMissing"; | ||
location: string; | ||
} | ||
declare type EntityRelationInfo = OneToManyRelationInfo | ManyToOneRelationInfo; | ||
interface OneToManyRelationInfo { | ||
type: "OneToMany"; | ||
parent: Class; | ||
child: Class; | ||
parentProperty: string; | ||
childProperty?: string; | ||
} | ||
interface ManyToOneRelationInfo { | ||
type: "ManyToOne"; | ||
parent: Class; | ||
child: Class; | ||
parentProperty?: string; | ||
childProperty: string; | ||
} | ||
declare function analyzeModel<T>(type: Class | Class[], ctx?: TraverseContext<T>): AnalysisMessage[]; | ||
declare namespace entityHelper { | ||
function getIdProp(entity: Class): import("@plumier/reflect").PropertyReflection | undefined; | ||
function getIdType(entity: Class): Class | undefined; | ||
function getRelationInfo([entity, relation]: [Class, string, Class?]): EntityRelationInfo; | ||
} | ||
export { ellipsis, toBoolean, getChildValue, Class, hasKeyOf, isCustomClass, entityHelper, findFilesRecursive, memoize, printTable, analyzeModel, AnalysisMessage, globAsync, EntityRelationInfo, OneToManyRelationInfo, ManyToOneRelationInfo }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.globAsync = exports.analyzeModel = exports.printTable = exports.memoize = exports.findFilesRecursive = exports.entityHelper = exports.isCustomClass = exports.hasKeyOf = exports.getChildValue = exports.toBoolean = exports.ellipsis = void 0; | ||
const tslib_1 = require("tslib"); | ||
const fs_1 = require("fs"); | ||
const glob_1 = tslib_1.__importDefault(require("glob")); | ||
const path_1 = require("path"); | ||
const tinspector_1 = require("tinspector"); | ||
const reflect_1 = tslib_1.__importStar(require("@plumier/reflect")); | ||
const util_1 = require("util"); | ||
const types_1 = require("./types"); | ||
const lstatAsync = util_1.promisify(fs_1.lstat); | ||
const existsAsync = util_1.promisify(fs_1.exists); | ||
String.prototype.format = function (...args) { | ||
@@ -14,2 +18,12 @@ return this.replace(/{(\d+)}/g, (m, i) => args[i]); | ||
}; | ||
function ellipsis(str, length) { | ||
if (str.length > length) { | ||
const leftPart = str.substring(0, length - 9); | ||
const rightPart = str.substring(str.length - 6); | ||
return `${leftPart}...${rightPart}`; | ||
} | ||
else | ||
return str; | ||
} | ||
exports.ellipsis = ellipsis; | ||
function getChildValue(object, path) { | ||
@@ -52,38 +66,33 @@ return path | ||
const cache = new Map(); | ||
return tinspector_1.useCache(cache, fn, getKey); | ||
return reflect_1.useCache(cache, fn, getKey); | ||
} | ||
exports.memoize = memoize; | ||
// --------------------------------------------------------------------- // | ||
// ------------------------------ TESTING ------------------------------ // | ||
// --------------------------------------------------------------------- // | ||
const log = console.log; | ||
var consoleLog; | ||
(function (consoleLog) { | ||
function startMock() { | ||
const fn = jest.fn(message => { }); | ||
console.log = fn; | ||
return fn; | ||
} | ||
consoleLog.startMock = startMock; | ||
function clearMock() { | ||
console.log = log; | ||
} | ||
consoleLog.clearMock = clearMock; | ||
})(consoleLog || (consoleLog = {})); | ||
exports.consoleLog = consoleLog; | ||
// --------------------------------------------------------------------- // | ||
// ---------------------------- FILE SYSTEM ---------------------------- // | ||
// --------------------------------------------------------------------- // | ||
function findFilesRecursive(path) { | ||
const removeExtension = (x) => x.replace(/\.[^/.]+$/, ""); | ||
if (fs_1.lstatSync(path).isDirectory()) { | ||
const files = glob_1.default.sync(`${path}/**/*+(.js|.ts)`) | ||
//take only file in extension list | ||
.filter(x => [".js", ".ts"].some(ext => path_1.extname(x) == ext)) | ||
//add root path + file name | ||
.map(x => removeExtension(x)); | ||
return Array.from(new Set(files)); | ||
function removeExtension(x) { | ||
return x.replace(/\.[^/.]+$/, ""); | ||
} | ||
function globAsync(path, opts) { | ||
return new Promise((resolve) => { | ||
glob_1.default(path, Object.assign({}, opts), (e, match) => resolve(match)); | ||
}); | ||
} | ||
exports.globAsync = globAsync; | ||
async function traverseDirectory(path) { | ||
const dirs = await globAsync(path, { nodir: true }); | ||
const files = dirs.map(x => removeExtension(x)); | ||
return Array.from(new Set(files)); | ||
} | ||
async function findFilesRecursive(path) { | ||
// if file / directory provided | ||
if (await existsAsync(path)) { | ||
if ((await lstatAsync(path)).isDirectory()) { | ||
return traverseDirectory(`${path}/**/*.{ts,js}`); | ||
} | ||
else | ||
return [removeExtension(path)]; | ||
} | ||
else | ||
return [path]; | ||
// else check if glob provided | ||
return traverseDirectory(path); | ||
} | ||
@@ -127,6 +136,89 @@ exports.findFilesRecursive = findFilesRecursive; | ||
exports.printTable = printTable; | ||
function cleanupConsole(mocks) { | ||
const cleanup = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; | ||
return mocks.map(x => x.map(y => y.replace(cleanup, ""))); | ||
function analyzeModel(type, ctx = { path: [], parentPath: [] }) { | ||
const parentType = ctx.parentPath[ctx.parentPath.length - 1]; | ||
const propName = ctx.path[ctx.path.length - 1]; | ||
const location = `${parentType === null || parentType === void 0 ? void 0 : parentType.name}.${propName}`; | ||
if (Array.isArray(type)) { | ||
if (type[0] === Object) | ||
return [{ location, issue: "ArrayTypeMissing" }]; | ||
return analyzeModel(type[0], ctx); | ||
} | ||
if (isCustomClass(type)) { | ||
// CIRCULAR: check if type already in path, skip immediately | ||
if (ctx.parentPath.some(x => x === type)) | ||
return []; | ||
const meta = reflect_1.default(type); | ||
if (meta.properties.length === 0) | ||
return [{ location: type.name, issue: "NoProperties" }]; | ||
const result = []; | ||
for (const prop of meta.properties) { | ||
const path = ctx.path.concat(prop.name); | ||
const typePath = ctx.parentPath.concat(type); | ||
const msgs = analyzeModel(prop.type, Object.assign(Object.assign({}, ctx), { path, parentPath: typePath })); | ||
result.push(...msgs); | ||
} | ||
return result; | ||
} | ||
if (type === Object) | ||
return [{ location, issue: "TypeMissing" }]; | ||
return []; | ||
} | ||
exports.cleanupConsole = cleanupConsole; | ||
exports.analyzeModel = analyzeModel; | ||
var entityHelper; | ||
(function (entityHelper) { | ||
function getIdProp(entity) { | ||
const meta = reflect_1.default(entity); | ||
for (const prop of meta.properties) { | ||
const decorator = prop.decorators.find((x) => x.kind === "plumier-meta:entity-id"); | ||
if (decorator) | ||
return prop; | ||
} | ||
} | ||
entityHelper.getIdProp = getIdProp; | ||
function getIdType(entity) { | ||
const prop = getIdProp(entity); | ||
return prop === null || prop === void 0 ? void 0 : prop.type; | ||
} | ||
entityHelper.getIdType = getIdType; | ||
function getRelationInfo([entity, relation]) { | ||
const meta = reflect_1.default(entity); | ||
const prop = meta.properties.find(x => x.name === relation); | ||
if (!prop) | ||
throw new Error(`${entity.name} doesn't have property named ${relation}`); | ||
if (prop.type === Array && !prop.type[0]) | ||
throw new Error(types_1.errorMessage.GenericControllerMissingTypeInfo.format(`${entity.name}.${relation}`)); | ||
const type = Array.isArray(prop.type) ? "OneToMany" : "ManyToOne"; | ||
if (type === "OneToMany") { | ||
const relDecorator = prop.decorators.find((x) => x.kind === "plumier-meta:relation"); | ||
if (!relDecorator) | ||
throw new Error(`${entity.name}.${relation} is not a valid relation, make sure its decorated with @entity.relation() decorator`); | ||
const child = prop.type[0]; | ||
return { | ||
type, parent: entity, child, | ||
parentProperty: relation, | ||
childProperty: relDecorator.inverseProperty, | ||
}; | ||
} | ||
else { | ||
const parent = prop.type; | ||
const parentMeta = reflect_1.default(parent); | ||
let parentProperty; | ||
for (const prop of parentMeta.properties) { | ||
const relDecorator = prop.decorators.find((x) => x.kind === "plumier-meta:relation"); | ||
if (!relDecorator) | ||
continue; | ||
if (relDecorator.inverseProperty === relation) { | ||
parentProperty = prop.name; | ||
break; | ||
} | ||
} | ||
return { | ||
type, parent, child: entity, | ||
childProperty: relation, | ||
parentProperty | ||
}; | ||
} | ||
} | ||
entityHelper.getRelationInfo = getRelationInfo; | ||
})(entityHelper || (entityHelper = {})); | ||
exports.entityHelper = entityHelper; |
"use strict"; | ||
//gist https://gist.github.com/RWOverdijk/6cef816cfdf5722228e01cc05fd4b094 | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.HttpStatus = void 0; | ||
/** | ||
@@ -5,0 +6,0 @@ * Hypertext Transfer Protocol (HTTP) response status codes. |
@@ -1,20 +0,23 @@ | ||
import { val } from "typedconverter"; | ||
import "./decorator.val"; | ||
import { val } from "@plumier/validator"; | ||
import "./decorator/val"; | ||
export { val }; | ||
export { AuthorizerFunction, checkAuthorize, RoleField, Authorizer, CustomAuthorizer, CustomAuthorizerFunction, AuthorizationContext, AuthorizerContext } from "./authorization"; | ||
export { AuthorizerFunction, checkAuthorize, Authorizer, CustomAuthorizer, CustomAuthorizerFunction, AuthorizationContext, AuthorizerContext, AuthorizeDecorator, authPolicy, entityPolicy, EntityPolicyAuthorizerFunction, PolicyAuthorizer, Public, Authenticated, AuthPolicy, CustomAuthPolicy, EntityAuthPolicy, EntityProviderQuery, EntityPolicyProviderDecorator, globalPolicies, PublicAuthPolicy, AuthenticatedAuthPolicy, ReadonlyAuthPolicy, WriteonlyAuthPolicy, executeAuthorizer, createAuthContext, throwAuthError, getRouteAuthorizeDecorators } from "./authorization"; | ||
export { updateRouteAuthorizationAccess, analyzeAuthPolicyNameConflict, createAuthorizationAnalyzer, getPolicyInfo } from "./authorization-analyzer"; | ||
export { HeaderPart, RequestPart, BindingDecorator, binder, ParameterBinderMiddleware, CustomBinderFunction } from "./binder"; | ||
export { invoke } from "./application-pipeline"; | ||
export { response } from "./response"; | ||
export { generateRoutes } from "./route-generator"; | ||
export { generateRoutes, findClassRecursive, appendRoute, IgnoreDecorator, RouteDecorator, transformController, ControllerTransformOption } from "./route-generator"; | ||
export { analyzeRoutes, printAnalysis } from "./route-analyzer"; | ||
export { router } from "./router"; | ||
export { Class, consoleLog, findFilesRecursive, getChildValue, hasKeyOf, isCustomClass, printTable, toBoolean, cleanupConsole } from "./common"; | ||
export { AuthDecoratorImpl, authorize } from "./decorator.authorize"; | ||
export { bind } from "./decorator.bind"; | ||
export { domain, middleware } from "./decorator"; | ||
export { route, RouteDecoratorImpl } from "./decorator.route"; | ||
export { rest, RestDecoratorImpl } from "./decorator.rest"; | ||
export { Class, findFilesRecursive, getChildValue, hasKeyOf, isCustomClass, printTable, toBoolean, ellipsis, analyzeModel, AnalysisMessage, entityHelper, globAsync, EntityRelationInfo, OneToManyRelationInfo, ManyToOneRelationInfo } from "./common"; | ||
export { AuthDecoratorImpl, authorize } from "./decorator/authorize"; | ||
export { ApiDescriptionDecorator, ApiEnumDecorator, ApiFieldNameDecorator, ApiRequiredDecorator, ApiResponseDecorator, ApiTagDecorator, api, ApiReadOnlyDecorator, ApiWriteOnlyDecorator, ApiHideRelationDecorator } from "./decorator/api"; | ||
export { bind } from "./decorator/bind"; | ||
export { domain, middleware, entityProvider, responseType, ResponseTypeDecorator } from "./decorator/common"; | ||
export { route, RouteDecoratorImpl } from "./decorator/route"; | ||
export { EntityIdDecorator, RelationDecorator, entity, DeleteColumnDecorator } from "./decorator/entity"; | ||
export { preSave, postSave, RequestHookDecorator } from "./decorator/request-hook"; | ||
export { meta } from "./decorator/meta"; | ||
export { HttpStatus } from "./http-status"; | ||
export { validate, ValidatorMiddleware } from "./validator"; | ||
export { printVirtualRoutes } from "./route-virtual"; | ||
export { ActionResult, Application, Configuration, DefaultFacility, CustomValidator, DependencyResolver, Facility, FileParser, FileUploadInfo, HttpMethod, HttpStatusError, Invocation, KoaMiddleware, Middleware, MiddlewareFunction, MiddlewareDecorator, MiddlewareUtil, PlumierApplication, PlumierConfiguration, RedirectActionResult, ActionContext, RouteInfo, RouteAnalyzerFunction, RouteAnalyzerIssue, ValidatorDecorator, CustomValidatorFunction, ValidatorContext, ValidationError, errorMessage, AsyncValidatorResult, DefaultDependencyResolver, CustomMiddleware, CustomMiddlewareFunction, FormFile, HttpCookie, VirtualRouteInfo, Metadata, GlobalMetadata, Omit, Optional } from "./types"; | ||
export { validate, ValidatorMiddleware, CustomValidator, ValidatorDecorator, CustomValidatorFunction, AsyncValidatorResult, ValidatorContext, } from "./validator"; | ||
export { ActionResult, Application, Configuration, DefaultFacility, DependencyResolver, Facility, HttpMethod, HttpStatusError, Invocation, KoaMiddleware, Middleware, MiddlewareFunction, MiddlewareDecorator, MiddlewareUtil, PlumierApplication, PlumierConfiguration, RedirectActionResult, ActionContext, RouteInfo, RouteAnalyzerFunction, RouteAnalyzerIssue, ValidationError, errorMessage, DefaultDependencyResolver, CustomConverter, CustomMiddleware, CustomMiddlewareFunction, FormFile, HttpCookie, KeyOf, Metadata, GlobalMetadata, Optional, RouteMetadata, VirtualRoute, GenericControllers, ControllerGeneric, NestedControllerGeneric, Repository, NestedRepository, FilterQueryType, NestedGenericControllerDecorator, MetadataImpl, JwtClaims, SelectQuery } from "./types"; |
136
lib/index.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const typedconverter_1 = require("typedconverter"); | ||
exports.val = typedconverter_1.val; | ||
require("./decorator.val"); | ||
exports.entityProvider = exports.middleware = exports.domain = exports.bind = exports.api = exports.authorize = exports.AuthDecoratorImpl = exports.globAsync = exports.entityHelper = exports.analyzeModel = exports.ellipsis = exports.toBoolean = exports.printTable = exports.isCustomClass = exports.hasKeyOf = exports.getChildValue = exports.findFilesRecursive = exports.router = exports.printAnalysis = exports.analyzeRoutes = exports.transformController = exports.appendRoute = exports.findClassRecursive = exports.generateRoutes = exports.response = exports.invoke = exports.ParameterBinderMiddleware = exports.binder = exports.getPolicyInfo = exports.createAuthorizationAnalyzer = exports.analyzeAuthPolicyNameConflict = exports.updateRouteAuthorizationAccess = exports.getRouteAuthorizeDecorators = exports.throwAuthError = exports.createAuthContext = exports.executeAuthorizer = exports.WriteonlyAuthPolicy = exports.ReadonlyAuthPolicy = exports.AuthenticatedAuthPolicy = exports.PublicAuthPolicy = exports.globalPolicies = exports.EntityAuthPolicy = exports.CustomAuthPolicy = exports.Authenticated = exports.Public = exports.PolicyAuthorizer = exports.entityPolicy = exports.authPolicy = exports.checkAuthorize = exports.val = void 0; | ||
exports.MetadataImpl = exports.NestedControllerGeneric = exports.ControllerGeneric = exports.FormFile = exports.DefaultDependencyResolver = exports.errorMessage = exports.ValidationError = exports.RedirectActionResult = exports.MiddlewareUtil = exports.HttpStatusError = exports.DefaultFacility = exports.ActionResult = exports.ValidatorMiddleware = exports.validate = exports.HttpStatus = exports.meta = exports.postSave = exports.preSave = exports.entity = exports.RouteDecoratorImpl = exports.route = exports.responseType = void 0; | ||
// TypeScript bug https://github.com/microsoft/TypeScript/issues/18877 | ||
const validator_1 = require("@plumier/validator"); | ||
Object.defineProperty(exports, "val", { enumerable: true, get: function () { return validator_1.val; } }); | ||
require("./decorator/val"); | ||
var authorization_1 = require("./authorization"); | ||
exports.checkAuthorize = authorization_1.checkAuthorize; | ||
Object.defineProperty(exports, "checkAuthorize", { enumerable: true, get: function () { return authorization_1.checkAuthorize; } }); | ||
Object.defineProperty(exports, "authPolicy", { enumerable: true, get: function () { return authorization_1.authPolicy; } }); | ||
Object.defineProperty(exports, "entityPolicy", { enumerable: true, get: function () { return authorization_1.entityPolicy; } }); | ||
Object.defineProperty(exports, "PolicyAuthorizer", { enumerable: true, get: function () { return authorization_1.PolicyAuthorizer; } }); | ||
Object.defineProperty(exports, "Public", { enumerable: true, get: function () { return authorization_1.Public; } }); | ||
Object.defineProperty(exports, "Authenticated", { enumerable: true, get: function () { return authorization_1.Authenticated; } }); | ||
Object.defineProperty(exports, "CustomAuthPolicy", { enumerable: true, get: function () { return authorization_1.CustomAuthPolicy; } }); | ||
Object.defineProperty(exports, "EntityAuthPolicy", { enumerable: true, get: function () { return authorization_1.EntityAuthPolicy; } }); | ||
Object.defineProperty(exports, "globalPolicies", { enumerable: true, get: function () { return authorization_1.globalPolicies; } }); | ||
Object.defineProperty(exports, "PublicAuthPolicy", { enumerable: true, get: function () { return authorization_1.PublicAuthPolicy; } }); | ||
Object.defineProperty(exports, "AuthenticatedAuthPolicy", { enumerable: true, get: function () { return authorization_1.AuthenticatedAuthPolicy; } }); | ||
Object.defineProperty(exports, "ReadonlyAuthPolicy", { enumerable: true, get: function () { return authorization_1.ReadonlyAuthPolicy; } }); | ||
Object.defineProperty(exports, "WriteonlyAuthPolicy", { enumerable: true, get: function () { return authorization_1.WriteonlyAuthPolicy; } }); | ||
Object.defineProperty(exports, "executeAuthorizer", { enumerable: true, get: function () { return authorization_1.executeAuthorizer; } }); | ||
Object.defineProperty(exports, "createAuthContext", { enumerable: true, get: function () { return authorization_1.createAuthContext; } }); | ||
Object.defineProperty(exports, "throwAuthError", { enumerable: true, get: function () { return authorization_1.throwAuthError; } }); | ||
Object.defineProperty(exports, "getRouteAuthorizeDecorators", { enumerable: true, get: function () { return authorization_1.getRouteAuthorizeDecorators; } }); | ||
var authorization_analyzer_1 = require("./authorization-analyzer"); | ||
Object.defineProperty(exports, "updateRouteAuthorizationAccess", { enumerable: true, get: function () { return authorization_analyzer_1.updateRouteAuthorizationAccess; } }); | ||
Object.defineProperty(exports, "analyzeAuthPolicyNameConflict", { enumerable: true, get: function () { return authorization_analyzer_1.analyzeAuthPolicyNameConflict; } }); | ||
Object.defineProperty(exports, "createAuthorizationAnalyzer", { enumerable: true, get: function () { return authorization_analyzer_1.createAuthorizationAnalyzer; } }); | ||
Object.defineProperty(exports, "getPolicyInfo", { enumerable: true, get: function () { return authorization_analyzer_1.getPolicyInfo; } }); | ||
var binder_1 = require("./binder"); | ||
exports.binder = binder_1.binder; | ||
exports.ParameterBinderMiddleware = binder_1.ParameterBinderMiddleware; | ||
Object.defineProperty(exports, "binder", { enumerable: true, get: function () { return binder_1.binder; } }); | ||
Object.defineProperty(exports, "ParameterBinderMiddleware", { enumerable: true, get: function () { return binder_1.ParameterBinderMiddleware; } }); | ||
var application_pipeline_1 = require("./application-pipeline"); | ||
exports.invoke = application_pipeline_1.invoke; | ||
Object.defineProperty(exports, "invoke", { enumerable: true, get: function () { return application_pipeline_1.invoke; } }); | ||
var response_1 = require("./response"); | ||
exports.response = response_1.response; | ||
Object.defineProperty(exports, "response", { enumerable: true, get: function () { return response_1.response; } }); | ||
var route_generator_1 = require("./route-generator"); | ||
exports.generateRoutes = route_generator_1.generateRoutes; | ||
Object.defineProperty(exports, "generateRoutes", { enumerable: true, get: function () { return route_generator_1.generateRoutes; } }); | ||
Object.defineProperty(exports, "findClassRecursive", { enumerable: true, get: function () { return route_generator_1.findClassRecursive; } }); | ||
Object.defineProperty(exports, "appendRoute", { enumerable: true, get: function () { return route_generator_1.appendRoute; } }); | ||
Object.defineProperty(exports, "transformController", { enumerable: true, get: function () { return route_generator_1.transformController; } }); | ||
var route_analyzer_1 = require("./route-analyzer"); | ||
exports.analyzeRoutes = route_analyzer_1.analyzeRoutes; | ||
exports.printAnalysis = route_analyzer_1.printAnalysis; | ||
Object.defineProperty(exports, "analyzeRoutes", { enumerable: true, get: function () { return route_analyzer_1.analyzeRoutes; } }); | ||
Object.defineProperty(exports, "printAnalysis", { enumerable: true, get: function () { return route_analyzer_1.printAnalysis; } }); | ||
var router_1 = require("./router"); | ||
exports.router = router_1.router; | ||
Object.defineProperty(exports, "router", { enumerable: true, get: function () { return router_1.router; } }); | ||
var common_1 = require("./common"); | ||
exports.consoleLog = common_1.consoleLog; | ||
exports.findFilesRecursive = common_1.findFilesRecursive; | ||
exports.getChildValue = common_1.getChildValue; | ||
exports.hasKeyOf = common_1.hasKeyOf; | ||
exports.isCustomClass = common_1.isCustomClass; | ||
exports.printTable = common_1.printTable; | ||
exports.toBoolean = common_1.toBoolean; | ||
exports.cleanupConsole = common_1.cleanupConsole; | ||
var decorator_authorize_1 = require("./decorator.authorize"); | ||
exports.AuthDecoratorImpl = decorator_authorize_1.AuthDecoratorImpl; | ||
exports.authorize = decorator_authorize_1.authorize; | ||
var decorator_bind_1 = require("./decorator.bind"); | ||
exports.bind = decorator_bind_1.bind; | ||
var decorator_1 = require("./decorator"); | ||
exports.domain = decorator_1.domain; | ||
exports.middleware = decorator_1.middleware; | ||
var decorator_route_1 = require("./decorator.route"); | ||
exports.route = decorator_route_1.route; | ||
exports.RouteDecoratorImpl = decorator_route_1.RouteDecoratorImpl; | ||
var decorator_rest_1 = require("./decorator.rest"); | ||
exports.rest = decorator_rest_1.rest; | ||
exports.RestDecoratorImpl = decorator_rest_1.RestDecoratorImpl; | ||
Object.defineProperty(exports, "findFilesRecursive", { enumerable: true, get: function () { return common_1.findFilesRecursive; } }); | ||
Object.defineProperty(exports, "getChildValue", { enumerable: true, get: function () { return common_1.getChildValue; } }); | ||
Object.defineProperty(exports, "hasKeyOf", { enumerable: true, get: function () { return common_1.hasKeyOf; } }); | ||
Object.defineProperty(exports, "isCustomClass", { enumerable: true, get: function () { return common_1.isCustomClass; } }); | ||
Object.defineProperty(exports, "printTable", { enumerable: true, get: function () { return common_1.printTable; } }); | ||
Object.defineProperty(exports, "toBoolean", { enumerable: true, get: function () { return common_1.toBoolean; } }); | ||
Object.defineProperty(exports, "ellipsis", { enumerable: true, get: function () { return common_1.ellipsis; } }); | ||
Object.defineProperty(exports, "analyzeModel", { enumerable: true, get: function () { return common_1.analyzeModel; } }); | ||
Object.defineProperty(exports, "entityHelper", { enumerable: true, get: function () { return common_1.entityHelper; } }); | ||
Object.defineProperty(exports, "globAsync", { enumerable: true, get: function () { return common_1.globAsync; } }); | ||
var authorize_1 = require("./decorator/authorize"); | ||
Object.defineProperty(exports, "AuthDecoratorImpl", { enumerable: true, get: function () { return authorize_1.AuthDecoratorImpl; } }); | ||
Object.defineProperty(exports, "authorize", { enumerable: true, get: function () { return authorize_1.authorize; } }); | ||
var api_1 = require("./decorator/api"); | ||
Object.defineProperty(exports, "api", { enumerable: true, get: function () { return api_1.api; } }); | ||
var bind_1 = require("./decorator/bind"); | ||
Object.defineProperty(exports, "bind", { enumerable: true, get: function () { return bind_1.bind; } }); | ||
var common_2 = require("./decorator/common"); | ||
Object.defineProperty(exports, "domain", { enumerable: true, get: function () { return common_2.domain; } }); | ||
Object.defineProperty(exports, "middleware", { enumerable: true, get: function () { return common_2.middleware; } }); | ||
Object.defineProperty(exports, "entityProvider", { enumerable: true, get: function () { return common_2.entityProvider; } }); | ||
Object.defineProperty(exports, "responseType", { enumerable: true, get: function () { return common_2.responseType; } }); | ||
var route_1 = require("./decorator/route"); | ||
Object.defineProperty(exports, "route", { enumerable: true, get: function () { return route_1.route; } }); | ||
Object.defineProperty(exports, "RouteDecoratorImpl", { enumerable: true, get: function () { return route_1.RouteDecoratorImpl; } }); | ||
var entity_1 = require("./decorator/entity"); | ||
Object.defineProperty(exports, "entity", { enumerable: true, get: function () { return entity_1.entity; } }); | ||
var request_hook_1 = require("./decorator/request-hook"); | ||
Object.defineProperty(exports, "preSave", { enumerable: true, get: function () { return request_hook_1.preSave; } }); | ||
Object.defineProperty(exports, "postSave", { enumerable: true, get: function () { return request_hook_1.postSave; } }); | ||
var meta_1 = require("./decorator/meta"); | ||
Object.defineProperty(exports, "meta", { enumerable: true, get: function () { return meta_1.meta; } }); | ||
var http_status_1 = require("./http-status"); | ||
exports.HttpStatus = http_status_1.HttpStatus; | ||
var validator_1 = require("./validator"); | ||
exports.validate = validator_1.validate; | ||
exports.ValidatorMiddleware = validator_1.ValidatorMiddleware; | ||
var route_virtual_1 = require("./route-virtual"); | ||
exports.printVirtualRoutes = route_virtual_1.printVirtualRoutes; | ||
Object.defineProperty(exports, "HttpStatus", { enumerable: true, get: function () { return http_status_1.HttpStatus; } }); | ||
var validator_2 = require("./validator"); | ||
Object.defineProperty(exports, "validate", { enumerable: true, get: function () { return validator_2.validate; } }); | ||
Object.defineProperty(exports, "ValidatorMiddleware", { enumerable: true, get: function () { return validator_2.ValidatorMiddleware; } }); | ||
var types_1 = require("./types"); | ||
exports.ActionResult = types_1.ActionResult; | ||
exports.DefaultFacility = types_1.DefaultFacility; | ||
exports.HttpStatusError = types_1.HttpStatusError; | ||
exports.MiddlewareUtil = types_1.MiddlewareUtil; | ||
exports.RedirectActionResult = types_1.RedirectActionResult; | ||
exports.ValidationError = types_1.ValidationError; | ||
exports.errorMessage = types_1.errorMessage; | ||
exports.DefaultDependencyResolver = types_1.DefaultDependencyResolver; | ||
exports.FormFile = types_1.FormFile; | ||
Object.defineProperty(exports, "ActionResult", { enumerable: true, get: function () { return types_1.ActionResult; } }); | ||
Object.defineProperty(exports, "DefaultFacility", { enumerable: true, get: function () { return types_1.DefaultFacility; } }); | ||
Object.defineProperty(exports, "HttpStatusError", { enumerable: true, get: function () { return types_1.HttpStatusError; } }); | ||
Object.defineProperty(exports, "MiddlewareUtil", { enumerable: true, get: function () { return types_1.MiddlewareUtil; } }); | ||
Object.defineProperty(exports, "RedirectActionResult", { enumerable: true, get: function () { return types_1.RedirectActionResult; } }); | ||
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return types_1.ValidationError; } }); | ||
Object.defineProperty(exports, "errorMessage", { enumerable: true, get: function () { return types_1.errorMessage; } }); | ||
Object.defineProperty(exports, "DefaultDependencyResolver", { enumerable: true, get: function () { return types_1.DefaultDependencyResolver; } }); | ||
Object.defineProperty(exports, "FormFile", { enumerable: true, get: function () { return types_1.FormFile; } }); | ||
Object.defineProperty(exports, "ControllerGeneric", { enumerable: true, get: function () { return types_1.ControllerGeneric; } }); | ||
Object.defineProperty(exports, "NestedControllerGeneric", { enumerable: true, get: function () { return types_1.NestedControllerGeneric; } }); | ||
Object.defineProperty(exports, "MetadataImpl", { enumerable: true, get: function () { return types_1.MetadataImpl; } }); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.response = void 0; | ||
const types_1 = require("./types"); | ||
@@ -4,0 +5,0 @@ var response; |
@@ -1,8 +0,8 @@ | ||
import { Configuration, RouteAnalyzerIssue, RouteInfo } from "./types"; | ||
import { Configuration, RouteAnalyzerIssue, RouteMetadata } from "./types"; | ||
interface TestResult { | ||
route: RouteInfo; | ||
route: RouteMetadata; | ||
issues: RouteAnalyzerIssue[]; | ||
} | ||
declare function analyzeRoutes(routes: RouteInfo[], config: Configuration): TestResult[]; | ||
declare function analyzeRoutes(routes: RouteMetadata[], config: Configuration): TestResult[]; | ||
declare function printAnalysis(results: TestResult[]): void; | ||
export { analyzeRoutes, printAnalysis }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.printAnalysis = exports.analyzeRoutes = void 0; | ||
const tslib_1 = require("tslib"); | ||
const chalk_1 = tslib_1.__importDefault(require("chalk")); | ||
const tinspector_1 = require("tinspector"); | ||
const authorization_1 = require("./authorization"); | ||
const common_1 = require("./common"); | ||
@@ -12,89 +11,58 @@ const types_1 = require("./types"); | ||
/* ------------------------------------------------------------------------------- */ | ||
//------ Analyzer Helpers | ||
function getModelsInParameters(par) { | ||
return par | ||
.map((x, i) => ({ type: x.type, index: i })) | ||
.filter(x => x.type && common_1.isCustomClass(x.type)) | ||
.map(x => ({ meta: tinspector_1.reflect((Array.isArray(x.type) ? x.type[0] : x.type)), index: x.index })); | ||
} | ||
function traverseModel(par) { | ||
const models = getModelsInParameters(par).map(x => x.meta); | ||
const child = models.map(x => traverseModel(x.properties)) | ||
.filter((x) => Boolean(x)) | ||
.reduce((a, b) => a.concat(b), []); | ||
return models.concat(child); | ||
} | ||
function traverseArray(parent, par) { | ||
const models = getModelsInParameters(par); | ||
if (models.length > 0) { | ||
return models.map((x, i) => traverseArray(x.meta.name, x.meta.properties)) | ||
.flatten(); | ||
} | ||
return par.filter(x => Array.isArray(x.type) && x.type[0] === Object) | ||
.map(x => `${parent}.${x.name}`); | ||
} | ||
function backingParameterTest(route, allRoutes) { | ||
if (route.kind === "VirtualRoute") | ||
return [{ type: "success" }]; | ||
const ids = route.url.split("/") | ||
.filter(x => x.startsWith(":")) | ||
.map(x => x.substring(1).toLowerCase()); | ||
const missing = ids.filter(id => route.action.parameters.map(x => x.name.toLowerCase()).indexOf(id) === -1); | ||
const missing = ids.filter(id => route.action.parameters.map(x => route.paramMapper.alias(x.name).toLowerCase()).indexOf(id) === -1); | ||
if (missing.length > 0) { | ||
return { | ||
type: "error", | ||
message: types_1.errorMessage.RouteDoesNotHaveBackingParam.format(missing.join(", ")) | ||
}; | ||
return [{ | ||
type: "error", | ||
message: types_1.errorMessage.RouteDoesNotHaveBackingParam.format(missing.join(", ")) | ||
}]; | ||
} | ||
else | ||
return { type: "success" }; | ||
return [{ type: "success" }]; | ||
} | ||
function metadataTypeTest(route, allRoutes) { | ||
const hasTypeInfo = route.action | ||
.parameters.some(x => Boolean(x.type)); | ||
if (!hasTypeInfo && route.action.parameters.length > 0) { | ||
return { | ||
type: "warning", | ||
message: types_1.errorMessage.ActionDoesNotHaveTypeInfo | ||
}; | ||
} | ||
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: types_1.errorMessage.DuplicateRouteFound.format(dup.map(x => getActionName(x)).join(" ")) | ||
}; | ||
return [{ | ||
type: "error", | ||
message: types_1.errorMessage.DuplicateRouteFound.format(dup.map(x => getActionName(x)).join(" ")) | ||
}]; | ||
} | ||
else | ||
return { type: "success" }; | ||
return [{ type: "success" }]; | ||
} | ||
function modelTypeInfoTest(route, allRoutes) { | ||
const classes = traverseModel(route.action.parameters) | ||
.filter(x => x.properties.every(par => typeof par.type == "undefined")) | ||
.map(x => x.type); | ||
//get only unique type | ||
const noTypeInfo = Array.from(new Set(classes)); | ||
if (noTypeInfo.length > 0) { | ||
return { | ||
type: "warning", | ||
message: types_1.errorMessage.ModelWithoutTypeInformation.format(noTypeInfo.map(x => x.name).join(", ")) | ||
}; | ||
function typeInfoTest(route, allRoutes) { | ||
const toRouteAnalyzerIssue = (x) => { | ||
if (x.issue === "NoProperties") | ||
return { type: "warning", message: types_1.errorMessage.ModelWithoutTypeInformation.format(x.location) }; | ||
if (x.issue === "ArrayTypeMissing") | ||
return { type: "warning", message: types_1.errorMessage.ArrayWithoutTypeInformation.format(x.location) }; | ||
return { type: "warning", message: types_1.errorMessage.PropertyWithoutTypeInformation.format(x.location) }; | ||
}; | ||
if (route.kind === "VirtualRoute") | ||
return [{ type: "success" }]; | ||
const issues = []; | ||
for (const prop of route.action.parameters) { | ||
if (prop.typeClassification === "Primitive") | ||
continue; | ||
const location = `${route.controller.name}.${route.action.name}.${prop.name}`; | ||
if (prop.type === Object || prop.type === undefined) | ||
issues.push({ type: "warning", message: types_1.errorMessage.ActionParameterDoesNotHaveTypeInfo.format(location) }); | ||
else if (Array.isArray(prop.type) && prop.type[0] === Object) | ||
issues.push({ type: "warning", message: types_1.errorMessage.ActionParameterDoesNotHaveTypeInfo.format(location) }); | ||
else { | ||
const iss = common_1.analyzeModel(prop.type).map(x => toRouteAnalyzerIssue(x)); | ||
issues.push(...iss); | ||
} | ||
} | ||
if (issues.length > 0) | ||
return issues; | ||
else | ||
return { type: "success" }; | ||
return [{ type: "success" }]; | ||
} | ||
function arrayTypeInfoTest(route, allRoutes) { | ||
const issues = traverseArray(`${route.controller.name}.${route.action.name}`, route.action.parameters); | ||
const array = Array.from(new Set(issues)); | ||
if (array.length > 0) { | ||
return { | ||
type: "warning", | ||
message: types_1.errorMessage.ArrayWithoutTypeInformation.format(array.join(", ")) | ||
}; | ||
} | ||
else | ||
return { type: 'success' }; | ||
} | ||
/* ------------------------------------------------------------------------------- */ | ||
@@ -104,6 +72,25 @@ /* -------------------------------- ANALYZER ------------------------------------- */ | ||
function getActionName(route) { | ||
return `${route.controller.name}.${route.action.name}(${route.action.parameters.map(x => x.name).join(", ")})`; | ||
if (route.kind === "ActionRoute") | ||
return `${route.controller.name}.${route.action.name}(${route.action.parameters.map(x => x.name).join(", ")})`; | ||
else | ||
return `${route.provider.name}`; | ||
} | ||
function getActionNameForReport(route) { | ||
const origin = getActionName(route); | ||
if (route.kind === "ActionRoute") { | ||
if (origin.length > 40) | ||
return `${common_1.ellipsis(route.controller.name, 25)}.${common_1.ellipsis(route.action.name, 15)}`; | ||
else | ||
return origin; | ||
} | ||
else { | ||
return common_1.ellipsis(origin, 40); | ||
} | ||
} | ||
function analyzeRoute(route, tests, allRoutes) { | ||
const issues = tests.map(test => test(route, allRoutes)).filter(x => x.type != "success"); | ||
const issues = []; | ||
for (const test of tests) { | ||
const result = test(route, allRoutes).filter(x => x.type !== "success"); | ||
issues.push(...result); | ||
} | ||
return { route, issues }; | ||
@@ -113,7 +100,4 @@ } | ||
const tests = [ | ||
backingParameterTest, metadataTypeTest, | ||
duplicateRouteTest, modelTypeInfoTest, | ||
arrayTypeInfoTest | ||
backingParameterTest, duplicateRouteTest, typeInfoTest, | ||
]; | ||
authorization_1.updateRouteAuthorizationAccess(routes, config); | ||
return routes.map(x => analyzeRoute(x, tests.concat(config.analyzers || []), routes)); | ||
@@ -123,13 +107,25 @@ } | ||
function printAnalysis(results) { | ||
console.log(); | ||
console.log("Route Analysis Report"); | ||
if (results.length == 0) | ||
console.log("No controller found"); | ||
const group = results.reduce((prev, cur) => { | ||
var _a, _b; | ||
const key = (_a = cur.route.group) !== null && _a !== void 0 ? _a : "___default___"; | ||
prev[key] = ((_b = prev[key]) !== null && _b !== void 0 ? _b : []).concat(cur); | ||
return prev; | ||
}, {}); | ||
for (const key in group) { | ||
printRoutes(group[key]); | ||
} | ||
} | ||
exports.printAnalysis = printAnalysis; | ||
function printRoutes(results) { | ||
const data = results.map(x => { | ||
const method = x.route.method.toUpperCase(); | ||
const action = getActionName(x.route); | ||
const action = getActionNameForReport(x.route); | ||
const issues = x.issues.map(issue => ` - ${issue.type} ${issue.message}`); | ||
return { method, url: x.route.url, action, issues, access: x.route.access }; | ||
return { method, url: common_1.ellipsis(x.route.url, 60), action, issues, access: x.route.access }; | ||
}); | ||
const hasAccess = data.every(x => !!x.access); | ||
console.log(); | ||
console.log("Route Analysis Report"); | ||
if (data.length == 0) | ||
console.log("No controller found"); | ||
common_1.printTable([ | ||
@@ -148,5 +144,3 @@ "action", | ||
}); | ||
if (data.length > 0) | ||
console.log(); | ||
console.log(); | ||
} | ||
exports.printAnalysis = printAnalysis; |
import { Class } from "./common"; | ||
import { HttpMethod, RouteInfo } from "./types"; | ||
import { GenericControllers, HttpMethod, RouteInfo, RouteMetadata } from "./types"; | ||
interface RouteDecorator { | ||
name: "Route"; | ||
name: "plumier-meta:route"; | ||
method: HttpMethod; | ||
url?: string; | ||
map: any; | ||
} | ||
interface VirtualRouteDecorator { | ||
name: "VirtualRoute"; | ||
method: HttpMethod; | ||
url: string; | ||
access: string; | ||
} | ||
interface IgnoreDecorator { | ||
name: "Ignore"; | ||
name: "plumier-meta:ignore"; | ||
} | ||
interface RootDecorator { | ||
name: "Root"; | ||
name: "plumier-meta:root"; | ||
url: string; | ||
map: any; | ||
} | ||
declare function generateRoutes(executionPath: string, controller: string | Class[] | Class): RouteInfo[]; | ||
export { generateRoutes, RouteDecorator, IgnoreDecorator, RootDecorator, VirtualRouteDecorator }; | ||
interface ControllerTransformOption { | ||
rootDir: string; | ||
rootPath: string; | ||
group: string; | ||
directoryAsPath: boolean; | ||
genericController?: GenericControllers; | ||
genericControllerNameConversion: (x: string) => string; | ||
} | ||
interface ClassWithRoot { | ||
root: string; | ||
type: Class; | ||
} | ||
declare function appendRoute(...args: string[]): string; | ||
declare function findClassRecursive(path: string): Promise<ClassWithRoot[]>; | ||
declare function transformController(object: Class, opt: ControllerTransformOption): RouteInfo[]; | ||
declare function generateRoutes(controller: string | string[] | Class[] | Class, option?: Partial<ControllerTransformOption>): Promise<RouteMetadata[]>; | ||
export { generateRoutes, transformController, RouteDecorator, IgnoreDecorator, RootDecorator, appendRoute, findClassRecursive, ControllerTransformOption }; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const fs_1 = require("fs"); | ||
exports.findClassRecursive = exports.appendRoute = exports.transformController = exports.generateRoutes = void 0; | ||
const reflect_1 = require("@plumier/reflect"); | ||
const path_1 = require("path"); | ||
const tinspector_1 = require("tinspector"); | ||
const common_1 = require("./common"); | ||
const types_1 = require("./types"); | ||
/* ------------------------------------------------------------------------------- */ | ||
/* ------------------------------- HELPERS --------------------------------------- */ | ||
/* ------------------------------------------------------------------------------- */ | ||
function createRoute(...args) { | ||
function appendRoute(...args) { | ||
return "/" + args | ||
@@ -20,19 +19,58 @@ .filter(x => !!x) | ||
} | ||
exports.appendRoute = appendRoute; | ||
function striveController(name) { | ||
return name.substring(0, name.lastIndexOf("Controller")).toLowerCase(); | ||
return name.replace(/controller$/i, ""); | ||
} | ||
function getControllerRoutes(root, controller) { | ||
const decs = controller.decorators.filter((x) => x.name == "Root"); | ||
function getRootRoutes(root, controller) { | ||
var _a; | ||
const decs = controller.decorators.filter((x) => x.name == "plumier-meta:root"); | ||
if (decs.length > 0) { | ||
return decs.slice().reverse().map(x => transformDecorator(root, "", x)); | ||
const result = []; | ||
for (let i = decs.length; i--;) { | ||
const item = decs[i]; | ||
result.push({ root: transformDecorator(root, "", item), map: (_a = item.map) !== null && _a !== void 0 ? _a : {} }); | ||
} | ||
return result; | ||
} | ||
else { | ||
return [createRoute(root, striveController(controller.name))]; | ||
return [{ root: appendRoute(root, striveController(controller.name)), map: {} }]; | ||
} | ||
} | ||
function getRoot(rootPath, path) { | ||
// directoryAsPath should not working with glob | ||
if (rootPath.indexOf("*") >= 0) | ||
return; | ||
const part = path.slice(rootPath.length).split("/").filter(x => !!x) | ||
.slice(0, -1); | ||
return (part.length === 0) ? undefined : createRoute(...part); | ||
return (part.length === 0) ? undefined : appendRoute(...part); | ||
} | ||
async function findClassRecursive(path) { | ||
var _a; | ||
//read all files and get module reflection | ||
const files = await common_1.findFilesRecursive(path); | ||
const result = []; | ||
for (const file of files) { | ||
const root = (_a = getRoot(path, file)) !== null && _a !== void 0 ? _a : ""; | ||
for (const member of reflect_1.reflect(file).members) { | ||
if (member.kind === "Class") | ||
result.push({ root, type: member.type }); | ||
} | ||
} | ||
return result; | ||
} | ||
exports.findClassRecursive = findClassRecursive; | ||
class ParamMapper { | ||
constructor(map) { | ||
this.map = map; | ||
} | ||
alias(parName) { | ||
let result; | ||
for (const item of this.map) { | ||
result = item[parName]; | ||
if (!!result) | ||
break; | ||
} | ||
return result !== null && result !== void 0 ? result : parName; | ||
} | ||
} | ||
/* ------------------------------------------------------------------------------- */ | ||
@@ -50,36 +88,47 @@ /* ---------------------------------- TRANSFORMER -------------------------------- */ | ||
else { | ||
return createRoute(root, actionDecorator.url || actionName.toLowerCase()); | ||
return appendRoute(root, actionDecorator.url || actionName.toLowerCase()); | ||
} | ||
} | ||
function transformMethodWithDecorator(root, controller, method) { | ||
if (method.decorators.some((x) => x.name == "Ignore")) | ||
function transformMethodWithDecorator(root, controller, method, group) { | ||
var _a; | ||
if (method.decorators.some((x) => x.name == "plumier-meta:ignore")) | ||
return []; | ||
const result = { action: method, controller }; | ||
const result = { kind: "ActionRoute", group, action: method, controller }; | ||
const infos = []; | ||
const rootMap = [root.map]; | ||
for (const decorator of method.decorators.slice().reverse()) { | ||
if (decorator.name === "Route") | ||
infos.push(Object.assign(Object.assign({}, result), { method: decorator.method, url: transformDecorator(root, method.name, decorator) })); | ||
if (decorator.name === "plumier-meta:route") | ||
infos.push(Object.assign(Object.assign({}, result), { method: decorator.method, url: transformDecorator(root.root, method.name, decorator), paramMapper: new ParamMapper(rootMap.concat((_a = decorator.map) !== null && _a !== void 0 ? _a : {})) })); | ||
} | ||
return infos; | ||
} | ||
function transformMethod(root, controller, method) { | ||
function transformMethod(root, controller, method, group) { | ||
return [{ | ||
method: "get", | ||
url: createRoute(root, method.name), | ||
kind: "ActionRoute", | ||
group, method: "get", | ||
url: appendRoute(root.root, method.name), | ||
controller, | ||
action: method, | ||
paramMapper: new ParamMapper([root.map]) | ||
}]; | ||
} | ||
function isController(controller) { | ||
const meta = reflect_1.reflect(controller); | ||
return !!meta.name.match(/controller$/i) | ||
|| !!meta.decorators.find((x) => x.name === "plumier-meta:root"); | ||
} | ||
function transformController(object, opt) { | ||
const controller = typeof object === "function" ? tinspector_1.reflect(object) : object; | ||
if (!controller.name.toLowerCase().endsWith("controller")) | ||
const controller = reflect_1.reflect(object); | ||
const rootRoutes = getRootRoutes(opt.rootPath, controller); | ||
const infos = []; | ||
// check for class @route.ignore() | ||
const ignoreDecorator = controller.decorators.find((x) => x.name === "plumier-meta:ignore"); | ||
if (ignoreDecorator) | ||
return []; | ||
const controllerRoutes = getControllerRoutes(opt && opt.root || "", controller); | ||
const infos = []; | ||
for (const ctl of controllerRoutes) { | ||
for (const ctl of rootRoutes) { | ||
for (const method of controller.methods) { | ||
if (method.decorators.some((x) => x.name == "Ignore" || x.name == "Route")) | ||
infos.push(...transformMethodWithDecorator(ctl, controller, method)); | ||
if (method.decorators.some((x) => x.name == "plumier-meta:ignore" || x.name == "plumier-meta:route")) | ||
infos.push(...transformMethodWithDecorator(ctl, controller, method, opt.group)); | ||
else | ||
infos.push(...transformMethod(ctl, controller, method)); | ||
infos.push(...transformMethod(ctl, controller, method, opt.group)); | ||
} | ||
@@ -89,33 +138,37 @@ } | ||
} | ||
function transformModule(path) { | ||
//read all files and get module reflection | ||
const files = common_1.findFilesRecursive(path); | ||
const infos = []; | ||
for (const file of files) { | ||
const root = getRoot(path, file); | ||
for (const member of tinspector_1.reflect(file).members) { | ||
if (member.kind === "Class" && member.name.toLocaleLowerCase().endsWith("controller")) | ||
infos.push(...transformController(member, { root })); | ||
exports.transformController = transformController; | ||
async function extractController(controller, option) { | ||
if (typeof controller === "string") { | ||
const ctl = path_1.isAbsolute(controller) ? controller : path_1.join(option.rootDir, controller); | ||
const types = await findClassRecursive(ctl); | ||
const result = []; | ||
for (const type of types) { | ||
const ctl = await extractController(type.type, option); | ||
result.push(...ctl.map(x => ({ | ||
root: option.directoryAsPath ? type.root : "", | ||
type: x.type | ||
}))); | ||
} | ||
return result; | ||
} | ||
return infos; | ||
else if (Array.isArray(controller)) { | ||
const raw = controller; | ||
const controllers = await Promise.all(raw.map(x => extractController(x, option))); | ||
return controllers.flatten(); | ||
} | ||
// common controller | ||
if (isController(controller)) { | ||
return [{ root: "", type: controller }]; | ||
} | ||
return []; | ||
} | ||
function generateRoutes(executionPath, controller) { | ||
async function generateRoutes(controller, option) { | ||
const opt = Object.assign({ group: undefined, rootPath: "", rootDir: "", directoryAsPath: true }, option); | ||
const controllers = await extractController(controller, opt); | ||
let routes = []; | ||
if (typeof controller === "string") { | ||
const path = path_1.isAbsolute(controller) ? controller : | ||
path_1.join(executionPath, controller); | ||
if (!fs_1.existsSync(path)) | ||
throw new Error(types_1.errorMessage.ControllerPathNotFound.format(path)); | ||
routes = transformModule(path); | ||
for (const controller of controllers) { | ||
routes.push(...transformController(controller.type, Object.assign(Object.assign({}, opt), { rootPath: appendRoute(controller.root, opt.rootPath) }))); | ||
} | ||
else if (Array.isArray(controller)) { | ||
routes = controller.map(x => transformController(x)) | ||
.flatten(); | ||
} | ||
else { | ||
routes = transformController(controller); | ||
} | ||
return routes; | ||
} | ||
exports.generateRoutes = generateRoutes; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.router = void 0; | ||
const path_to_regexp_1 = require("path-to-regexp"); | ||
@@ -25,16 +26,7 @@ const application_pipeline_1 = require("./application-pipeline"); | ||
} | ||
function createQuery(query, { keys, match }) { | ||
const raw = keys.reduce((a, b, i) => { | ||
function createQuery({ keys, match }) { | ||
return keys.reduce((a, b, i) => { | ||
a[b.name] = match[i + 1]; | ||
return a; | ||
}, query); | ||
return new Proxy(raw, { | ||
get: (target, name) => { | ||
for (const key in target) { | ||
if (key.toLowerCase() === name.toString().toLowerCase()) | ||
return target[key]; | ||
} | ||
return target[name]; | ||
} | ||
}); | ||
}, {}); | ||
} | ||
@@ -55,5 +47,4 @@ /* ------------------------------------------------------------------------------- */ | ||
if (handler) { | ||
Object.defineProperty(ctx.request, "query", { | ||
value: createQuery(ctx.query, handler) | ||
}); | ||
if (ctx.request.addQuery) | ||
ctx.request.addQuery(createQuery(handler)); | ||
ctx.route = handler.route; | ||
@@ -60,0 +51,0 @@ } |
@@ -5,9 +5,18 @@ /// <reference types="node" /> | ||
import Koa, { Context } from "koa"; | ||
import { ClassReflection, MethodReflection, PropertyReflection, ParameterReflection } from "tinspector"; | ||
import { VisitorExtension } from "typedconverter"; | ||
import { RoleField } from "./authorization"; | ||
import { Class } from "./common"; | ||
import { ClassReflection, MethodReflection, ParameterReflection, PropertyReflection, Class } from "@plumier/reflect"; | ||
import { Result, VisitorInvocation } from "@plumier/validator"; | ||
import { HttpStatus } from "./http-status"; | ||
export declare type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; | ||
export declare type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; | ||
export declare type KeyOf<T> = Extract<keyof T, string>; | ||
export interface ApplyToOption { | ||
/** | ||
* Apply decorator into specific action, only work on controller scoped decorator. | ||
* | ||
* Should specify a correct action name(s) | ||
*/ | ||
applyTo?: string | string[]; | ||
} | ||
export interface JwtClaims { | ||
[key: string]: any; | ||
} | ||
export interface HttpCookie { | ||
@@ -44,3 +53,6 @@ key: string; | ||
export declare type HttpMethod = "post" | "get" | "put" | "delete" | "patch" | "head" | "trace" | "options"; | ||
export declare type RouteMetadata = RouteInfo | VirtualRoute; | ||
export interface RouteInfo { | ||
kind: "ActionRoute"; | ||
group?: string; | ||
url: string; | ||
@@ -51,8 +63,14 @@ method: HttpMethod; | ||
access?: string; | ||
paramMapper: { | ||
alias: (name: string) => string; | ||
}; | ||
} | ||
export interface VirtualRouteInfo { | ||
className: string; | ||
export interface VirtualRoute { | ||
kind: "VirtualRoute"; | ||
group?: string; | ||
url: string; | ||
method: HttpMethod; | ||
access: string; | ||
provider: Class; | ||
access?: string; | ||
openApiOperation?: any; | ||
} | ||
@@ -63,10 +81,14 @@ export interface RouteAnalyzerIssue { | ||
} | ||
export declare type RouteAnalyzerFunction = (route: RouteInfo, allRoutes: RouteInfo[]) => RouteAnalyzerIssue; | ||
export declare type RouteAnalyzerFunction = (route: RouteMetadata, allRoutes: RouteMetadata[]) => RouteAnalyzerIssue[]; | ||
export interface Facility { | ||
generateRoutes(app: Readonly<PlumierApplication>): Promise<RouteMetadata[]>; | ||
setup(app: Readonly<PlumierApplication>): void; | ||
initialize(app: Readonly<PlumierApplication>, routes: RouteInfo[], vRoutes: VirtualRouteInfo[]): Promise<void>; | ||
preInitialize(app: Readonly<PlumierApplication>): Promise<void>; | ||
initialize(app: Readonly<PlumierApplication>, routes: RouteMetadata[]): Promise<void>; | ||
} | ||
export declare class DefaultFacility implements Facility { | ||
generateRoutes(app: Readonly<PlumierApplication>): Promise<RouteMetadata[]>; | ||
setup(app: Readonly<PlumierApplication>): void; | ||
initialize(app: Readonly<PlumierApplication>, routes: RouteInfo[], vRoutes: VirtualRouteInfo[]): Promise<void>; | ||
preInitialize(app: Readonly<PlumierApplication>): Promise<void>; | ||
initialize(app: Readonly<PlumierApplication>, routes: RouteMetadata[]): Promise<void>; | ||
} | ||
@@ -78,5 +100,10 @@ declare module "koa" { | ||
config: Readonly<Configuration>; | ||
user?: JwtClaims; | ||
} | ||
interface Request { | ||
addQuery(query: any): void; | ||
} | ||
interface DefaultState { | ||
caller: "system" | "invoke"; | ||
user?: JwtClaims; | ||
} | ||
@@ -96,3 +123,3 @@ } | ||
ctx: Readonly<T>; | ||
metadata?: GlobalMetadata; | ||
metadata?: T extends ActionContext ? Metadata : GlobalMetadata; | ||
proceed(): Promise<ActionResult>; | ||
@@ -133,3 +160,3 @@ } | ||
*/ | ||
use(middleware: string | symbol): Application; | ||
use(middleware: string | symbol, scope?: "Global" | "Action"): Application; | ||
/** | ||
@@ -141,3 +168,3 @@ * Use plumier middleware | ||
*/ | ||
use(middleware: Middleware): Application; | ||
use(middleware: Middleware, scope?: "Global" | "Action"): Application; | ||
/** | ||
@@ -152,3 +179,3 @@ * Use plumier middleware | ||
*/ | ||
use(middleware: MiddlewareFunction): Application; | ||
use(middleware: MiddlewareFunction, scope?: "Global" | "Action"): Application; | ||
/** | ||
@@ -193,32 +220,45 @@ * Set facility (advanced configuration) | ||
} | ||
export interface ValidatorDecorator { | ||
type: "ValidatorDecorator"; | ||
validator: CustomValidatorFunction | string | symbol; | ||
} | ||
export interface ValidatorContext { | ||
name: string; | ||
ctx: ActionContext; | ||
parent?: { | ||
value: any; | ||
type: Class; | ||
decorators: any[]; | ||
}; | ||
metadata: Metadata; | ||
} | ||
export interface AsyncValidatorResult { | ||
path: string; | ||
messages: string[]; | ||
} | ||
export declare type CustomValidatorFunction = (value: any, info: ValidatorContext) => undefined | string | AsyncValidatorResult[] | Promise<AsyncValidatorResult[] | string | undefined>; | ||
export interface CustomValidator { | ||
validate(value: any, info: ValidatorContext): undefined | string | AsyncValidatorResult[] | Promise<AsyncValidatorResult[] | string | undefined>; | ||
} | ||
export declare class FormFile { | ||
/** | ||
* Size of the file (bytes) | ||
*/ | ||
size: number; | ||
/** | ||
* Temporary path of the uploaded file | ||
*/ | ||
path: string; | ||
/** | ||
* Original file name provided by client | ||
*/ | ||
name: string; | ||
/** | ||
* Mime type of the file | ||
*/ | ||
type: string; | ||
/** | ||
* The file timestamp | ||
*/ | ||
mtime?: string | undefined; | ||
constructor(size: number, path: string, name: string, type: string, mtime?: string | undefined); | ||
constructor( | ||
/** | ||
* Size of the file (bytes) | ||
*/ | ||
size: number, | ||
/** | ||
* Temporary path of the uploaded file | ||
*/ | ||
path: string, | ||
/** | ||
* Original file name provided by client | ||
*/ | ||
name: string, | ||
/** | ||
* Mime type of the file | ||
*/ | ||
type: string, | ||
/** | ||
* The file timestamp | ||
*/ | ||
mtime?: string | undefined); | ||
/** | ||
* Copy uploaded file into target directory, file name automatically generated | ||
@@ -233,13 +273,75 @@ * @param dir target directory | ||
} | ||
export interface FileUploadInfo { | ||
field: string; | ||
fileName: string; | ||
originalName: string; | ||
mime: string; | ||
size: number; | ||
encoding: string; | ||
export declare type FilterQueryType = "equal" | "partial" | "range" | "gte" | "gt" | "lte" | "lt" | "ne"; | ||
export interface NestedGenericControllerDecorator { | ||
kind: "plumier-meta:relation-prop-name"; | ||
type: Class; | ||
relation: string; | ||
} | ||
export interface FileParser { | ||
save(subDirectory?: string): Promise<FileUploadInfo[]>; | ||
export declare type GenericControllers = [Class<ControllerGeneric>, Class<NestedControllerGeneric>]; | ||
export interface SelectQuery { | ||
columns?: any; | ||
relations?: any; | ||
} | ||
export interface Repository<T> { | ||
find(offset: number, limit: number, query: any, select: SelectQuery, order: any): Promise<T[]>; | ||
insert(data: Partial<T>): Promise<T>; | ||
findById(id: any, select: SelectQuery): Promise<T | undefined>; | ||
update(id: any, data: Partial<T>): Promise<T | undefined>; | ||
delete(id: any): Promise<T | undefined>; | ||
count(query?: any): Promise<number>; | ||
} | ||
export interface NestedRepository<P, T> { | ||
find(pid: any, offset: number, limit: number, query: any, select: SelectQuery, order: any): Promise<T[]>; | ||
insert(pid: any, data: Partial<T>): Promise<T>; | ||
findParentById(id: any): Promise<P | undefined>; | ||
findById(id: any, select: SelectQuery): Promise<T | undefined>; | ||
update(id: any, data: Partial<T>): Promise<T | undefined>; | ||
delete(id: any): Promise<T | undefined>; | ||
count(pid: any, query?: any): Promise<number>; | ||
} | ||
export declare abstract class ControllerGeneric<T = any, TID = any> { | ||
abstract readonly entityType: Class<T>; | ||
} | ||
export declare abstract class NestedControllerGeneric<P = any, T = any, PID = any, TID = any> { | ||
abstract readonly entityType: Class<T>; | ||
abstract readonly parentEntityType: Class<P>; | ||
} | ||
export declare type AccessModifier = "read" | "write" | "route"; | ||
export interface AuthorizationContext { | ||
/** | ||
* Current property value, only available on authorize read/write | ||
*/ | ||
value?: any; | ||
/** | ||
* Current property's parent value, only available on authorize read/write | ||
*/ | ||
parentValue?: any; | ||
/** | ||
* Current login user JWT claim | ||
*/ | ||
user: JwtClaims | undefined; | ||
/** | ||
* Current request context | ||
*/ | ||
ctx: ActionContext; | ||
/** | ||
* Metadata information of the current request | ||
*/ | ||
metadata: Metadata; | ||
/** | ||
* Type of authorization applied read/write/route/filter | ||
*/ | ||
access: AccessModifier; | ||
} | ||
export interface Authorizer { | ||
authorize(info: AuthorizationContext): boolean | Promise<boolean>; | ||
} | ||
export interface AuthPolicy { | ||
name: string; | ||
equals(id: string, ctx: AuthorizationContext): boolean; | ||
authorize(ctx: AuthorizationContext): Promise<boolean>; | ||
conflict(other: AuthPolicy): boolean; | ||
friendlyName(): string; | ||
} | ||
export declare type CustomConverter = (next: VisitorInvocation, ctx: ActionContext) => Result; | ||
export interface Configuration { | ||
@@ -250,7 +352,10 @@ mode: "debug" | "production"; | ||
*/ | ||
middlewares: (string | symbol | MiddlewareFunction | Middleware)[]; | ||
middlewares: { | ||
middleware: (string | symbol | MiddlewareFunction | Middleware); | ||
scope: "Global" | "Action"; | ||
}[]; | ||
/** | ||
* Specify controller path (absolute or relative to entry point) or the controller classes array. | ||
*/ | ||
controller: string | Class[] | Class; | ||
controller: string | string[] | Class[] | Class; | ||
/** | ||
@@ -272,3 +377,3 @@ * Set custom dependency resolver for dependency injection | ||
*/ | ||
typeConverterVisitors?: VisitorExtension[]; | ||
typeConverterVisitors: CustomConverter[]; | ||
/** | ||
@@ -279,10 +384,6 @@ * Set custom route analyser functions | ||
/** | ||
* Role field / function used to specify current login user role inside JWT claim for authorization | ||
* Global authorizations | ||
*/ | ||
roleField: RoleField; | ||
globalAuthorizations: string | string[]; | ||
/** | ||
* Global authorization decorators, use mergeDecorator for multiple | ||
*/ | ||
globalAuthorizationDecorators?: (...args: any[]) => void; | ||
/** | ||
* Enable/disable authorization, when enabled all routes will be private by default. Default false | ||
@@ -300,2 +401,22 @@ */ | ||
trustProxyHeader: boolean; | ||
/** | ||
* Implementation of generic controllers, first tuple for simple controller, second tuple for one to many controller | ||
*/ | ||
genericController?: GenericControllers; | ||
/** | ||
* Generic controller name conversion to make plural route | ||
*/ | ||
genericControllerNameConversion?: (x: string) => string; | ||
/** | ||
* Custom authorization policy | ||
*/ | ||
authPolicies: Class<AuthPolicy>[]; | ||
/** | ||
* Transform property value of response before its being parsed by response authorization | ||
*/ | ||
responseTransformer?: (prop: PropertyReflection, value: any) => any; | ||
/** | ||
* Provide Open API security scheme https://swagger.io/docs/specification/authentication/ | ||
*/ | ||
openApiSecuritySchemes?: any; | ||
} | ||
@@ -323,6 +444,18 @@ export interface PlumierConfiguration extends Configuration { | ||
export interface Metadata { | ||
/** | ||
* Controller object graph | ||
*/ | ||
controller: ClassReflection; | ||
/** | ||
* Current action object graph | ||
*/ | ||
action: MethodReflection; | ||
access?: string; | ||
/** | ||
* Action parameter helper, used to query current action parameter name or value | ||
*/ | ||
actionParams: ParameterMetadata; | ||
/** | ||
* Reflection information about the current location (class/method/property) on which the decorator applied | ||
*/ | ||
current?: CurrentMetadataType; | ||
@@ -358,3 +491,3 @@ } | ||
} | ||
export declare class MetadataImpl { | ||
export declare class MetadataImpl implements Metadata { | ||
/** | ||
@@ -376,3 +509,3 @@ * Controller metadata object graph | ||
*/ | ||
actionParams?: ParameterMetadata; | ||
actionParams: ParameterMetadata; | ||
/** | ||
@@ -382,16 +515,22 @@ * Metadata information where target (Validator/Authorizer/Middleware) applied, can be a Property, Parameter, Method, Class. | ||
current?: CurrentMetadataType; | ||
constructor(params: any[] | undefined, routeInfo: RouteInfo, current?: CurrentMetadataType); | ||
constructor(params: any[], routeInfo: RouteInfo, current?: CurrentMetadataType); | ||
} | ||
export declare namespace errorMessage { | ||
const RouteDoesNotHaveBackingParam = "PLUM1000: Route parameters ({0}) doesn't have appropriate backing parameter"; | ||
const ActionDoesNotHaveTypeInfo = "PLUM1001: Parameter binding skipped because action doesn't have @route decorator"; | ||
const DuplicateRouteFound = "PLUM1003: Duplicate route found in {0}"; | ||
const ControllerPathNotFound = "PLUM1004: Controller file or directory {0} not found"; | ||
const ModelWithoutTypeInformation = "PLUM1005: Parameter binding skipped because {0} doesn't have @domain() decorator"; | ||
const ArrayWithoutTypeInformation = "PLUM1006: Parameter binding skipped because array field without @array() decorator found in ({0})"; | ||
const ModelNotFound = "PLUM1007: Domain model not found, no class decorated with @domain() on provided classes"; | ||
const ModelPathNotFound = "PLUM1007: Domain model not found, no class decorated with @domain() on path {0}"; | ||
const PublicNotInParameter = "PLUM1008: @authorize.public() can not be applied to parameter"; | ||
const ObjectNotFound = "PLUM1009: Object with id {0} not found in Object registry"; | ||
const UnableToInstantiateModel = "PLUM2000: Unable to instantiate {0}. Domain model should not throw error inside constructor"; | ||
const RouteDoesNotHaveBackingParam = "Route parameters ({0}) doesn't have appropriate backing parameter"; | ||
const DuplicateRouteFound = "Duplicate route found in {0}"; | ||
const ControllerPathNotFound = "Controller file or directory {0} not found"; | ||
const ObjectNotFound = "Object with id {0} not found in Object registry"; | ||
const ActionParameterDoesNotHaveTypeInfo = "Parameter binding skipped because action parameters doesn't have type information in ({0})"; | ||
const ModelWithoutTypeInformation = "Parameter binding skipped because {0} doesn't have type information on its properties"; | ||
const ArrayWithoutTypeInformation = "Parameter binding skipped because array element doesn't have type information in ({0})"; | ||
const PropertyWithoutTypeInformation = "Parameter binding skipped because property doesn't have type information in ({0})"; | ||
const GenericControllerImplementationNotFound = "Generic controller implementation not installed"; | ||
const GenericControllerRequired = "@genericController() required generic controller implementation, please install the appropriate facility"; | ||
const GenericControllerMissingTypeInfo = "{0} marked with @genericController() but doesn't have type information"; | ||
const GenericControllerInNonArrayProperty = "Nested generic controller can not be created using non array relation on: {0}.{1}"; | ||
const CustomRouteEndWithParameter = "Custom route path '{0}' on {1} entity, require path that ends with route parameter, example: animals/:animalId"; | ||
const CustomRouteRequiredTwoParameters = "Nested custom route path '{0}' on {1} entity, must have two route parameters, example: users/:userId/animals/:animalId"; | ||
const CustomRouteMustHaveOneParameter = "Custom route path '{0}' on {1} entity, must have one route parameter, example: animals/:animalId"; | ||
const EntityRequireID = "Entity {0} used by generic controller doesn't have an ID property"; | ||
const UnableToInstantiateModel = "Unable to instantiate {0}. Domain model should not throw error inside constructor"; | ||
const UnableToConvertValue = "Unable to convert \"{0}\" into {1}"; | ||
@@ -398,0 +537,0 @@ const FileSizeExceeded = "File {0} size exceeded the maximum size"; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.errorMessage = exports.MetadataImpl = exports.ParameterMetadata = exports.ValidationError = exports.HttpStatusError = exports.NestedControllerGeneric = exports.ControllerGeneric = exports.FormFile = exports.DefaultDependencyResolver = exports.MiddlewareUtil = exports.DefaultFacility = exports.RedirectActionResult = exports.ActionResult = void 0; | ||
const tslib_1 = require("tslib"); | ||
const fs_1 = require("fs"); | ||
const path_1 = require("path"); | ||
const tinspector_1 = require("tinspector"); | ||
const reflect_1 = tslib_1.__importStar(require("@plumier/reflect")); | ||
const util_1 = require("util"); | ||
const decorator_1 = require("./decorator"); | ||
const http_status_1 = require("./http-status"); | ||
@@ -67,4 +67,6 @@ const copyFileAsync = util_1.promisify(fs_1.copyFile); | ||
class DefaultFacility { | ||
async generateRoutes(app) { return []; } | ||
setup(app) { } | ||
async initialize(app, routes, vRoutes) { } | ||
async preInitialize(app) { } | ||
async initialize(app, routes) { } | ||
} | ||
@@ -107,3 +109,3 @@ exports.DefaultFacility = DefaultFacility; | ||
register(id) { | ||
return tinspector_1.decorateClass(cls => { | ||
return reflect_1.decorateClass(cls => { | ||
this.registry.set(id, cls); | ||
@@ -130,3 +132,23 @@ return { type: "RegistryDecorator", id }; | ||
let FormFile = class FormFile { | ||
constructor(size, path, name, type, mtime) { | ||
constructor( | ||
/** | ||
* Size of the file (bytes) | ||
*/ | ||
size, | ||
/** | ||
* Temporary path of the uploaded file | ||
*/ | ||
path, | ||
/** | ||
* Original file name provided by client | ||
*/ | ||
name, | ||
/** | ||
* Mime type of the file | ||
*/ | ||
type, | ||
/** | ||
* The file timestamp | ||
*/ | ||
mtime) { | ||
this.size = size; | ||
@@ -153,6 +175,12 @@ this.path = path; | ||
FormFile = tslib_1.__decorate([ | ||
decorator_1.domain(), | ||
reflect_1.default.parameterProperties(), | ||
tslib_1.__metadata("design:paramtypes", [Number, String, String, String, String]) | ||
], FormFile); | ||
exports.FormFile = FormFile; | ||
class ControllerGeneric { | ||
} | ||
exports.ControllerGeneric = ControllerGeneric; | ||
class NestedControllerGeneric { | ||
} | ||
exports.NestedControllerGeneric = NestedControllerGeneric; | ||
// --------------------------------------------------------------------- // | ||
@@ -212,4 +240,4 @@ // ------------------------------- ERROR ------------------------------- // | ||
this.access = routeInfo.access; | ||
if (params) | ||
this.actionParams = new ParameterMetadata(params, routeInfo.action.parameters); | ||
//if (params) | ||
this.actionParams = new ParameterMetadata(params, routeInfo.action.parameters); | ||
this.current = current; | ||
@@ -225,14 +253,20 @@ } | ||
//PLUM1XXX User configuration error | ||
errorMessage.RouteDoesNotHaveBackingParam = "PLUM1000: Route parameters ({0}) doesn't have appropriate backing parameter"; | ||
errorMessage.ActionDoesNotHaveTypeInfo = "PLUM1001: Parameter binding skipped because action doesn't have @route decorator"; | ||
errorMessage.DuplicateRouteFound = "PLUM1003: Duplicate route found in {0}"; | ||
errorMessage.ControllerPathNotFound = "PLUM1004: Controller file or directory {0} not found"; | ||
errorMessage.ModelWithoutTypeInformation = "PLUM1005: Parameter binding skipped because {0} doesn't have @domain() decorator"; | ||
errorMessage.ArrayWithoutTypeInformation = "PLUM1006: Parameter binding skipped because array field without @array() decorator found in ({0})"; | ||
errorMessage.ModelNotFound = "PLUM1007: Domain model not found, no class decorated with @domain() on provided classes"; | ||
errorMessage.ModelPathNotFound = "PLUM1007: Domain model not found, no class decorated with @domain() on path {0}"; | ||
errorMessage.PublicNotInParameter = "PLUM1008: @authorize.public() can not be applied to parameter"; | ||
errorMessage.ObjectNotFound = "PLUM1009: Object with id {0} not found in Object registry"; | ||
errorMessage.RouteDoesNotHaveBackingParam = "Route parameters ({0}) doesn't have appropriate backing parameter"; | ||
errorMessage.DuplicateRouteFound = "Duplicate route found in {0}"; | ||
errorMessage.ControllerPathNotFound = "Controller file or directory {0} not found"; | ||
errorMessage.ObjectNotFound = "Object with id {0} not found in Object registry"; | ||
errorMessage.ActionParameterDoesNotHaveTypeInfo = "Parameter binding skipped because action parameters doesn't have type information in ({0})"; | ||
errorMessage.ModelWithoutTypeInformation = "Parameter binding skipped because {0} doesn't have type information on its properties"; | ||
errorMessage.ArrayWithoutTypeInformation = "Parameter binding skipped because array element doesn't have type information in ({0})"; | ||
errorMessage.PropertyWithoutTypeInformation = "Parameter binding skipped because property doesn't have type information in ({0})"; | ||
errorMessage.GenericControllerImplementationNotFound = "Generic controller implementation not installed"; | ||
errorMessage.GenericControllerRequired = "@genericController() required generic controller implementation, please install the appropriate facility"; | ||
errorMessage.GenericControllerMissingTypeInfo = "{0} marked with @genericController() but doesn't have type information"; | ||
errorMessage.GenericControllerInNonArrayProperty = "Nested generic controller can not be created using non array relation on: {0}.{1}"; | ||
errorMessage.CustomRouteEndWithParameter = "Custom route path '{0}' on {1} entity, require path that ends with route parameter, example: animals/:animalId"; | ||
errorMessage.CustomRouteRequiredTwoParameters = "Nested custom route path '{0}' on {1} entity, must have two route parameters, example: users/:userId/animals/:animalId"; | ||
errorMessage.CustomRouteMustHaveOneParameter = "Custom route path '{0}' on {1} entity, must have one route parameter, example: animals/:animalId"; | ||
errorMessage.EntityRequireID = "Entity {0} used by generic controller doesn't have an ID property"; | ||
//PLUM2XXX internal app error | ||
errorMessage.UnableToInstantiateModel = `PLUM2000: Unable to instantiate {0}. Domain model should not throw error inside constructor`; | ||
errorMessage.UnableToInstantiateModel = `Unable to instantiate {0}. Domain model should not throw error inside constructor`; | ||
//End user error (no error code) | ||
@@ -239,0 +273,0 @@ errorMessage.UnableToConvertValue = `Unable to convert "{0}" into {1}`; |
@@ -1,2 +0,25 @@ | ||
import { ActionContext, ActionResult, Invocation, Middleware } from "./types"; | ||
import { Class } from "./common"; | ||
import { ActionContext, ActionResult, Invocation, Middleware, Metadata } from "./types"; | ||
export interface ValidatorDecorator { | ||
type: "ValidatorDecorator"; | ||
validator: CustomValidatorFunction | string | symbol; | ||
} | ||
export interface ValidatorContext { | ||
name: string; | ||
ctx: ActionContext; | ||
parent?: { | ||
value: any; | ||
type: Class; | ||
decorators: any[]; | ||
}; | ||
metadata: Metadata; | ||
} | ||
export interface AsyncValidatorResult { | ||
path: string; | ||
messages: string[]; | ||
} | ||
export declare type CustomValidatorFunction = (value: any, info: ValidatorContext) => undefined | string | AsyncValidatorResult[] | Promise<AsyncValidatorResult[] | string | undefined>; | ||
export interface CustomValidator { | ||
validate(value: any, info: ValidatorContext): undefined | string | AsyncValidatorResult[] | Promise<AsyncValidatorResult[] | string | undefined>; | ||
} | ||
declare function validate(ctx: ActionContext): Promise<any[]>; | ||
@@ -3,0 +26,0 @@ declare class ValidatorMiddleware implements Middleware { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ValidatorMiddleware = exports.validate = void 0; | ||
const tslib_1 = require("tslib"); | ||
const tinspector_1 = tslib_1.__importDefault(require("tinspector")); | ||
const tc = tslib_1.__importStar(require("typedconverter")); | ||
const reflect_1 = tslib_1.__importDefault(require("@plumier/reflect")); | ||
const tc = tslib_1.__importStar(require("@plumier/validator")); | ||
const common_1 = require("./common"); | ||
@@ -25,3 +26,3 @@ const types_1 = require("./types"); | ||
if (common_1.isCustomClass(i.type)) { | ||
const meta = tinspector_1.default(i.type); | ||
const meta = reflect_1.default(i.type); | ||
const classDecorators = meta.decorators.filter((x) => x.type === "ValidatorDecorator"); | ||
@@ -37,3 +38,3 @@ decorators.push(...classDecorators); | ||
if (node.parent) { | ||
return tinspector_1.default(node.parent.type).properties.find(x => x.name === getName(node.path)); | ||
return reflect_1.default(node.parent.type).properties.find(x => x.name === getName(node.path)); | ||
} | ||
@@ -68,5 +69,6 @@ else { | ||
} | ||
function validateSync(ctx, visitors) { | ||
function validateSync(ctx, customVisitors) { | ||
const result = []; | ||
const issues = []; | ||
const visitors = customVisitors.map(v => (next) => v(next, ctx)); | ||
for (const [index, parMeta] of ctx.route.action.parameters.entries()) { | ||
@@ -111,3 +113,3 @@ const rawParameter = ctx.parameters[index]; | ||
const nodes = []; | ||
const visitors = [customValidatorNodeVisitor(nodes), ...(ctx.config.typeConverterVisitors || [])]; | ||
const visitors = [customValidatorNodeVisitor(nodes), ...ctx.config.typeConverterVisitors]; | ||
const syncResult = validateSync(ctx, visitors); | ||
@@ -114,0 +116,0 @@ const asyncResult = await validateAsync(ctx, nodes); |
{ | ||
"name": "@plumier/core", | ||
"version": "1.0.0-rc2.59+f9a4594", | ||
"version": "1.0.0", | ||
"description": "Delightful Node.js Rest Framework", | ||
@@ -17,3 +17,4 @@ "main": "lib/index.js", | ||
"scripts": { | ||
"compile": "tsc -p tsconfig.build.json" | ||
"compile": "tsc -p tsconfig.build.json", | ||
"copy-readme": "cpy ../../readme.md ." | ||
}, | ||
@@ -23,11 +24,18 @@ "author": "Ketut Sandiarsa", | ||
"dependencies": { | ||
"@types/glob": "^7.1.1", | ||
"@types/koa": "^2.11.2", | ||
"chalk": "^3.0.0", | ||
"@plumier/reflect": "^1.0.0", | ||
"@plumier/validator": "^1.0.0", | ||
"@types/debug": "^4.1.5", | ||
"@types/glob": "^7.1.3", | ||
"chalk": "^4.1.1", | ||
"debug": "^4.3.1", | ||
"glob": "^7.1.6", | ||
"path-to-regexp": "^6.1.0", | ||
"tinspector": "^2.3.1", | ||
"tslib": "^1.11.1", | ||
"typedconverter": "^2.3.0" | ||
"path-to-regexp": "^6.2.0", | ||
"tslib": "^2.2.0" | ||
}, | ||
"peerDependencies": { | ||
"@types/koa": "*" | ||
}, | ||
"devDependencies": { | ||
"cpy-cli": "^3.1.1" | ||
}, | ||
"bugs": { | ||
@@ -43,6 +51,3 @@ "url": "https://github.com/plumier/plumier/issues" | ||
}, | ||
"gitHead": "f9a4594cd6e1122b939263e24803d7c3c761eee7", | ||
"devDependencies": { | ||
"upath": "^1.2.0" | ||
} | ||
"gitHead": "7e8d4af7b6163780d960e5820e5dc469878e93bd" | ||
} |
204
readme.md
# Plumier | ||
Delightful Node.js Rest Framework | ||
[![Build Status](https://travis-ci.org/plumier/plumier.svg?branch=master)](https://travis-ci.org/plumier/plumier) | ||
[![Build status](https://ci.appveyor.com/api/projects/status/6carp7h4q50v4pj6?svg=true)](https://ci.appveyor.com/project/ktutnik/plumier-isghw) | ||
[![Build Status](https://github.com/plumier/plumier/workflows/ubuntu/badge.svg)](https://github.com/plumier/plumier/actions?query=workflow%3Aubuntu) | ||
[![Build status](https://github.com/plumier/plumier/workflows/windows/badge.svg)](https://github.com/plumier/plumier/actions?query=workflow%3Awindows) | ||
[![Coverage Status](https://coveralls.io/repos/github/plumier/plumier/badge.svg?branch=master)](https://coveralls.io/github/plumier/plumier?branch=master) | ||
[![Greenkeeper badge](https://badges.greenkeeper.io/plumier/plumier.svg)](https://greenkeeper.io/) | ||
[![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lernajs.io/) | ||
![npm (canary)](https://img.shields.io/npm/v/plumier/canary) | ||
![npm (latest)](https://img.shields.io/npm/v/plumier/latest) | ||
[![npm](https://img.shields.io/npm/v/plumier/canary)](https://www.npmjs.com/package/plumier?activeTab=versions) | ||
[![npm](https://img.shields.io/npm/v/plumier/latest)](https://www.npmjs.com/package/plumier?activeTab=versions) | ||
## Motivation | ||
## Documentation | ||
Read the project documentation on https://plumierjs.com | ||
My subjective opinion about TypeScript frameworks nowadays is most of them are too advanced, even to start a very simple API you need to prepare yourself with some advanced knowledge of Separation of Concern, Dependency Injection, SOLID principle and many other design pattern and best practices comes from Object Oriented world. | ||
Most of those frameworks take advantage of OO fanciness, where framework provided a mandatory rule on how you should separate your logic and layout your source code to keep it clean and SOLID. | ||
In the other hands frameworks doesn't put robustness and secureness as priority because with its fancy separation you can create your own implementation of type conversion, validator or authorization on top of an existing library such as Joi and Passport. | ||
### What About Express? | ||
I am a big fans of Express, I spent years developing API using Express. Good parts about Express is its simplicity, its easy to master Express only by reading the documentation or by spending a 10 minutes tutorial. Because its only consist of Routing and middleware. | ||
Bad things about Express is its getting harder when you get into the detail. By default express doesn't have a built-in type conversion validator and authorization functionalities. Thus you need to combine some npm packages to do the detail things. You need to configure schema for joi validation and mongoose, setting this and that for authorization, at the end your code is so far from simple. | ||
## Enter Plumier | ||
Welcome to Plumier where robustness and secureness is mandatory and fanciness is optional. Unlike most TypeScript framework Plumier focus on development happiness and productivity while keep simplest implementation robust and secure. | ||
The main goal is to make your development time fast and delightful by providing built-in functionalities such as automatic data type conversion, comprehensive list (40+ types) of validator, authorization to programmatically restrict access to some endpoints and more cool features such as: | ||
* [Parameter binding](https://plumierjs.com/docs/refs/parameter-binding) | ||
* [Route generation](https://plumierjs.com/docs/refs/route) | ||
* [Static route generation analysis](https://plumierjs.com/docs/refs/static-analysis) | ||
* [Meta programming on middleware basis](https://medium.com/hackernoon/adding-an-auditing-system-into-a-rest-api-4fbb522240ea) | ||
* API versioning based on reflection | ||
All above features created with dedicated reflection library to possibly perform rich meta programming on top of TypeScript language to make everything feel more automatic with less configurations. | ||
Furthermore Plumier doesn't force you to follow some design pattern or best practice. Plumier application is highly configurable that make you able to layout your source code freely. | ||
### Robust and Secure | ||
Plumier provided some built-in functionalities that work in the background to make the most trivial implementation keep secure and robust. | ||
```typescript | ||
class AnimalsController { | ||
@route.get() | ||
list(offset:number, @val.int({ min: 1 }) limit:number) { | ||
//implementation | ||
} | ||
} | ||
``` | ||
Above controller generate single endpoints `GET /animals/list?offset=0&list=10`. Plumier uses a dedicated type introspection (reflection) library to make it able to extract TypeScript type annotation than translate it into metadata and provide functionalities that working on the background. | ||
1. It automatically bound `offset` and `limit` parameter with request query by name, no further configuration needed. | ||
2. It automatically convert the request query `offset` and `limit` value into appropriate parameter data type. This function prevent bad user submitting bad value causing conversion error or even sql injection. | ||
3. It automatically validate the `limit` parameter and make sure if the provided value is a positive number. | ||
4. It taking care of query case insensitivity, `GET /animals/list?OFFSET=0&LIMIT=10` will keep working. Note that query is case sensitive in most frameworks. | ||
Plumier has [comprehensive list](refs/validation#decorators) of decorator based validators, it easily can be applied on method parameters or domain model properties. | ||
```typescript | ||
@domain() | ||
class User { | ||
constructor( | ||
@val.length({ min: 5, max: 128 }) | ||
public name: string, | ||
@val.email() | ||
public email: string, | ||
@val.before() | ||
public dateOfBirth: Date, | ||
public active: boolean | ||
) { } | ||
} | ||
``` | ||
Furthermore Plumier provided built-in decorator based authorization to easily restrict access to your API endpoints. | ||
```typescript | ||
class UsersController { | ||
// GET /users?offset&limit | ||
// only accessible by Admin | ||
@authorize.role("Admin") | ||
@route.get("") | ||
list(offset:number, limit:number) { } | ||
// POST /users | ||
// accessible by public | ||
@authorize.public() | ||
@route.post("") | ||
save(data:User){} | ||
} | ||
``` | ||
Above code showing that some authorization decorator applied to the method to restrict access to each endpoint handled by controller's method. | ||
### Useful Reflection Based Helpers | ||
Another benefit of using reflection library is Plumier able to provided an official [Mongoose](https://mongoosejs.com/) helper to automatically generate mongoose schema from domain model. | ||
```typescript | ||
import { collection, model } from "@plumier/mongoose" | ||
// mark domain model as collection | ||
@collection() | ||
class User { | ||
constructor( | ||
public name:string, | ||
public email:string, | ||
public dateOfBirth:Date, | ||
public role: "Admin" | "User", | ||
public active:boolean | ||
){} | ||
} | ||
// model() function automatically generate Mongoose schema | ||
// based on User properties | ||
const UserModel = model(User) | ||
``` | ||
Read more information about Mongoose helper [here](refs/mongoose-helper). | ||
### Reduce Duplication | ||
There is a best practice spread among static type programmers: **Never use your domain model as DTO**. Literally its a good advice because in a common framework using domain model as DTO can lead to some security issue, but this will ends up in another issue: bloated code and duplication. | ||
Plumier provided an advanced authorization functionalities which enables you to restrict write some property of request body by providing `@authorize` decorator on the domain model. | ||
```typescript | ||
import { authorize } from "plumier" | ||
import { collection } from "@plumier/mongoose" | ||
@collection() | ||
class User { | ||
constructor( | ||
public name:string, | ||
public email:string, | ||
public dateOfBirth:Date, | ||
//restrict access only to Admin | ||
@authorize.role("Admin") | ||
public role: "Admin" | "User", | ||
public active:boolean | ||
){} | ||
} | ||
``` | ||
Using above code, only user with `Admin` role will be able to set the `role` property. Using this functionalities will cut a lot of bloated DTO classes and duplication, and make the security aspect of the application easily reviewed. | ||
### Lightweight | ||
Above all, with all those features above, Plumier is a lightweight framework. | ||
``` | ||
GET method benchmark starting... | ||
Server Base Method Req/s Cost (%) | ||
koa GET 32566.55 0.00 | ||
plumier koa GET 31966.55 1.84 | ||
express GET 19047.60 0.00 | ||
nest express GET 16972.91 10.89 | ||
loopback express GET 3719.80 80.47 | ||
POST method benchmark starting... | ||
Server Base Method Req/s Cost (%) | ||
koa POST 12651.46 0.00 | ||
plumier koa POST 11175.10 11.67 | ||
express POST 9521.28 0.00 | ||
nest express POST 5251.00 44.85 | ||
loopback express POST 2294.00 75.91 | ||
``` | ||
Above is a full stack benchmark (routing, body parser, validator, type conversion) result of Plumier and other TypeScript framework. Showing that using Plumier is as fast as using Koa. The benchmark source code can be found [here](https://github.com/ktutnik/full-stack-benchmarks). | ||
Creating lightweight framework is not easy, from result above Plumier only 1.84% slower than Koa (its base framework) in the other hand Nest 10.89% and Loopback 4 is 80% slower than their base framework. | ||
## Requirements | ||
* Node.js >= 10.0.0 | ||
* TypeScript | ||
## Blog Posts and Publications | ||
* [Reason, motivation and how Plumier designed](https://medium.com/hackernoon/i-spent-a-year-to-reinvent-a-node-js-framework-b3b0b1602ad5) | ||
* [How to use Plumier with mongoose](https://hackernoon.com/create-secure-restful-api-with-plumier-and-mongoose-3ngz32lu) | ||
* [Advanced usage of Plumier middleware to perform AOP and metaprogramming](https://hackernoon.com/adding-an-auditing-system-into-a-rest-api-4fbb522240ea) | ||
## Documentation | ||
Go to Plumier [documentation](https://plumierjs.com) for complete documentation and tutorial | ||
## Tutorials | ||
* [Basic REST api tutorial using Knex.js](https://plumierjs.com/docs/tutorials/basic-sql/get-started) | ||
## Examples | ||
* [Basic REST API with Knex.js](https://github.com/plumier/tutorial-todo-sql-backend) | ||
* [Basic REST api with Mongoose](https://github.com/plumier/tutorial-todo-mongodb-backend) | ||
* [Plumier - React - Monorepo - Social Login](https://github.com/plumier/tutorial-monorepo-social-login) | ||
* [Plumier - Vue.js - Monorepo - Social Login](https://github.com/plumier/tutorial-social-login-vue) | ||
* [Plumier - React Native - Monorepo](https://github.com/plumier/tutorial-todo-monorepo-react-native) | ||
## Contributing | ||
@@ -205,6 +19,6 @@ To run Plumier project on local machine, some setup/app required | ||
* Visual Studio Code (Recommended) | ||
* Yarn `npm install -g yarn` | ||
* Yarn (required) | ||
### Local Setup | ||
* Fork and clone the project | ||
* Fork and clone the project `git clone` | ||
* Install dependencies by `yarn install` | ||
@@ -215,6 +29,4 @@ * Run test by `yarn test` | ||
Plumier already provided vscode `task` and `launch` setting. To start debugging a test scenario: | ||
* Build the project | ||
* Locate the test file and narrow the test runs by using `.only` | ||
* Put breakpoint on any location you need on `.ts` file | ||
* On start/debug configuration select `Jest Current File` and start debugging | ||
* Process will halt properly on the `.ts` file. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
207932
47
4720
1
3
10
31
+ Added@plumier/reflect@^1.0.0
+ Added@plumier/validator@^1.0.0
+ Added@types/debug@^4.1.5
+ Addeddebug@^4.3.1
+ Added@plumier/reflect@1.1.3(transitive)
+ Added@plumier/validator@1.1.3(transitive)
+ Added@types/acorn@4.0.6(transitive)
+ Added@types/debug@4.1.12(transitive)
+ Added@types/ms@0.7.34(transitive)
+ Addedacorn@8.8.1(transitive)
+ Addedchalk@4.1.2(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addedms@2.1.3(transitive)
- Removed@types/koa@^2.11.2
- Removedtinspector@^2.3.1
- Removedtypedconverter@^2.3.0
- Removed@types/acorn@4.0.5(transitive)
- Removedacorn@7.1.17.3.1(transitive)
- Removedchalk@3.0.0(transitive)
- Removedtinspector@2.3.13.2.2(transitive)
- Removedtslib@1.14.1(transitive)
- Removedtypedconverter@2.3.6(transitive)
Updated@types/glob@^7.1.3
Updatedchalk@^4.1.1
Updatedpath-to-regexp@^6.2.0
Updatedtslib@^2.2.0