forked from yoone/API
141 lines
5.7 KiB
TypeScript
141 lines
5.7 KiB
TypeScript
import { Middleware, IMiddleware } from '@midwayjs/core';
|
|
import { NextFunction, Context } from '@midwayjs/koa';
|
|
import * as qs from 'qs';
|
|
|
|
@Middleware()
|
|
export class QueryNormalizeMiddleware implements IMiddleware<Context, NextFunction> {
|
|
// 数值与布尔转换函数,用于将字符串转换为合适的类型
|
|
private toPrimitive(value: any): any {
|
|
const s = String(value);
|
|
if (s === 'true') return true;
|
|
if (s === 'false') return false;
|
|
const n = Number(s);
|
|
return Number.isFinite(n) && s !== '' ? n : value;
|
|
}
|
|
|
|
// 深度遍历对象并对字符串进行trim
|
|
private trimDeep(input: any): any {
|
|
if (input === null || input === undefined) return input;
|
|
if (typeof input === 'string') return input.trim();
|
|
if (Array.isArray(input)) return input.map(v => this.trimDeep(v));
|
|
if (typeof input === 'object') {
|
|
const out: Record<string, any> = {};
|
|
for (const key of Object.keys(input)) {
|
|
out[key] = this.trimDeep((input as any)[key]);
|
|
}
|
|
return out;
|
|
}
|
|
return input;
|
|
}
|
|
|
|
// 将路径数组对应的值赋到对象中,支持构建嵌套结构与数组
|
|
private assignByPath(target: Record<string, any>, path: string[], value: any): void {
|
|
let cur: any = target;
|
|
for (let i = 0; i < path.length; i++) {
|
|
const key = path[i];
|
|
const isLast = i === path.length - 1;
|
|
if (isLast) {
|
|
if (key === '') {
|
|
if (!Array.isArray(cur)) return;
|
|
cur.push(value);
|
|
} else {
|
|
if (cur[key] === undefined) cur[key] = value;
|
|
else if (Array.isArray(cur[key])) cur[key].push(value);
|
|
else cur[key] = [cur[key], value];
|
|
}
|
|
} else {
|
|
if (!cur[key] || typeof cur[key] !== 'object') cur[key] = {};
|
|
cur = cur[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 解析可能为 JSON 字符串或鍵值串的输入为对象
|
|
private parseLooseObject(input: any): Record<string, any> {
|
|
if (!input) return {};
|
|
if (typeof input === 'object') return input as Record<string, any>;
|
|
const str = String(input).trim();
|
|
try {
|
|
if (str.startsWith('{') || str.startsWith('[')) {
|
|
const json = JSON.parse(str);
|
|
if (json && typeof json === 'object') return json as Record<string, any>;
|
|
}
|
|
} catch {}
|
|
const obj: Record<string, any> = {};
|
|
const pairs = str.split(/[&;,]/).map(s => s.trim()).filter(Boolean);
|
|
for (const pair of pairs) {
|
|
const idxEq = pair.indexOf('=');
|
|
const idxColon = pair.indexOf(':');
|
|
const idx = idxEq >= 0 ? idxEq : idxColon;
|
|
if (idx < 0) continue;
|
|
const key = decodeURIComponent(pair.slice(0, idx)).trim();
|
|
const valueRaw = decodeURIComponent(pair.slice(idx + 1)).trim();
|
|
obj[key] = this.toPrimitive(valueRaw);
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
resolve() {
|
|
return async (ctx: Context, next: NextFunction) => {
|
|
const raw = String((ctx.request as any).querystring || '');
|
|
const parsed = qs.parse(raw, { allowDots: true, depth: 10, ignoreQueryPrefix: false, comma: true });
|
|
const query = { ...(ctx.request.query || {}), ...(parsed as any) } as Record<string, any>;
|
|
const trimmedTop: Record<string, any> = {};
|
|
for (const k of Object.keys(query)) {
|
|
const v = (query as any)[k];
|
|
trimmedTop[k] = typeof v === 'string' ? String(v).trim() : v;
|
|
}
|
|
Object.assign(query, trimmedTop);
|
|
|
|
// 解析 where 对象,支持 JSON 字符串与括号或点号语法
|
|
const hasWhereInput = (query as any).where !== undefined;
|
|
let whereObj: Record<string, any> = this.parseLooseObject((query as any).where);
|
|
for (const k of Object.keys(query)) {
|
|
if (k === 'where') continue;
|
|
if (k.startsWith('where[') || k.startsWith('where.')) {
|
|
const pathStr = k.replace(/^where\.?/, '').replace(/\]/g, '').replace(/\[/g, '.');
|
|
const path = pathStr.split('.');
|
|
const val = this.toPrimitive((query as any)[k]);
|
|
this.assignByPath(whereObj, path, val);
|
|
}
|
|
}
|
|
const hasWhereBracketKeys = Object.keys(query).some(k => k.startsWith('where[') || k.startsWith('where.'));
|
|
if (hasWhereInput || hasWhereBracketKeys) (query as any).where = this.trimDeep(whereObj);
|
|
|
|
// 解析 order 对象,支持 JSON 字符串与括号或点号语法
|
|
const hasOrderInput = (query as any).order !== undefined;
|
|
let orderObj: Record<string, any> = this.parseLooseObject((query as any).order);
|
|
for (const k of Object.keys(query)) {
|
|
if (k === 'order') continue;
|
|
if (k.startsWith('order[') || k.startsWith('order.')) {
|
|
const pathStr = k.replace(/^order\.?/, '').replace(/\]/g, '').replace(/\[/g, '.');
|
|
const path = pathStr.split('.');
|
|
const val = this.toPrimitive((query as any)[k]);
|
|
this.assignByPath(orderObj, path, val);
|
|
}
|
|
}
|
|
const hasOrderBracketKeys = Object.keys(query).some(k => k.startsWith('order[') || k.startsWith('order.'));
|
|
if (hasOrderInput || hasOrderBracketKeys) (query as any).order = this.trimDeep(orderObj);
|
|
|
|
// 将常见分页参数转换为数字类型
|
|
if (query.page !== undefined) (query as any).page = Number(query.page);
|
|
if ((query as any).page_size !== undefined) (query as any).page_size = Number((query as any).page_size);
|
|
if ((query as any).per_page !== undefined) (query as any).per_page = Number((query as any).per_page);
|
|
if ((query as any).customer_id !== undefined) (query as any).customer_id = Number((query as any).customer_id);
|
|
|
|
ctx.request.query = query as any;
|
|
(ctx as any).query = query as any;
|
|
return await next();
|
|
};
|
|
}
|
|
|
|
static getName(): string {
|
|
return 'queryNormalize';
|
|
}
|
|
|
|
static getPriority(): number {
|
|
// 优先级靠前,优先处理查询参数
|
|
return -1;
|
|
}
|
|
}
|