tachybase_todo/packages/resourcer/src/action.ts

362 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import _, { isArray } from 'lodash';
import compose from 'koa-compose';
import Resource from './resource';
import { requireModule, mergeFields } from './utils';
import { HandlerType } from './resourcer';
import Middleware, { MiddlewareType } from './middleware';
export type ActionType = string | HandlerType | ActionOptions;
export type DefaultActionType = 'list' | 'create' | 'get' | 'update' | 'destroy' | 'set' | 'add' | 'remove';
export type ActionName = DefaultActionType | Omit<String, DefaultActionType>;
export interface ActionContext {
action?: Action;
[key: string]: any;
}
export type FieldsOptions = string[] | {
only?: string[];
appends?: string[];
} | {
except?: string[];
appends?: string[];
};
export type FieldsOptionsFn = (ctx: ActionContext) => FieldsOptions | Promise<FieldsOptions>;
/**
* 过滤参数
*
* TODO细节待定
*/
export interface FilterOptions {
[key: string]: any;
}
export type FilterOptionsFn = (ctx: ActionContext) => FilterOptions | Promise<FieldsOptions>;
export type ParamsCallback = (ctx: ActionContext) => ActionParams | Promise<ActionParams>;
export interface ActionOptions {
/**
* 默认数据
*/
defaultValues?: any;
/**
* 字段
*
* 示例一:
* ['col1', 'col2', 'relation.col1'];
*
* 示例二:
* {
* only: ['col1'],
* }
*
* 示例三:
* {
* except: ['col1'],
* }
*/
fields?: FieldsOptions;
/**
* 过滤
*/
filter?: FilterOptions;
/**
* 排序
*/
sort?: string | string[];
/**
* 当前页码
*/
page?: number;
/**
* 每页显示数量
*/
perPage?: number;
/**
* 最大每页显示数量
*/
maxPerPage?: number;
/**
* 中间件
*/
middleware?: MiddlewareType;
/**
* 中间件
*
* 与 middleware 用法相同
*/
middlewares?: MiddlewareType;
/**
* 当前 Action 待执行的方法
*
* 支持 Function 和 require 调用
*/
handler?: HandlerType;
/**
* 其他扩展配置
*/
[key: string]: any;
}
/**
* action params 与 action options 略有不同
* - options 的参数更灵活,主要用于开发配置
* - params 是开发配置参数 + 客户端参数的结合体
*/
export interface ActionParams {
/**
* 输出哪些字段
*
* 与 ActionOptions 的不同,这里的 fields 是 object提供 onlyexceptappends 三种情况
*/
fields?: {
only?: string[];
except?: string[];
appends?: string[];
};
/**
* 过滤
*/
filter?: FilterOptions;
/**
* 排序
*
* 与 ActionOptions 的不同,这里的 sort 只有一种 array 类型
*/
sort?: string[];
/**
* 当前页码
*/
page?: number;
/**
* 每页显示数量
*/
perPage?: number;
/**
* 数据,默认为 options.defaultValues + request.body
*/
values?: any;
/**
* 当前资源的主体,对应的表名或 Model 名称
*/
resourceName?: string;
/**
* 资源标识符
*/
resourceKey?: string;
/**
* 资源的从属关系
*/
associatedName?: string;
/**
* 从属关系的标识符
*/
associatedKey?: string;
/**
* 从属关系的当前实例
*/
associated ?: any;
/**
* 资源提供哪些行为或方法
*/
actionName?: string;
/**
* 其他扩展配置
*/
[key: string]: any;
}
export const DEFAULT_PAGE = 1;
export const DEFAULT_PER_PAGE = 20;
export const MAX_PER_PAGE = 100;
export class Action {
protected handler: any;
protected resource: Resource;
protected name: ActionName;
protected options: ActionOptions;
protected parameters: ActionParams = {};
protected context: ActionContext = {};
public readonly middlewares: Array<Middleware> = [];
constructor(options: ActionOptions) {
options = requireModule(options);
if (typeof options === 'function') {
options = { handler: options };
}
const { middleware, middlewares = [], handler } = options;
this.middlewares = Middleware.toInstanceArray(middleware || middlewares);
this.handler = handler;
this.options = options;
}
get params(): ActionParams {
return this.parameters;
}
clone() {
const options = _.cloneDeep(this.options);
delete options.middleware;
delete options.middlewares;
const action = new Action(options);
action.setName(this.name);
action.setResource(this.resource);
action.middlewares.push(...this.middlewares);
return action;
}
setContext(context: any) {
this.context = context;
}
setParam(key: string, value: any) {
if (/\[\]$/.test(key)) {
key = key.substr(0, key.length - 2);
let values = _.get(this.parameters, key);
if (_.isArray(values)) {
values.push(value);
} else {
values = [];
values.push(value);
}
value = values;
}
_.set(this.parameters, key, value);
}
async mergeParams(params: ActionParams) {
const {
filter,
fields,
values,
page: paramPage,
perPage: paramPerPage,
per_page,
...restPrams
} = params;
const {
filter: optionsFilter,
fields: optionsFields,
page = DEFAULT_PAGE,
perPage = DEFAULT_PER_PAGE,
maxPerPage = MAX_PER_PAGE
} = this.options;
const options = _.omit(this.options, [
'defaultValues',
'filter',
'fields',
'maxPerPage',
'page',
'perPage',
'handler',
'middlewares',
'middleware',
]);
const data: ActionParams = {
...options,
...restPrams,
};
if (!_.isEmpty(this.options.defaultValues) || !_.isEmpty(values)) {
data.values = _.merge(_.cloneDeep(this.options.defaultValues), values);
}
// TODO: to be unified by style funciton
if (per_page || paramPerPage) {
data.perPage = per_page || paramPerPage;
}
if (paramPage || data.perPage) {
data.page = paramPage || page;
data.perPage = data.perPage == -1 ? maxPerPage : Math.min(data.perPage || perPage, maxPerPage);
}
// if (typeof optionsFilter === 'function') {
// this.parameters = _.cloneDeep(data);
// optionsFilter = await optionsFilter(this.context);
// }
if (!_.isEmpty(optionsFilter) || !_.isEmpty(filter)) {
const filterOptions = [_.cloneDeep(optionsFilter), filter].filter(Boolean);
// TODO(feature): change 'and' to symbol
data.filter = filterOptions.length > 1 ? { and: filterOptions } : filterOptions[0];
}
// if (typeof optionsFields === 'function') {
// this.parameters = _.cloneDeep(data);
// optionsFields = await optionsFields(this.context);
// }
if (!_.isEmpty(optionsFields) || !_.isEmpty(fields)) {
data.fields = mergeFields(optionsFields, fields);
}
this.parameters = _.cloneDeep(data);
}
setResource(resource: Resource) {
this.resource = resource;
return this;
}
getResource() {
return this.resource;
}
getOptions(): ActionOptions {
return this.options;
}
setName(name: ActionName) {
this.name = name;
return this;
}
getName() {
return this.name;
}
getMiddlewareHandlers() {
return this.middlewares
.filter(middleware => middleware.canAccess(this.name))
.map(middleware => middleware.getHandler());
}
getHandler() {
const handler = requireModule(this.handler || this.resource.resourcer.getRegisteredHandler(this.name));
if (typeof handler !== 'function') {
throw new Error('Handler must be a function!');
}
return handler;
}
getHandlers() {
return [...this.resource.resourcer.getMiddlewares(),...this.getMiddlewareHandlers(), this.getHandler()].filter(Boolean);
}
async execute(context: any, next?: any) {
return await compose(this.getHandlers())(context, next);
}
static toInstanceMap(actions: object, resource?: Resource) {
return new Map(Object.entries(actions).map(([key, options]) => {
let action: Action;
if (options instanceof Action) {
action = options;
} else {
action = new Action(options);
}
action.setName(key);
action.setResource(resource);
resource && action.middlewares.unshift(...resource.middlewares);
return [key, action];
}));
}
}
export default Action;