tachybase_todo/packages/resourcer/src/parameter.ts

298 lines
8.0 KiB
TypeScript
Raw Normal View History

import _ from 'lodash';
const types = new Map<string, typeof ActionParameter>();
export class ActionParameterTypes {
static register(type: Exclude<string, '_'>, Type: typeof ActionParameter) {
types.set(type, Type);
}
static get(type: string): typeof ActionParameter {
return types.get(type);
}
static getAllKeys(): Iterable<string> {
return types.keys();
}
static getAllTypes(): Iterable<typeof ActionParameter> {
return types.values();
}
}
// TODO(optimize) 当前要兼容 resourcer 和 action 已有的参数形式
// 更好的情况可以定义参数从 ctx 对象中的来源路径,适应性可能更好
export abstract class ActionParameter {
// TODO(optimize): 设计稍微有点奇怪
// 选取未来传入参数集中的哪些键,同时值为默认值
static picking: { [key: string]: any }
// 默认逻辑是直接选取传入对象中的对应键
static pick(params: { [key: string]: any }) {
const result: { [key: string]: any } = {};
Object.keys(this.picking).forEach(key => {
if (typeof this.picking[key] !== 'undefined') {
result[key] = this.picking[key];
}
if (typeof params[key] !== 'undefined') {
result[key] = params[key];
}
});
return result;
}
// 暂时以对象加一层包装,以便类似 pager 这样有多个参数的情况
params: { [key: string]: any } = {};
constructor(params) {
this.merge(params);
}
get() {
return _.cloneDeep(this.params);
}
merge(params, strategy?: string): void {
this.params = ActionParameter.pick(params);
}
}
export class FieldsParameter extends ActionParameter {
static picking = { fields: {} };
static parse(fields: any) {
if (!fields) {
return {}
}
if (typeof fields === 'string') {
fields = fields.split(',').map(field => field.trim());
}
if (Array.isArray(fields)) {
const onlyFields = [];
const output: any = {};
fields.forEach(item => {
if (typeof item === 'string') {
onlyFields.push(item);
} else if (typeof item === 'object') {
if (item.only) {
onlyFields.push(...item.only.toString().split(','));
}
Object.assign(output, this.parse(item));
}
});
if (onlyFields.length) {
output.only = onlyFields;
}
return output;
}
if (fields.only && typeof fields.only === 'string') {
fields.only = fields.only.split(',').map(field => field.trim());
}
if (fields.except && typeof fields.except === 'string') {
fields.except = fields.except.split(',').map(field => field.trim());
}
if (fields.appends && typeof fields.appends === 'string') {
fields.appends = fields.appends.split(',').map(field => field.trim());
}
return fields;
}
static intersect(defaults: any, inputs: any) {
let fields: any = {};
defaults = this.parse(defaults);
inputs = this.parse(inputs);
if (inputs.only) {
// 前端提供 only后端提供 only
if (defaults.only) {
fields.only = defaults.only.filter(field => inputs.only.includes(field));
}
// 前端提供 only后端提供 except输出 only 排除 except
else if (defaults.except) {
fields.only = inputs.only.filter(field => !defaults.except.includes(field));
}
// 前端提供 only后端没有提供 only 或 except
else {
fields.only = inputs.only;
}
} else if (inputs.except) {
// 前端提供 except后端提供 only只输出 only 里排除 except 的字段
if (defaults.only) {
fields.only = defaults.only.filter(field => !inputs.except.includes(field));
}
// 前端提供 except后端提供 except 或不提供,合并 except
else {
2021-03-28 13:34:51 +08:00
fields.except = _.uniq([...inputs.except, ...(defaults.except || [])]);
}
}
// 前端没提供 only 或 except
else {
fields = defaults;
}
// 如果前端提供了 appends
if (!_.isEmpty(inputs.appends)) {
2021-03-28 13:34:51 +08:00
fields.appends = _.uniq([...inputs.appends, ...(defaults.appends || [])]);
}
if (!fields.appends) {
fields.appends = [];
}
return fields;
}
static append(defaults, inputs) {
let fields: any = {};
defaults = this.parse(defaults);
inputs = this.parse(inputs);
['only', 'except', 'appends'].forEach(key => {
if (defaults[key] && defaults[key].length || inputs[key] && inputs[key].length) {
fields[key] = _.uniq([...(defaults[key] || []), ...(inputs[key] || [])]);
}
});
return fields;
}
params = { fields: {} };
merge(params, strategy: 'intersect' | 'append' | 'replace' = 'intersect') {
switch (strategy) {
case 'intersect':
this.params = {
fields: FieldsParameter.intersect(this.params.fields, FieldsParameter.pick(params).fields)
};
break;
case 'append':
this.params = {
fields: FieldsParameter.append(this.params.fields, FieldsParameter.pick(params).fields)
};
break;
default:
throw new Error('not implemented yet');
}
}
}
export class FilterParameter extends ActionParameter {
static picking = { filter: {} };
params = { filter: {} };
merge(params, strategy: 'and' | 'or' = 'and') {
const { filter } = FilterParameter.pick(params);
if (!filter || _.isEmpty(filter)) {
return;
}
if (_.isEmpty(this.params.filter)) {
this.params.filter = filter;
} else {
this.params.filter = {
[strategy]: [this.params.filter, filter]
};
}
}
}
export class PageParameter extends ActionParameter {
static picking = { page: undefined, perPage: undefined, per_page: undefined };
static DEFAULT_PAGE = 1;
static DEFAULT_PER_PAGE = 20;
static MAX_PER_PAGE = 100;
params = { page: undefined, perPage: undefined };
maxPerPage = PageParameter.MAX_PER_PAGE;
constructor(options = { maxPerPage: PageParameter.MAX_PER_PAGE }) {
super(options);
if (typeof options.maxPerPage !== 'undefined') {
this.maxPerPage = options.maxPerPage;
}
}
merge(params) {
const data = PageParameter.pick(params);
if (typeof params.per_page !== 'undefined') {
data.perPage = params.per_page;
}
if (data.page || data.perPage) {
2021-03-28 13:34:51 +08:00
data.page = data.page || this.params.page || PageParameter.DEFAULT_PAGE;
data.perPage = data.perPage == -1 ? this.maxPerPage : Math.min(data.perPage || this.params.perPage || PageParameter.DEFAULT_PER_PAGE, this.maxPerPage);
Object.assign(this.params, data);
}
}
}
export class PayloadParameter extends ActionParameter {
static picking = { values: undefined };
merge(params, strategy: 'replace' | 'merge' | 'intersect' = 'merge') {
const data = PayloadParameter.pick(params);
switch (strategy) {
case 'replace':
this.params.values = data.values;
break;
case 'merge':
_.merge(this.params, _.cloneDeep(data));
break;
default:
throw new Error('not implemented yet');
}
}
}
export class UnknownParameter {
params: { [key: string]: any } = {};
constructor(options) {
this.merge(options);
}
get() {
return _.cloneDeep(this.params);
}
merge(params, strategy: 'replace' | 'merge' | 'intersect' = 'merge') {
const knownKeys = new Set<string>();
for (const key of ActionParameterTypes.getAllKeys()) {
Object.keys(ActionParameterTypes.get(key).picking).forEach(key => knownKeys.add(key));
}
const data = _.omit(params, Array.from(knownKeys));
switch (strategy) {
case 'merge':
_.merge(this.params, _.cloneDeep(data));
break;
case 'replace':
this.params = data;
break;
default:
throw new Error('not implemented yet');
}
}
}
ActionParameterTypes.register('fields', FieldsParameter);
ActionParameterTypes.register('filter', FilterParameter);
ActionParameterTypes.register('page', PageParameter);
ActionParameterTypes.register('payload', PayloadParameter);