tachybase_todo/packages/resourcer/src/resourcer.ts

312 lines
7.5 KiB
TypeScript
Raw Normal View History

2020-10-24 15:34:43 +08:00
import qs from 'qs';
import glob from 'glob';
import compose from 'koa-compose';
import Action, { ActionName } from './action';
import Resource, { ResourceOptions } from './resource';
import { parseRequest, getNameByParams, ParsedParams, requireModule } from './utils';
2020-10-24 15:34:43 +08:00
import { pathToRegexp } from 'path-to-regexp';
export interface ResourcerContext {
resourcer?: Resourcer;
action?: Action;
params?: ParsedParams;
2020-10-24 15:34:43 +08:00
[key: string]: any;
}
export interface KoaMiddlewareOptions {
/**
*
*/
prefix?: string;
/**
* resource name
*
* relatedTable ? relatedTable.table : table
*/
nameRule?: (params: ParsedParams) => string;
2020-10-24 15:34:43 +08:00
/**
* key - ctx[paramsKey]
*
* paramsKey params
*/
paramsKey?: string;
/**
* action name
*
*
*
* - list
* - create
* - get
* - update
* - delete
*/
accessors?: {
/**
*
*/
list?: string;
/**
*
*/
create?: string;
/**
*
*/
get?: string;
/**
*
*/
update?: string;
/**
*
*/
delete?: string;
};
}
export interface ExecuteOptions {
/**
*
*/
resource: string;
/**
* action name
*
*
* - list
* - create
* - get
* - update
* - delete
*/
action: ActionName;
}
export type HandlerType = (ctx: ResourcerContext, next: () => Promise<any>) => any;
export interface Handlers {
[key: string]: HandlerType;
}
export interface ImportOptions {
/**
*
*/
directory: string;
/**
* ['js', 'ts', 'json']
*/
extensions?: string[];
}
export class Resourcer {
protected resources = new Map<string, Resource>();
/**
* action handlers
*/
protected handlers = new Map<ActionName, any>();
protected paramsKey = 'params';
protected middlewares = [];
/**
* resource
*
* TODO: 配置的文件驱动现在会全部初始化
*
* @param {object} [options]
* @param {string} [options.directory]
* @param {array} [options.extensions = ['js', 'ts', 'json']]
*/
public import(options: ImportOptions): Map<string, Resource> {
const { extensions = ['js', 'ts', 'json'], directory } = options;
const patten = `${directory}/*.{${extensions.join(',')}}`;
const files = glob.sync(patten, {
ignore: [
'**/*.d.ts'
]
});
2020-10-24 15:34:43 +08:00
const resources = new Map<string, Resource>();
files.forEach((file: string) => {
const options = requireModule(file);
const table = this.define(typeof options === 'function' ? options(this) : options);
resources.set(table.getName(), table);
});
return resources;
}
/**
* resource
*
* @param name
* @param options
*/
define(options: ResourceOptions) {
const { name } = options;
const resource = new Resource(options, this);
this.resources.set(name, resource);
return resource;
}
isDefined(name: string) {
return this.resources.has(name);
}
2020-10-24 15:34:43 +08:00
/**
* action handlers
*
* @param handlers
*/
registerHandlers(handlers: Handlers) {
for (const [name, handler] of Object.entries(handlers)) {
this.registerHandler(name, handler);
}
2020-10-24 15:34:43 +08:00
}
registerHandler(name: ActionName, handler: HandlerType) {
this.handlers.set(name, handler);
}
2020-10-24 15:34:43 +08:00
getRegisteredHandler(name: ActionName) {
return this.handlers.get(name);
}
getRegisteredHandlers() {
return this.handlers;
}
getResource(name: string): Resource {
if (!this.resources.has(name)) {
throw new Error(`${name} resource does not exist`);
}
return this.resources.get(name);
}
getAction(name: string, action: ActionName): Action {
// 支持注册局部 action
2020-11-11 22:52:27 +08:00
if (this.handlers.has(`${name}:${action}`)) {
return this.getResource(name).getAction(`${name}:${action}`);
}
2020-10-24 15:34:43 +08:00
return this.getResource(name).getAction(action);
}
getParamsKey() {
return this.paramsKey;
}
getMiddlewares() {
return this.middlewares;
}
use(middlewares: HandlerType | HandlerType[]) {
if (typeof middlewares === 'function') {
this.middlewares.push(middlewares);
} else if (Array.isArray(middlewares)) {
this.middlewares.push(...middlewares);
}
}
middleware(options: KoaMiddlewareOptions = {}) {
const { prefix, accessors, paramsKey = 'params', nameRule = getNameByParams } = options;
return async (ctx: ResourcerContext, next: () => Promise<any>) => {
ctx.resourcer = this;
let params = parseRequest({
path: ctx.request.path,
method: ctx.request.method,
}, {
prefix,
accessors,
});
if (!params) {
return next();
}
try {
const resource = this.getResource(nameRule(params));
// 为关系资源时,暂时需要再执行一遍 parseRequest
if (resource.options.type !== 'single') {
params = parseRequest({
path: ctx.request.path,
method: ctx.request.method,
type: resource.options.type,
}, {
prefix,
accessors,
});
if (!params) {
return next();
}
}
// action 需要 clone 之后再赋给 ctx
ctx.action = this.getAction(nameRule(params), params.actionName).clone();
ctx.action.setContext(ctx);
// 自带 query 处理的不太给力,需要用 qs 转一下
const query = qs.parse(ctx.request.querystring, {
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
strictNullHandling: true
});
2020-10-24 15:34:43 +08:00
// filter 支持 json string
if (typeof query.filter === 'string') {
query.filter = JSON.parse(query.filter);
}
// 兼容 ctx.params 的处理,之后的版本里会去掉
ctx[paramsKey] = {
table: params.resourceName,
tableKey: params.resourceKey,
relatedTable: params.associatedName,
relatedKey: params.resourceKey,
action: params.actionName,
};
if (pathToRegexp('/resourcer/{:associatedName.}?:resourceName{\\::actionName}').test(ctx.request.path)) {
await ctx.action.mergeParams({
...query,
...params,
...ctx.request.body,
});
} else {
await ctx.action.mergeParams({
...query,
...params,
values: ctx.request.body,
});
}
return compose(ctx.action.getHandlers())(ctx, next);
} catch (error) {
return next();
}
}
}
/**
* API
*
* @param options
* @param context
* @param next
*/
async execute(options: ExecuteOptions, context: ResourcerContext = {}, next?: any) {
const { resource, action } = options;
context.resourcer = this;
context.action = this.getAction(resource, action);
return await context.action.execute(context, next);
}
}
export default Resourcer;