agnostic-query
Version:
Type-safe fluent builder for portable query schemas. Runtime-agnostic, database-agnostic — the same QuerySchema drives Drizzle, Kysely, db0, or raw SQL.
151 lines (150 loc) • 3.89 kB
JavaScript
//#region src/core/where.ts
const unaryComparisonOps = [
"=",
">",
">=",
"<",
"<=",
"like",
"ilike"
];
const multiComparisonOps = ["in"];
const setComparisonOps = [
"@>",
"<@",
"&&"
];
const predicateOps = ["is null"];
const multiLogicalWhereOps = ["and", "or"];
const unaryLogicalWhereOp = "not";
const newComparisonWhere = () => (col, op, value) => buildInputWhere(col, op, value);
const buildInputWhere = (col, op, value) => {
const field = Array.isArray(col) ? col : [col];
if (op === "in") return {
field,
op,
values: value
};
if (op === "is null") return {
field,
op
};
return {
field,
op,
value
};
};
const mergeWhere = (existing, next) => {
if (!existing) return next;
if (existing.op === "and") return {
op: "and",
conditions: [...existing.conditions || [], next]
};
return {
op: "and",
conditions: [existing, next]
};
};
/**
* 类型守卫:将 `QueryWhere` 收窄为 `ComparisonWhere`。
*
* TS 无法通过 `op === 'and' || op === 'or'` 的否定方向消除
* `MultiLogicalWhere`(其 discriminant `op` 是 `'and' | 'or'` 联合类型),
* 导致 `field` / `value` / `values` 在后继代码中不可被类型访问。
* 此守卫通过显式排除逻辑运算符来绕过该限制。
*/
const isComparisonWhere = (where) => where.op !== "not" && where.op !== "and" && where.op !== "or";
const createExpr = (q) => {
return {
_q: q,
where(col, op, value) {
if (col === null || col === void 0) return createExpr(q);
if (col && typeof col === "object" && "op" in col) return createExpr(col);
const field = Array.isArray(col) ? col : [col];
return createExpr(op === "in" ? {
field,
op,
values: value
} : op === "is null" ? {
field,
op
} : {
field,
op,
value
});
},
and(exprs) {
return createExpr({
op: "and",
conditions: exprs.map((e) => e._q).filter(Boolean)
});
},
or(exprs) {
return createExpr({
op: "or",
conditions: exprs.map((e) => e._q).filter(Boolean)
});
},
not(expr) {
if (expr._q === null || expr._q === void 0) return createExpr();
return createExpr({
op: "not",
condition: expr._q
});
}
};
};
const newWhere = (state) => {
const where = (col, op, value) => {
return newWhere(mergeWhere(state, buildInputWhere(col, op, value)));
};
return {
toJSON: () => state,
where: (col, op, value) => {
if (col === null || col === void 0) return newWhere(state);
if (typeof col === "function") {
const cbWhere = col(createExpr())._q;
return newWhere(state ? {
op: "and",
conditions: [state, cbWhere]
} : cbWhere);
}
if (col && typeof col === "object" && "op" in col) {
const whereObj = col;
return newWhere(state ? {
op: "and",
conditions: [state, whereObj]
} : whereObj);
}
return where(col, op, value);
}
};
};
const fieldEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
const isComparisonNode = (node, field) => "field" in node && fieldEqual(node.field, field);
const findWhere = (where) => {
const search = (field, op) => {
if (!where) return;
const fieldPath = Array.isArray(field) ? field : [field];
const walk = (node) => {
if (isComparisonNode(node, fieldPath)) {
if (!op || node.op === op) return node;
}
if (node.op === "not") return walk(node.condition);
if (node.op === "and" || node.op === "or") for (const sub of node.conditions) {
const found = walk(sub);
if (found) return found;
}
};
return walk(where);
};
return {
eq: (field) => search(field, "="),
in: (field) => search(field, "in"),
find: search
};
};
//#endregion
export { buildInputWhere, createExpr, findWhere, isComparisonWhere, mergeWhere, multiComparisonOps, multiLogicalWhereOps, newComparisonWhere, newWhere, predicateOps, setComparisonOps, unaryComparisonOps, unaryLogicalWhereOp };