UNPKG

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.

381 lines (360 loc) 11.4 kB
import type { FieldPathByShape, GetPathType, SchemaShape } from './schema.ts'; export const unaryComparisonOps = [ '=', // eq, equal '>', // gt, greater than '>=', // gte, greater than or equal '<', // lt, less than '<=', // lte, less than or equal 'like', 'ilike', ] as const; export type UnaryComparisonOp = (typeof unaryComparisonOps)[number]; export const toMultiComparisonOps = ['in'] as const; export const setComparisonOps = [ '@>', // a contains b, eg: [1, 2, 3] @> [2, 3]; meta contains { "key": "value" } '<@', // b contains a eg: [2, 3] <@ [1, 2, 3] '&&', //overlap eg: [1, 2] && [2, 3] ] as const; export type SetComparisonOp = (typeof setComparisonOps)[number]; export type WhereComparisonOp = | UnaryComparisonOp | 'is null' | 'in' | SetComparisonOp; export const predicateOps = ['is null'] as const; export type PredicateOp = (typeof predicateOps)[number]; export const multiLogicalWhereOps = ['and', 'or'] as const; export type MultiLogicalWhereOp = (typeof multiLogicalWhereOps)[number]; export const unaryLogicalWhereOp = 'not'; export type UnaryLogicalWhereOp = typeof unaryLogicalWhereOp; export type WhereOp = | UnaryComparisonOp | SetComparisonOp | 'in' | PredicateOp | MultiLogicalWhereOp | UnaryLogicalWhereOp; export type UnaryComparisonWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = { field: TField; op: UnaryComparisonOp; value: GetPathType<TShape, TField>; }; export type SetComparisonWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = { field: TField; op: SetComparisonOp; value: GetPathType<TShape, TField>; }; export type ToMultiComparisonWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = { field: TField; op: 'in'; values: GetPathType<TShape, TField>[]; }; export type PredicateWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = { field: TField; op: PredicateOp; }; export type ComparisonWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = | PredicateWhere<TShape, TField> | UnaryComparisonWhere<TShape, TField> | SetComparisonWhere<TShape, TField> | ToMultiComparisonWhere<TShape, TField>; export type ComparisonWhereValue< TShape extends SchemaShape, Col extends (keyof TShape & string) | FieldPathByShape<TShape>, Op extends WhereComparisonOp, > = Op extends 'in' ? Col extends keyof TShape & string ? TShape[Col] extends readonly any[] ? 'in is not allowed on array fields' : TShape[Col][] : Col extends FieldPathByShape<TShape> ? GetPathType<TShape, Col> extends readonly any[] ? 'in is not allowed on array fields' : GetPathType<TShape, Col>[] : never : Op extends PredicateOp ? never : Col extends keyof TShape & string ? TShape[Col] : Col extends FieldPathByShape<TShape> ? GetPathType<TShape, Col> : never; export const newComparisonWhere = <TShape extends SchemaShape>() => < Col extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends WhereComparisonOp, >( col: Col, op: Op, value: ComparisonWhereValue<TShape, Col, Op>, ) => { const field = Array.isArray(col) ? col : [col]; const inputWhere = op === 'in' ? { field, op, values: value } : { field, op, value }; return inputWhere as ComparisonWhere<TShape>; }; /** * 类型守卫:将 `QueryWhere` 收窄为 `ComparisonWhere`。 * * TS 无法通过 `op === 'and' || op === 'or'` 的否定方向消除 * `MultiLogicalWhere`(其 discriminant `op` 是 `'and' | 'or'` 联合类型), * 导致 `field` / `value` / `values` 在后继代码中不可被类型访问。 * 此守卫通过显式排除逻辑运算符来绕过该限制。 */ export const isComparisonWhere = <TShape extends SchemaShape>( where: QueryWhere<TShape>, ): where is ComparisonWhere<TShape> => where.op !== 'not' && where.op !== 'and' && where.op !== 'or'; export type UnaryLogicalWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = { op: 'not'; condition: QueryWhere<TShape, TField>; }; export type MultiLogicalWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = { op: MultiLogicalWhereOp; conditions: QueryWhere<TShape, TField>[]; }; export type QueryWhere< TShape extends SchemaShape = SchemaShape, TField extends FieldPathByShape<TShape> = FieldPathByShape<TShape>, > = | UnaryComparisonWhere<TShape, TField> | SetComparisonWhere<TShape, TField> | ToMultiComparisonWhere<TShape, TField> | PredicateWhere<TShape, TField> | MultiLogicalWhere<TShape, TField> | UnaryLogicalWhere<TShape, TField>; export interface WhereExpr<TShape extends SchemaShape> { _q: QueryWhere<TShape> | null; where< Col extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends WhereComparisonOp, >( col: Col, op: Op, value: ComparisonWhereValue<TShape, Col, Op>, ): WhereExpr<TShape>; where< Col extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends PredicateOp, >(col: Col, op: Op): WhereExpr<TShape>; where(where?: QueryWhere<TShape> | null): WhereExpr<TShape>; and(conditions: WhereExpr<TShape>[]): WhereExpr<TShape>; or(conditions: WhereExpr<TShape>[]): WhereExpr<TShape>; not(condition: WhereExpr<TShape>): WhereExpr<TShape>; } export const createExpr = <TShape extends SchemaShape>( q?: QueryWhere<TShape> | null, ): WhereExpr<TShape> => { const expr = { _q: q, where< Col extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends WhereComparisonOp, >(col: Col, op: Op, value: ComparisonWhereValue<TShape, Col, Op>) { if (col === null || col === undefined) { return createExpr(q); } if (col && typeof col === 'object' && 'op' in col) { return createExpr(col as unknown as QueryWhere<TShape>); } const field = Array.isArray(col) ? col : [col]; const inputWhere = op === 'in' ? { field, op, values: value } : op === 'is null' ? { field, op } : { field, op, value }; return createExpr(inputWhere as QueryWhere<TShape>); }, and(exprs: WhereExpr<TShape>[]) { return createExpr({ op: 'and', conditions: exprs .map((e) => e._q) .filter(Boolean) as QueryWhere<TShape>[], }); }, or(exprs: WhereExpr<TShape>[]) { return createExpr({ op: 'or', conditions: exprs .map((e) => e._q) .filter(Boolean) as QueryWhere<TShape>[], }); }, not(expr: WhereExpr<TShape>) { if (expr._q === null || expr._q === undefined) { return createExpr(); } return createExpr({ op: 'not', condition: expr._q }); }, }; return expr as WhereExpr<TShape>; }; interface NewWhere<TShape extends SchemaShape = SchemaShape> { toJSON(): QueryWhere<TShape> | null | undefined; where(cb: (eb: WhereExpr<TShape>) => WhereExpr<TShape>): NewWhere<TShape>; where< Col extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends WhereComparisonOp, >( col: Col, op: Op, value: ComparisonWhereValue<TShape, Col, Op>, ): NewWhere<TShape>; where< Col extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends PredicateOp, >(col: Col, op: Op): NewWhere<TShape>; where(where?: QueryWhere<TShape> | null): NewWhere<TShape>; } export const newWhere = <TShape extends SchemaShape>( state?: QueryWhere<TShape> | null, ): NewWhere<TShape> => { const where = < Col extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends WhereComparisonOp, >( col: Col, op: Op, value: ComparisonWhereValue<TShape, Col, Op>, ) => { const field = Array.isArray(col) ? col : [col]; const inputWhere = op === 'in' ? { field, op, values: value } : op === 'is null' ? { field, op } : { field, op, value }; const oldWheres = state?.op === 'and' ? state.conditions || [] : state ? [state] : []; const changedWhere = state ? { op: 'and', conditions: [...oldWheres, inputWhere], } : inputWhere; return newWhere<TShape>(changedWhere as QueryWhere<TShape>); }; return { toJSON: () => state, where: (col: any, op?: any, value?: any) => { if (col === null || col === undefined) { return newWhere<TShape>(state); } if (typeof col === 'function') { const cbWhere = (col as (eb: WhereExpr<TShape>) => WhereExpr<TShape>)( createExpr(), )._q; const changedWhere = state ? { op: 'and', conditions: [state, cbWhere] } : cbWhere; return newWhere<TShape>(changedWhere as QueryWhere<TShape>); } // 新增:col 是 QueryWhere 对象 if (col && typeof col === 'object' && 'op' in col) { const changedWhere: QueryWhere<TShape> = state ? { op: 'and', conditions: [state, col] } : col; return newWhere<TShape>(changedWhere); } return where(col, op, value); }, }; }; const fieldEqual = (a: readonly any[], b: readonly any[]): boolean => a.length === b.length && a.every((v, i) => v === b[i]); const isComparisonNode = <TField extends readonly any[]>( node: QueryWhere, field: TField, ): node is ComparisonWhere => 'field' in node && fieldEqual(node.field, field); export const findWhere = <TShape extends SchemaShape>( where?: QueryWhere<TShape> | null, ) => { const search = < TField extends FieldPathByShape<TShape> | (keyof TShape & string), Op extends WhereComparisonOp, >( field: TField, op?: Op, ) => { if (!where) return; const fieldPath = Array.isArray(field) ? field : [field]; type NormalizeField< TShape extends SchemaShape, Col extends (keyof TShape & string) | FieldPathByShape<TShape>, > = (Col extends FieldPathByShape<TShape> ? Col : [Col] & FieldPathByShape<TShape>) & FieldPathByShape<TShape>; type ReturnNode< TShape extends SchemaShape, Col extends (keyof TShape & string) | FieldPathByShape<TShape>, Op extends WhereComparisonOp, > = Op extends 'in' ? ToMultiComparisonWhere<TShape, NormalizeField<TShape, Col>> : Op extends SetComparisonOp ? SetComparisonWhere<TShape, NormalizeField<TShape, Col>> : Op extends PredicateOp ? PredicateWhere<TShape, NormalizeField<TShape, Col>> : UnaryComparisonWhere<TShape, NormalizeField<TShape, Col>>; const walk = ( node: QueryWhere<TShape>, ): ReturnNode<TShape, TField, Op> | undefined => { if (isComparisonNode(node, fieldPath)) { if (!op || node.op === op) return node as ReturnNode<TShape, TField, Op>; } 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: <TField extends FieldPathByShape<TShape>>(field: TField) => search(field, '='), in: <TField extends FieldPathByShape<TShape>>(field: TField) => search(field, 'in'), find: search, }; }; // type DemoShape = { // id: number; // name: string; // tags: { id: number; name: string }[]; // category: string[]; // address: { // city: { // name: string; // }; // }; // }; // const where = {} as QueryWhere<DemoShape>; // findWhere(where).find('address', '=')?.value;