- 基于 grill-me Q1-Q61 决策 - Vitest 测试与 CI workflow - middleware: parseFilters 核心实现 - component: Vue3 TransForm 基础组件 + Storybook Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export { DEFAULT_OPERATOR_MAP, mapOperators } from './operatorMap.js';
|
||||
export { splitMultiValue } from './splitMultiValue.js';
|
||||
export { toTree } from './toTree.js';
|
||||
export { parseFilters } from './parseFilters.js';
|
||||
@@ -0,0 +1,25 @@
|
||||
/** @type {Record<string, string>} */
|
||||
export const DEFAULT_OPERATOR_MAP = {
|
||||
EQ: '=',
|
||||
NE: '<>',
|
||||
GT: '>',
|
||||
GTE: '>=',
|
||||
LT: '<',
|
||||
LTE: '<=',
|
||||
LIKE: 'LIKE',
|
||||
IN: 'IN',
|
||||
NOT_IN: 'NOT IN',
|
||||
BETWEEN: 'BETWEEN',
|
||||
IS_NULL: 'IS NULL',
|
||||
IS_NOT_NULL: 'IS NOT NULL',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} query
|
||||
* @param {Record<string, string>} [operatorMap]
|
||||
*/
|
||||
export function mapOperators(query, operatorMap = DEFAULT_OPERATOR_MAP) {
|
||||
const q = String(query).toUpperCase();
|
||||
const merged = { ...DEFAULT_OPERATOR_MAP, ...operatorMap };
|
||||
return merged[q] ?? q;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { mapOperators } from './operatorMap.js';
|
||||
import { splitMultiValue } from './splitMultiValue.js';
|
||||
import { toTree } from './toTree.js';
|
||||
|
||||
const DEFAULT_SKIP_FLAGS = ['disabled', 'hidden'];
|
||||
|
||||
/**
|
||||
* @param {object} item
|
||||
* @param {string[]} skipFlags
|
||||
*/
|
||||
function shouldSkipByFlags(item, skipFlags) {
|
||||
return skipFlags.some((flag) => item[flag] === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{field?: string, value?: *, query?: string, logic?: string, empty?: boolean, disabled?: boolean, hidden?: boolean}>} conditions
|
||||
* @param {{
|
||||
* allowedFields: string[],
|
||||
* mode?: 'pass-through'|'flat'|'tree',
|
||||
* unknownField?: 'error'|'skip',
|
||||
* strict?: boolean,
|
||||
* operatorMap?: Record<string, string>,
|
||||
* skipFlags?: string[],
|
||||
* skipEmpty?: boolean,
|
||||
* }} options
|
||||
*/
|
||||
export function parseFilters(conditions, options) {
|
||||
const {
|
||||
allowedFields,
|
||||
mode = 'pass-through',
|
||||
unknownField = 'error',
|
||||
strict = false,
|
||||
operatorMap = {},
|
||||
skipFlags = DEFAULT_SKIP_FLAGS,
|
||||
skipEmpty = false,
|
||||
} = options;
|
||||
|
||||
if (!Array.isArray(allowedFields) || allowedFields.length === 0) {
|
||||
throw new Error('parseFilters: allowedFields is required');
|
||||
}
|
||||
if (!Array.isArray(conditions)) {
|
||||
if (strict) throw new Error('parseFilters: conditions must be an array');
|
||||
return mode === 'tree' ? [] : [];
|
||||
}
|
||||
|
||||
const allowed = new Set(allowedFields.map(String));
|
||||
const errors = [];
|
||||
const out = [];
|
||||
|
||||
for (let i = 0; i < conditions.length; i++) {
|
||||
const c = conditions[i];
|
||||
if (!c || typeof c !== 'object') {
|
||||
if (strict) errors.push({ index: i, reason: 'invalid item' });
|
||||
continue;
|
||||
}
|
||||
if (shouldSkipByFlags(c, skipFlags)) continue;
|
||||
if (skipEmpty && c.empty === true) continue;
|
||||
|
||||
if (c.field == null || c.query == null) {
|
||||
if (strict) errors.push({ index: i, reason: 'missing field or query' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const field = String(c.field);
|
||||
if (!allowed.has(field)) {
|
||||
if (unknownField === 'error') {
|
||||
throw new Error(`parseFilters: unknown field "${field}"`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const query = String(c.query).toUpperCase();
|
||||
const item = {
|
||||
field,
|
||||
query,
|
||||
op: mapOperators(query, operatorMap),
|
||||
value: c.value ?? null,
|
||||
};
|
||||
|
||||
const parsed = splitMultiValue(query, c.value);
|
||||
if (parsed != null) item.values = parsed;
|
||||
|
||||
if (c.empty === true) item.empty = true;
|
||||
if (c.disabled === true) item.disabled = true;
|
||||
if (c.hidden === true) item.hidden = true;
|
||||
if (c.readOnly === true) item.readOnly = true;
|
||||
if (c.logic != null) item.logic = c.logic;
|
||||
|
||||
out.push(item);
|
||||
}
|
||||
|
||||
if (strict && errors.length) {
|
||||
throw new Error(`parseFilters: ${errors.length} validation error(s)`);
|
||||
}
|
||||
|
||||
if (mode === 'flat') {
|
||||
return out.map(({ logic, ...rest }) => rest);
|
||||
}
|
||||
if (mode === 'tree') {
|
||||
return toTree(out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @param {string} query
|
||||
* @param {string|null|undefined} value
|
||||
* @returns {string[]|[string,string]|null}
|
||||
*/
|
||||
export function splitMultiValue(query, value) {
|
||||
if (value == null || value === '') return null;
|
||||
const s = String(value);
|
||||
const q = String(query).toUpperCase();
|
||||
if (q === 'IN' || q === 'NOT_IN') {
|
||||
return s.split(',').map((v) => v.trim()).filter(Boolean);
|
||||
}
|
||||
if (q === 'BETWEEN') {
|
||||
const parts = s.split(',').map((v) => v.trim());
|
||||
return parts.length >= 2 ? [parts[0], parts[1]] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @param {Array<object>} flat
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function toTree(flat) {
|
||||
if (!flat.length) return [];
|
||||
if (flat.length === 1) return [flat[0]];
|
||||
|
||||
let acc = flat[0];
|
||||
for (let i = 1; i < flat.length; i++) {
|
||||
const logic = flat[i].logic || 'AND';
|
||||
acc = { logic, children: [acc, flat[i]] };
|
||||
}
|
||||
return [acc];
|
||||
}
|
||||
Reference in New Issue
Block a user