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.
155 lines (142 loc) • 4.13 kB
text/typescript
import {
and,
arrayContained,
arrayContains,
arrayOverlaps,
asc,
type ColumnsSelection,
desc,
eq,
gt,
gte,
ilike,
inArray,
isNull,
like,
lt,
lte,
not,
or,
type SQL,
sql,
} from 'drizzle-orm';
import type { BuildColumns } from 'drizzle-orm/column-builder';
import type { SelectedFields } from 'drizzle-orm/gel-core/query-builders/select.types';
import type { NeonHttpDatabase } from 'drizzle-orm/neon-http';
import {
type PgDatabase,
type PgQueryResultHKT,
PgSelectBase,
type PgTableWithColumns,
} from 'drizzle-orm/pg-core';
import type { PgColumnBuilderBase } from 'drizzle-orm/pg-core/columns/common';
import type { PgSelectBuilder } from 'drizzle-orm/pg-core/query-builders';
import type { PgTable, TableConfig } from 'drizzle-orm/pg-core/table';
import type { PgliteDatabase } from 'drizzle-orm/pglite';
import type { SelectMode } from 'drizzle-orm/query-builders/select.types';
import type { QuerySchema } from '../core/index.ts';
import type { QueryOrderBy } from '../core/order-by.ts';
import type { SchemaShape } from '../core/schema';
import type {
QueryWhere,
SetComparisonOp,
UnaryComparisonOp,
} from '../core/where.ts';
import { isComparisonWhere } from '../core/where.ts';
import { fieldToStr } from '../sql/pg.ts';
export const opMap = {
'=': eq,
'>': gt,
'>=': gte,
'<': lt,
'<=': lte,
like,
ilike,
} satisfies Record<UnaryComparisonOp, (column: any, value: any) => SQL>;
const setOps = {
'@>': arrayContains,
'<@': arrayContained,
'&&': arrayOverlaps,
};
const _toDrizzleWhere = (
table: any,
where?: QueryWhere | null,
): SQL | undefined => {
if (!where) return undefined;
if (where.op === 'not') {
const subCondition = _toDrizzleWhere(table, where.condition);
return subCondition ? not(subCondition) : undefined;
}
if (where.op === 'and' || where.op === 'or') {
const conditions = where.conditions
.map((c) => _toDrizzleWhere(table, c))
.filter((c): c is SQL => !!c);
if (conditions.length === 0) return;
return where.op === 'and' ? and(...conditions) : or(...conditions);
}
if (!isComparisonWhere(where)) return;
const [rootKey, ...segments] = where.field;
const column = table[rootKey];
if (!column) {
console.warn(`Field ${rootKey} does not exist on table`);
return;
}
const target =
segments.length === 0 ? column : sql.raw(fieldToStr(where.field));
if (where.op === 'in') return inArray(target, where.values);
if (where.op === 'is null') return isNull(target);
if (where.op in setOps) {
const opFn = setOps[where.op as SetComparisonOp];
if (!opFn) return;
return opFn(target, where.value);
}
const opFn = opMap[where.op as UnaryComparisonOp];
if (!opFn) return;
return opFn(target, where.value);
};
export const toDrizzleWhere = (
table: any,
where?: QueryWhere | null,
extraConditions?: SQL,
): SQL | undefined => {
const whereConditions = _toDrizzleWhere(table, where);
if (!extraConditions) return whereConditions;
if (!whereConditions) return extraConditions;
return and(extraConditions, whereConditions);
};
export const toDrizzleOrderBy = <TShape extends Record<string, any>>(
table: any,
orderBy?: QueryOrderBy<TShape>[],
): SQL[] => {
if (!orderBy) return [];
return orderBy.map((c) => {
const fieldKey = c.field[0];
const col = table[fieldKey];
const fn = c.direction === 'desc' ? desc : asc;
return fn(col);
});
};
export const toDrizzle = <TShape extends SchemaShape>(
db: PgDatabase<PgQueryResultHKT, Record<string, any>>,
table: any,
querySchema?: QuerySchema<TShape>,
) => {
if (!querySchema) return db.select().from(table) as Promise<TShape[]>;
const query = db
.select()
.from(table)
.where(toDrizzleWhere(table, querySchema.where))
.orderBy(...toDrizzleOrderBy(table, querySchema.orderBy));
if (querySchema.limit && querySchema.offset) {
return query.limit(querySchema.limit).offset(querySchema.offset) as Promise<
TShape[]
>;
}
if (querySchema.limit) {
return query.limit(querySchema.limit) as Promise<TShape[]>;
}
if (querySchema.offset) {
return query.offset(querySchema.offset) as Promise<TShape[]>;
}
return query as Promise<TShape[]>;
};