tachybase_todo/packages/resourcer/src/parameter.ts
2021-03-28 13:34:51 +08:00

298 lines
8.0 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 _ 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 {
fields.except = _.uniq([...inputs.except, ...(defaults.except || [])]);
}
}
// 前端没提供 only 或 except
else {
fields = defaults;
}
// 如果前端提供了 appends
if (!_.isEmpty(inputs.appends)) {
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) {
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);