nocobase2/packages/core/resourcer/src/utils.ts
jack zhang 62b2b5c68b
chore: add copyright information to the file header (#4028)
* fix: add license code

* fix: bug

* fix: bug

* fix: upgrade

* fix: improve

* chore: add copyright information to the file header

* fix: d.ts bug

* fix: bug

* fix: e2e bug

* fix: merge main

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2024-04-30 15:51:31 +08:00

302 lines
8.3 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.

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import _ from 'lodash';
// @ts-ignore
import { pathToRegexp } from 'path-to-regexp';
import qs from 'qs';
import { ResourceType } from './resource';
export interface ParseRequest {
path: string;
method: string;
namespace?: string;
// 资源类型
type?: ResourceType;
}
export interface ParseOptions {
prefix?: string;
accessors?: {
list?: string;
create?: string;
get?: string;
update?: string;
delete?: string;
set?: string;
add?: string;
};
}
export interface ParsedParams {
actionName?: string;
resourceName?: string;
resourceIndex?: string;
associatedName?: string;
associatedIndex?: string;
// table: string;
// tableKey?: string | number;
// relatedTable?: string;
// relatedKey?: string | number;
// action?: ActionName,
}
export function getNameByParams(params: ParsedParams): string {
const { resourceName, associatedName } = params;
return associatedName ? `${associatedName}.${resourceName}` : resourceName;
}
export function parseRequest(request: ParseRequest, options: ParseOptions = {}): ParsedParams | false {
const accessors = {
// 常规 actions
list: 'list',
create: 'create',
get: 'get',
update: 'update',
delete: 'destroy',
// associate 操作
add: 'add',
set: 'set',
remove: 'remove',
...(options.accessors || {}),
};
const keys = [];
const regexp = pathToRegexp('/resourcer/{:associatedName.}?:resourceName{\\::actionName}', keys);
const reqPath = decodeURI(request.path);
const matches = regexp.exec(reqPath);
if (matches) {
const params = {};
keys.forEach((obj, index) => {
if (matches[index + 1] === undefined) {
return;
}
params[obj.name] = matches[index + 1];
});
return params;
}
const defaults = {
single: {
'/:resourceName': {
get: accessors.list,
post: accessors.create,
delete: accessors.delete,
},
'/:resourceName/:resourceIndex': {
get: accessors.get,
put: accessors.update,
patch: accessors.update,
delete: accessors.delete,
},
'/:associatedName/:associatedIndex/:resourceName': {
get: accessors.list,
post: accessors.create,
delete: accessors.delete,
},
'/:associatedName/:associatedIndex/:resourceName/:resourceIndex': {
get: accessors.get,
post: accessors.create,
put: accessors.update,
patch: accessors.update,
delete: accessors.delete,
},
},
hasOne: {
'/:associatedName/:associatedIndex/:resourceName': {
get: accessors.get,
post: accessors.update,
put: accessors.update,
patch: accessors.update,
delete: accessors.delete,
},
},
hasMany: {
'/:associatedName/:associatedIndex/:resourceName': {
get: accessors.list,
post: accessors.create,
delete: accessors.delete,
},
'/:associatedName/:associatedIndex/:resourceName/:resourceIndex': {
get: accessors.get,
post: accessors.create,
put: accessors.update,
patch: accessors.update,
delete: accessors.delete,
},
},
belongsTo: {
'/:associatedName/:associatedIndex/:resourceName': {
get: accessors.get,
delete: accessors.remove,
},
'/:associatedName/:associatedIndex/:resourceName/:resourceIndex': {
post: accessors.set,
},
},
belongsToMany: {
'/:associatedName/:associatedIndex/:resourceName': {
get: accessors.list,
post: accessors.set,
},
'/:associatedName/:associatedIndex/:resourceName/:resourceIndex': {
get: accessors.get,
post: accessors.add,
put: accessors.update, // Many to Many 的 update 是针对 through
patch: accessors.update, // Many to Many 的 update 是针对 through
delete: accessors.remove,
},
},
set: {
'/:associatedName/:associatedIndex/:resourceName': {
get: accessors.list,
post: accessors.add,
delete: accessors.remove,
},
},
};
const params: ParsedParams = {};
let prefix = (options.prefix || '').trim().replace(/\/$/, '');
if (prefix && !prefix.startsWith('/')) {
prefix = `/${prefix}`;
}
const { type = 'single' } = request;
for (const path in defaults[type]) {
const keys = [];
const regexp = pathToRegexp(`${prefix}${path}`, keys, {});
const matches = regexp.exec(reqPath);
if (!matches) {
continue;
}
keys.forEach((obj, index) => {
if (matches[index + 1] === undefined) {
return;
}
params[obj.name] = matches[index + 1];
});
params.actionName = _.get(defaults, [type, path, request.method.toLowerCase()]);
}
if (Object.keys(params).length === 0) {
return false;
}
if (params.resourceName) {
const [resourceName, actionName] = params.resourceName.split(':');
if (actionName) {
params.resourceName = resourceName;
params.actionName = actionName;
}
}
if (params.associatedIndex) {
params.associatedIndex = decodeURIComponent(params.associatedIndex);
}
return params;
}
export function parseQuery(input: string): any {
// 自带 query 处理的不太给力,需要用 qs 转一下
const query = qs.parse(input, {
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
strictNullHandling: true,
// 逗号分隔转换为数组
// comma: true,
});
// filter 支持 json string
if (typeof query.filter === 'string') {
query.filter = JSON.parse(query.filter);
}
return query;
}
export function parseFields(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, parseFields(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;
}
export function mergeFields(defaults: any, inputs: any) {
let fields: any = {};
defaults = parseFields(defaults);
inputs = parseFields(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;
}