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.

101 lines (89 loc) 3.17 kB
import type { QuerySchema } from '../core'; import type { QueryOrderBy } from '../core/order-by.ts'; import type { FieldPath, SchemaShape } from '../core/schema.ts'; import type { QueryWhere } from '../core/where.ts'; import { isComparisonWhere } from '../core/where.ts'; import type { SqlResult } from './types.ts'; /** PostgreSQL quoted identifier. * - "users" -> "users" * - "user"name" -> "user""name" (双引号内 `""` 转义一个 `"`) */ const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`; export const fieldToStr = (field: FieldPath): string => { if (field.length === 1) return quoteIdent(field[0]); const [root, ...rest] = field; if (rest.every((p) => typeof p === 'number')) { return `${quoteIdent(root)}${rest.map((i) => `[${i + 1}]`).join('')}`; } const segStr = rest.map((p) => typeof p === 'number' ? String(p) : `'${p.replace(/'/g, "''")}'`, ); const last = segStr.pop()!; const prefix = segStr.join('->'); return prefix ? `${quoteIdent(root)}->${prefix}->>${last}` : `${quoteIdent(root)}->>${last}`; }; const build = (where: QueryWhere): SqlResult | undefined => { if (where.op === 'not') { const inner = build(where.condition); if (!inner) return; return { sql: `NOT (${inner.sql})`, params: inner.params }; } if (where.op === 'and' || where.op === 'or') { const parts = where.conditions .map((c) => build(c)) .filter((c): c is SqlResult => c !== undefined); if (parts.length === 0) return; const joiner = ` ${where.op.toUpperCase()} `; const sql = parts.map((p) => p.sql).join(joiner); return { sql: parts.length > 1 ? `(${sql})` : sql, params: parts.flatMap((p) => p.params), }; } if (!isComparisonWhere(where)) return; const fieldStr = fieldToStr(where.field); if (where.op === 'in') { const placeholders = where.values.map(() => '?').join(', '); return { sql: `${fieldStr} IN (${placeholders})`, params: where.values }; } if (where.op === 'is null') { return { sql: `${fieldStr} IS NULL`, params: [] }; } return { sql: `${fieldStr} ${where.op} ?`, params: [where.value] }; }; export const toSqlWhere = ( where?: QueryWhere | null, ): SqlResult | undefined => { if (!where) return; return build(where); }; export const toSqlOrderBy = <TShape extends SchemaShape>( orderBy?: QueryOrderBy<TShape>[], ): SqlResult | undefined => { if (!orderBy) return; return { sql: orderBy .map((c) => `${fieldToStr(c.field)} ${c.direction.toUpperCase()}`) .join(', '), params: [], }; }; export const toSql = <TShape extends SchemaShape>( json: QuerySchema<TShape>, ): SqlResult | undefined => { if (!json.table) return; const where = json.where ? toSqlWhere(json.where) : undefined; const orderBy = json.orderBy?.length ? toSqlOrderBy(json.orderBy) : undefined; const parts = [ `SELECT * FROM ${quoteIdent(json.table)}`, where ? `WHERE ${where.sql}` : '', orderBy ? `ORDER BY ${orderBy.sql}` : '', json.limit !== undefined ? `LIMIT ${json.limit}` : '', json.offset !== undefined ? `OFFSET ${json.offset}` : '', ].filter(Boolean); return { sql: parts.join(' '), params: [...(where?.params ?? []), ...(orderBy?.params ?? [])], }; };