- Fix noPrecisionLoss on BIGINT UNSIGNED max value (2^64-1) with biome-ignore comments - Fix noThenProperty on custom thenable with biome-ignore comment - Auto-format remaining files (biome check --write) - Re-stage auto-fixed files from previous commit
1059 lines
30 KiB
TypeScript
1059 lines
30 KiB
TypeScript
import "server-only";
|
|
|
|
import {
|
|
and,
|
|
asc,
|
|
desc,
|
|
eq,
|
|
gt,
|
|
gte,
|
|
inArray,
|
|
isNotNull,
|
|
isNull,
|
|
like,
|
|
lt,
|
|
lte,
|
|
ne,
|
|
not,
|
|
notInArray,
|
|
or,
|
|
SQL,
|
|
sql,
|
|
} from "drizzle-orm";
|
|
import { createTableRelationsHelpers, One } from "drizzle-orm/relations";
|
|
import * as relationsDefs from "@/db/relations";
|
|
import * as schema from "@/db/schema";
|
|
import { db } from "@/lib/db";
|
|
|
|
type Client = typeof db;
|
|
|
|
const FULL = { ...schema, ...relationsDefs } as Record<string, any>;
|
|
|
|
const COLUMNS_SYM = Symbol.for("drizzle:Columns");
|
|
|
|
function capitalize(s: string): string {
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
}
|
|
|
|
function lowerFirst(s: string): string {
|
|
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
}
|
|
|
|
function getTable(model: string): any {
|
|
const table = FULL[capitalize(model)];
|
|
if (!table) {
|
|
throw new Error(`[prisma] unknown model "${model}"`);
|
|
}
|
|
return table;
|
|
}
|
|
|
|
function tableName(table: any): string {
|
|
return table[Symbol.for("drizzle:Name")] ?? "?";
|
|
}
|
|
|
|
function tableColumns(table: any): Record<string, any> {
|
|
return table[COLUMNS_SYM] ?? table;
|
|
}
|
|
|
|
function getRelations(table: any): Record<string, any> {
|
|
const relDef = FULL[`${lowerFirst(tableName(table))}Relations`];
|
|
if (!relDef) return {};
|
|
return relDef.config(createTableRelationsHelpers(table));
|
|
}
|
|
|
|
function coerceValue(col: any, value: any): any {
|
|
if (value === null || value === undefined) return value;
|
|
if (typeof value === "object" && typeof value.toNumber === "function") {
|
|
return Number(value.toNumber());
|
|
}
|
|
if (col?.dataType === "bigint") {
|
|
if (typeof value === "number") return BigInt(Math.trunc(value));
|
|
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
return BigInt(value);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function coerceArray(col: any, values: unknown[]): unknown[] {
|
|
return values.map((v) => coerceValue(col, v));
|
|
}
|
|
|
|
function buildFieldCondition(col: any, value: any): SQL {
|
|
if (value === null) return isNull(col);
|
|
if (typeof value !== "object") return eq(col, coerceValue(col, value));
|
|
|
|
const parts: SQL[] = [];
|
|
for (const [op, v] of Object.entries(value)) {
|
|
switch (op) {
|
|
case "equals":
|
|
parts.push(eq(col, coerceValue(col, v)));
|
|
break;
|
|
case "not":
|
|
if (v === null) parts.push(isNotNull(col));
|
|
else if (typeof v === "object")
|
|
parts.push(not(buildFieldCondition(col, v)));
|
|
else parts.push(ne(col, coerceValue(col, v)));
|
|
break;
|
|
case "in":
|
|
parts.push(inArray(col, coerceArray(col, v as unknown[])));
|
|
break;
|
|
case "notIn":
|
|
parts.push(notInArray(col, coerceArray(col, v as unknown[])));
|
|
break;
|
|
case "lt":
|
|
parts.push(lt(col, coerceValue(col, v)));
|
|
break;
|
|
case "lte":
|
|
parts.push(lte(col, coerceValue(col, v)));
|
|
break;
|
|
case "gt":
|
|
parts.push(gt(col, coerceValue(col, v)));
|
|
break;
|
|
case "gte":
|
|
parts.push(gte(col, coerceValue(col, v)));
|
|
break;
|
|
case "contains":
|
|
parts.push(like(col, `%${v}%`));
|
|
break;
|
|
case "startsWith":
|
|
parts.push(like(col, `${v}%`));
|
|
break;
|
|
case "endsWith":
|
|
parts.push(like(col, `%${v}`));
|
|
break;
|
|
case "mode":
|
|
case "search":
|
|
// MySQL collations are case-insensitive by default; full-text not used.
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return parts.length === 1
|
|
? parts[0]
|
|
: (and(...(parts as [SQL, ...SQL[]])) as SQL);
|
|
}
|
|
|
|
function buildCondition(table: any, where: any): SQL | undefined {
|
|
if (where === undefined || where === null) return undefined;
|
|
const cols = tableColumns(table);
|
|
const conds: SQL[] = [];
|
|
|
|
for (const [key, value] of Object.entries(where)) {
|
|
if (key === "AND") {
|
|
const list = Array.isArray(value) ? value : [value];
|
|
const inner = list
|
|
.map((w: any) => buildCondition(table, w))
|
|
.filter((w): w is SQL => Boolean(w));
|
|
if (inner.length) conds.push(and(...(inner as [SQL, ...SQL[]])) as SQL);
|
|
} else if (key === "OR") {
|
|
const list = Array.isArray(value) ? value : [value];
|
|
const inner = list
|
|
.map((w: any) => buildCondition(table, w))
|
|
.filter((w): w is SQL => Boolean(w));
|
|
if (inner.length) conds.push(or(...(inner as [SQL, ...SQL[]])) as SQL);
|
|
} else if (key === "NOT") {
|
|
const inner = Array.isArray(value)
|
|
? and(
|
|
...value
|
|
.map((w: any) => buildCondition(table, w))
|
|
.filter((w): w is SQL => Boolean(w)),
|
|
)
|
|
: buildCondition(table, value);
|
|
if (inner) conds.push(not(inner));
|
|
} else {
|
|
const col = cols[key];
|
|
if (!col) {
|
|
// Compound unique key, e.g. `eventId_userId`.
|
|
const parts = key.split("_");
|
|
if (parts.length > 1 && parts.every((p) => cols[p])) {
|
|
conds.push(
|
|
and(
|
|
...(parts.map((p) =>
|
|
eq(
|
|
(cols as Record<string, any>)[p],
|
|
coerceValue(
|
|
(cols as Record<string, any>)[p],
|
|
(value as any)?.[p],
|
|
),
|
|
),
|
|
) as [SQL, ...SQL[]]),
|
|
) as SQL,
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
`[prisma] unknown field "${key}" on model "${tableName(table)}"`,
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
conds.push(buildFieldCondition(col, value));
|
|
}
|
|
}
|
|
|
|
if (conds.length === 0) return undefined;
|
|
return conds.length === 1 ? conds[0] : and(...conds);
|
|
}
|
|
|
|
function buildOrderBy(table: any, orderBy: any): SQL[] {
|
|
if (!orderBy) return [];
|
|
const entries = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
const cols = tableColumns(table);
|
|
return entries.flatMap((entry: any) => {
|
|
if (typeof entry === "string") {
|
|
const col = cols[entry];
|
|
return col ? [asc(col)] : [];
|
|
}
|
|
return Object.entries(entry).flatMap(([key, dir]) => {
|
|
if (key === "_count") return [];
|
|
const col = cols[key];
|
|
if (!col) return [];
|
|
return dir === "desc" ? [desc(col)] : [asc(col)];
|
|
});
|
|
});
|
|
}
|
|
|
|
function projection(table: any, select: any): Record<string, any> | undefined {
|
|
if (!select || typeof select !== "object") return undefined;
|
|
const cols = tableColumns(table);
|
|
const out: Record<string, any> = {};
|
|
for (const [key, enabled] of Object.entries(select)) {
|
|
if (key === "_count") continue;
|
|
if (enabled && cols[key]) out[key] = cols[key];
|
|
}
|
|
return Object.keys(out).length ? out : undefined;
|
|
}
|
|
|
|
function pickRow(row: any, select: any): any {
|
|
if (!select || typeof select !== "object") return row;
|
|
const out: any = {};
|
|
for (const [key, enabled] of Object.entries(select)) {
|
|
if (enabled && key in row) out[key] = row[key];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
interface Join {
|
|
fkCols: any[]; // columns on the *target* used to filter/group
|
|
refCols: any[]; // columns on the *source* used to match
|
|
}
|
|
|
|
function resolveJoin(table: any, rel: any): Join {
|
|
if (rel instanceof One) {
|
|
const cfg = rel.config ?? { fields: [], references: [] };
|
|
return { fkCols: cfg.references, refCols: cfg.fields };
|
|
}
|
|
if (rel.config?.fields && rel.config.references) {
|
|
return { fkCols: rel.config.fields, refCols: rel.config.references };
|
|
}
|
|
// Implicit many: find the reverse one-relation on the target.
|
|
const targetRels = getRelations(rel.referencedTable);
|
|
for (const r of Object.values(targetRels)) {
|
|
if (r instanceof One && r.referencedTable === table) {
|
|
const cfg = r.config ?? { fields: [], references: [] };
|
|
return { fkCols: cfg.fields, refCols: cfg.references };
|
|
}
|
|
}
|
|
throw new Error(
|
|
`[prisma] cannot resolve join for relation "${rel.fieldName}" on "${tableName(table)}"`,
|
|
);
|
|
}
|
|
|
|
function colKey(col: any): string {
|
|
return col.key ?? col.name;
|
|
}
|
|
|
|
async function queryRelationRows(
|
|
client: Client,
|
|
table: any,
|
|
rel: any,
|
|
opts: any,
|
|
keyValues: unknown[],
|
|
): Promise<Map<string, any>> {
|
|
const join = resolveJoin(table, rel);
|
|
const byCol = join.fkCols[0];
|
|
const condition = inArray(byCol, coerceArray(byCol, keyValues));
|
|
const sel = projection(rel.referencedTable, opts.select);
|
|
const orderBy = buildOrderBy(rel.referencedTable, opts.orderBy);
|
|
|
|
let stmt: any = sel
|
|
? client.select({ ...sel, __fk: byCol }).from(rel.referencedTable)
|
|
: client.select().from(rel.referencedTable);
|
|
stmt = stmt.where(condition);
|
|
if (orderBy.length) stmt = stmt.orderBy(...orderBy);
|
|
|
|
const rows = (await stmt) as any[];
|
|
const map = new Map<string, any>();
|
|
for (const row of rows) {
|
|
const key = row.__fk ?? row[colKey(byCol)];
|
|
map.set(String(key), sel ? pickRow(row, opts.select) : row);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function attachCounts(
|
|
rows: any[],
|
|
table: any,
|
|
rel: any,
|
|
counts: Map<string, number>,
|
|
): void {
|
|
const join = resolveJoin(table, rel);
|
|
const refCol = join.refCols[0];
|
|
for (const row of rows) {
|
|
if (!row._count) row._count = {};
|
|
const refValue = row[colKey(refCol)];
|
|
row._count[rel.fieldName] =
|
|
refValue == null ? 0 : (counts.get(String(refValue)) ?? 0);
|
|
}
|
|
}
|
|
|
|
async function runCounts(
|
|
client: Client,
|
|
table: any,
|
|
rows: any[],
|
|
countSpec: Record<string, boolean>,
|
|
): Promise<void> {
|
|
const rels = getRelations(table);
|
|
for (const [relName, enabled] of Object.entries(countSpec)) {
|
|
if (!enabled) continue;
|
|
const rel = rels[relName];
|
|
if (!rel) continue;
|
|
const join = resolveJoin(table, rel);
|
|
const refCol = join.refCols[0];
|
|
const values = rows
|
|
.map((r) => r[colKey(refCol)])
|
|
.filter((v) => v !== null && v !== undefined);
|
|
const counts = new Map<string, number>();
|
|
if (values.length > 0) {
|
|
const byCol = join.fkCols[0];
|
|
const grouped = (await client
|
|
.select({
|
|
__fk: byCol,
|
|
__c: sql<number>`COUNT(*)`,
|
|
})
|
|
.from(rel.referencedTable)
|
|
.where(inArray(byCol, coerceArray(byCol, values)))
|
|
.groupBy(byCol)) as any[];
|
|
for (const g of grouped) counts.set(String(g.__fk), Number(g.__c));
|
|
}
|
|
attachCounts(rows, table, rel, counts);
|
|
}
|
|
}
|
|
|
|
async function attachIncludes(
|
|
client: Client,
|
|
table: any,
|
|
rows: any[],
|
|
includes: any,
|
|
): Promise<void> {
|
|
if (!includes || typeof includes !== "object" || rows.length === 0) return;
|
|
const rels = getRelations(table);
|
|
|
|
const oneJobs: Array<{ rel: any; opts: any; values: unknown[] }> = [];
|
|
const manyJobs: Array<{ rel: any; opts: any; values: unknown[] }> = [];
|
|
const countSpec: Record<string, boolean> = {};
|
|
|
|
for (const [name, optsRaw] of Object.entries(includes)) {
|
|
if (name === "_count") {
|
|
if (
|
|
optsRaw !== null &&
|
|
typeof optsRaw === "object" &&
|
|
(optsRaw as any).select
|
|
) {
|
|
Object.assign(countSpec, (optsRaw as any).select);
|
|
}
|
|
continue;
|
|
}
|
|
const rel = rels[name];
|
|
if (!rel) continue;
|
|
const opts = optsRaw === true ? {} : (optsRaw ?? {});
|
|
const join = resolveJoin(table, rel);
|
|
const refCol = join.refCols[0];
|
|
const values = rows
|
|
.map((r) => r[colKey(refCol)])
|
|
.filter((v) => v !== null && v !== undefined);
|
|
if (rel instanceof One) {
|
|
oneJobs.push({ rel, opts, values });
|
|
} else {
|
|
manyJobs.push({ rel, opts, values });
|
|
}
|
|
}
|
|
|
|
for (const job of oneJobs) {
|
|
const { rel, opts, values } = job;
|
|
if (values.length === 0) {
|
|
for (const row of rows) row[rel.fieldName] = null;
|
|
continue;
|
|
}
|
|
const map = await queryRelationRows(client, table, rel, opts, values);
|
|
const join = resolveJoin(table, rel);
|
|
const refCol = join.refCols[0];
|
|
for (const row of rows) {
|
|
const refValue = row[colKey(refCol)];
|
|
row[rel.fieldName] =
|
|
refValue == null ? null : (map.get(String(refValue)) ?? null);
|
|
}
|
|
}
|
|
|
|
for (const job of manyJobs) {
|
|
const { rel, opts, values } = job;
|
|
if (values.length === 0) {
|
|
for (const row of rows) row[rel.fieldName] = [];
|
|
continue;
|
|
}
|
|
const map = await queryRelationRows(client, table, rel, opts, values);
|
|
const join = resolveJoin(table, rel);
|
|
const refCol = join.refCols[0];
|
|
for (const row of rows) {
|
|
const refValue = row[colKey(refCol)];
|
|
row[rel.fieldName] =
|
|
refValue == null ? [] : (map.get(String(refValue)) ?? []);
|
|
}
|
|
}
|
|
|
|
if (Object.keys(countSpec).length) {
|
|
await runCounts(client, table, rows, countSpec);
|
|
}
|
|
}
|
|
|
|
// ---------- Query execution ----------
|
|
|
|
interface ReadArgs {
|
|
where?: any;
|
|
select?: any;
|
|
include?: any;
|
|
orderBy?: any;
|
|
take?: number;
|
|
skip?: number;
|
|
}
|
|
|
|
async function runFindMany(
|
|
client: Client,
|
|
model: string,
|
|
args: ReadArgs,
|
|
): Promise<any[]> {
|
|
const table = getTable(model);
|
|
const condition = buildCondition(table, args.where);
|
|
const orderBy = buildOrderBy(table, args.orderBy);
|
|
const sel = projection(table, args.select);
|
|
|
|
let stmt: any = sel
|
|
? client.select({ ...sel }).from(table)
|
|
: client.select().from(table);
|
|
if (condition) stmt = stmt.where(condition);
|
|
if (orderBy.length) stmt = stmt.orderBy(...orderBy);
|
|
if (args.take !== undefined) stmt = stmt.limit(args.take);
|
|
else if (args.skip)
|
|
// biome-ignore lint/correctness/noPrecisionLoss: SQL literal for BIGINT UNSIGNED max value (2^64-1 — exceeds JS number precision)
|
|
stmt = stmt.limit(18446744073709551615);
|
|
if (args.skip) stmt = stmt.offset(args.skip);
|
|
|
|
const rows = (await stmt) as any[];
|
|
|
|
await attachIncludes(client, table, rows, args.include);
|
|
|
|
if (args.select && typeof args.select === "object" && args.select._count) {
|
|
const countSpec: Record<string, boolean> = {};
|
|
for (const [relName, enabled] of Object.entries(
|
|
args.select._count.select ?? {},
|
|
)) {
|
|
if (enabled) countSpec[relName] = true;
|
|
}
|
|
if (Object.keys(countSpec).length) {
|
|
await runCounts(client, table, rows, countSpec);
|
|
}
|
|
const projected = rows.map((row) => pickRow(row, args.select));
|
|
return projected;
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
async function runFindOne(
|
|
client: Client,
|
|
model: string,
|
|
args: ReadArgs,
|
|
orThrow: boolean,
|
|
): Promise<any> {
|
|
const rows = await runFindMany(client, model, { ...args, take: 1 });
|
|
if (rows.length === 0) {
|
|
if (orThrow) throw new Error(`[prisma] ${model} not found`);
|
|
return null;
|
|
}
|
|
return rows[0];
|
|
}
|
|
|
|
// ---------- Mutations ----------
|
|
|
|
function pkColumns(table: any): any[] {
|
|
const cols = tableColumns(table);
|
|
return Object.values(cols).filter((c) => c.primary);
|
|
}
|
|
|
|
function coerceData(table: any, data: any): Record<string, any> {
|
|
const cols = tableColumns(table);
|
|
const coerced: Record<string, any> = {};
|
|
for (const [key, value] of Object.entries(data ?? {})) {
|
|
coerced[key] = coerceValue(cols[key], value);
|
|
}
|
|
return coerced;
|
|
}
|
|
|
|
async function runCreate(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<any> {
|
|
const table = getTable(model);
|
|
const [result] = (await client
|
|
.insert(table)
|
|
.values(coerceData(table, args.data))) as any;
|
|
|
|
const pks = pkColumns(table);
|
|
const where: any = {};
|
|
if (pks.length === 1 && pks[0].autoIncrement) {
|
|
where[colKey(pks[0])] = result.insertId;
|
|
} else {
|
|
for (const pk of pks) {
|
|
where[colKey(pk)] = args.data?.[colKey(pk)];
|
|
}
|
|
}
|
|
return runFindOne(
|
|
client,
|
|
model,
|
|
{ where, select: args.select, include: args.include },
|
|
false,
|
|
);
|
|
}
|
|
|
|
async function runUpdate(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<any> {
|
|
const table = getTable(model);
|
|
const condition = buildCondition(table, args.where);
|
|
if (condition) {
|
|
await client
|
|
.update(table)
|
|
.set(coerceData(table, args.data))
|
|
.where(condition);
|
|
}
|
|
return runFindOne(
|
|
client,
|
|
model,
|
|
{ where: args.where, select: args.select, include: args.include },
|
|
false,
|
|
);
|
|
}
|
|
|
|
async function runDelete(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<any> {
|
|
const table = getTable(model);
|
|
const row = await runFindOne(client, model, { where: args.where }, false);
|
|
if (!row) return null;
|
|
const condition = buildCondition(table, args.where);
|
|
if (condition) {
|
|
await client.delete(table).where(condition);
|
|
}
|
|
return row;
|
|
}
|
|
|
|
async function runDeleteMany(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<{ count: number }> {
|
|
const table = getTable(model);
|
|
const condition = buildCondition(table, args.where);
|
|
if (!condition) {
|
|
const [result] = (await client.delete(table)) as any;
|
|
return { count: Number(result.affectedRows) };
|
|
}
|
|
const [result] = (await client.delete(table).where(condition)) as any;
|
|
return { count: Number(result.affectedRows) };
|
|
}
|
|
|
|
async function runUpdateMany(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<{ count: number }> {
|
|
const table = getTable(model);
|
|
const condition = buildCondition(table, args.where);
|
|
const [result] = condition
|
|
? ((await client
|
|
.update(table)
|
|
.set(coerceData(table, args.data))
|
|
.where(condition)) as any)
|
|
: ((await client.update(table).set(coerceData(table, args.data))) as any);
|
|
return { count: Number(result.affectedRows) };
|
|
}
|
|
|
|
async function runCreateMany(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<{ count: number }> {
|
|
const table = getTable(model);
|
|
const rowsData = Array.isArray(args.data) ? args.data : [args.data];
|
|
const values = rowsData.map((data: any) => coerceData(table, data));
|
|
const [result] = (await client.insert(table).values(values)) as any;
|
|
return { count: Number(result.affectedRows) };
|
|
}
|
|
|
|
async function runUpsert(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<any> {
|
|
const existing = await runFindOne(
|
|
client,
|
|
model,
|
|
{ where: args.where },
|
|
false,
|
|
);
|
|
if (existing) {
|
|
return runUpdate(client, model, {
|
|
where: args.where,
|
|
data: args.update,
|
|
select: args.select,
|
|
include: args.include,
|
|
});
|
|
}
|
|
return runCreate(client, model, {
|
|
data: args.create,
|
|
select: args.select,
|
|
include: args.include,
|
|
});
|
|
}
|
|
|
|
async function runCount(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<number> {
|
|
const table = getTable(model);
|
|
const condition = buildCondition(table, args.where);
|
|
let stmt: any = client.select({ __c: sql<number>`COUNT(*)` }).from(table);
|
|
if (condition) stmt = stmt.where(condition);
|
|
const [row] = (await stmt) as any;
|
|
return Number(row?.__c ?? 0);
|
|
}
|
|
|
|
const AGG_FNS = {
|
|
_sum: "SUM",
|
|
_min: "MIN",
|
|
_max: "MAX",
|
|
_avg: "AVG",
|
|
} as const;
|
|
|
|
async function runAggregate(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<any> {
|
|
const table = getTable(model);
|
|
const cols = tableColumns(table);
|
|
const condition = buildCondition(table, args.where);
|
|
|
|
const sel: Record<string, any> = {};
|
|
const meta: Record<
|
|
string,
|
|
{ agg: keyof typeof AGG_FNS; field: string; col: any }
|
|
> = {};
|
|
for (const agg of ["_sum", "_min", "_max", "_avg"] as const) {
|
|
if (!args[agg]) continue;
|
|
for (const [field, enabled] of Object.entries(args[agg])) {
|
|
if (!enabled) continue;
|
|
const col = cols[field];
|
|
if (!col) continue;
|
|
const alias = `__${agg}${field}`;
|
|
sel[alias] = sql`${sql.raw(AGG_FNS[agg])}(${col})`;
|
|
meta[alias] = { agg, field, col };
|
|
}
|
|
}
|
|
|
|
const out: Record<string, any> = {};
|
|
if (Object.keys(sel).length) {
|
|
let stmt: any = client.select(sel).from(table);
|
|
if (condition) stmt = stmt.where(condition);
|
|
if (args.take) stmt = stmt.limit(args.take);
|
|
if (args.skip) stmt = stmt.limit(Number.MAX_SAFE_INTEGER).offset(args.skip);
|
|
const [row] = (await stmt) as any;
|
|
for (const [alias, { agg, field, col }] of Object.entries(meta)) {
|
|
if (!out[agg]) out[agg] = {};
|
|
const raw = row?.[alias];
|
|
if (raw === null || raw === undefined) {
|
|
out[agg][field] = null;
|
|
} else if (col.dataType === "bigint") {
|
|
out[agg][field] = BigInt(raw);
|
|
} else {
|
|
out[agg][field] = Number(raw);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (args._count) {
|
|
let stmt: any = client.select({ __c: sql<number>`COUNT(*)` }).from(table);
|
|
if (condition) stmt = stmt.where(condition);
|
|
const [row] = (await stmt) as any;
|
|
out._count = Number(row?.__c ?? 0);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
async function runGroupBy(
|
|
client: Client,
|
|
model: string,
|
|
args: any,
|
|
): Promise<any[]> {
|
|
const table = getTable(model);
|
|
const cols = tableColumns(table);
|
|
const condition = buildCondition(table, args.where);
|
|
const byCols: any[] = (args.by ?? []).map((f: string) => {
|
|
const col = cols[f];
|
|
if (!col)
|
|
throw new Error(`[prisma] groupBy unknown field "${f}" on "${model}"`);
|
|
return col;
|
|
});
|
|
|
|
const sel: Record<string, any> = {};
|
|
for (const col of byCols) sel[colKey(col)] = col;
|
|
|
|
const aggMeta: Record<
|
|
string,
|
|
Record<string, { alias: string; col?: any; all: boolean }>
|
|
> = {};
|
|
for (const agg of ["_count", "_sum", "_min", "_max", "_avg"] as const) {
|
|
const spec = args[agg];
|
|
if (!spec) continue;
|
|
aggMeta[agg] = {};
|
|
for (const [field, enabled] of Object.entries(spec)) {
|
|
if (!enabled) continue;
|
|
if (field === "_all") {
|
|
const alias = `__${agg}_all`;
|
|
sel[alias] = sql<number>`COUNT(*)`;
|
|
aggMeta[agg]._all = { alias, all: true };
|
|
continue;
|
|
}
|
|
const col = cols[field];
|
|
if (!col) continue;
|
|
const fn =
|
|
agg === "_count"
|
|
? "COUNT"
|
|
: agg === "_sum"
|
|
? "SUM"
|
|
: agg === "_min"
|
|
? "MIN"
|
|
: agg === "_max"
|
|
? "MAX"
|
|
: "AVG";
|
|
const alias = `__${agg}${field}`;
|
|
sel[alias] = sql`${sql.raw(fn)}(${col})`;
|
|
aggMeta[agg][field] = { alias, col, all: false };
|
|
}
|
|
}
|
|
|
|
let stmt: any = client.select(sel).from(table);
|
|
if (condition) stmt = stmt.where(condition);
|
|
if (byCols.length) stmt = stmt.groupBy(...byCols);
|
|
|
|
if (args.having) {
|
|
const havings: SQL[] = [];
|
|
for (const [field, ops] of Object.entries(args.having as any)) {
|
|
for (const [agg, op] of Object.entries(ops as any)) {
|
|
if (agg !== "_count") continue;
|
|
const col = cols[field];
|
|
const countExpr = col ? sql`COUNT(${col})` : sql`COUNT(*)`;
|
|
for (const [cmp, val] of Object.entries(op as any)) {
|
|
if (cmp === "gte") havings.push(sql`${countExpr} >= ${val}`);
|
|
else if (cmp === "gt") havings.push(sql`${countExpr} > ${val}`);
|
|
else if (cmp === "lte") havings.push(sql`${countExpr} <= ${val}`);
|
|
else if (cmp === "lt") havings.push(sql`${countExpr} < ${val}`);
|
|
else if (cmp === "equals") havings.push(sql`${countExpr} = ${val}`);
|
|
}
|
|
}
|
|
}
|
|
if (havings.length) stmt = stmt.having(...(havings as [SQL]));
|
|
}
|
|
|
|
if (args.orderBy) {
|
|
const entries = Array.isArray(args.orderBy) ? args.orderBy : [args.orderBy];
|
|
const orderSql: SQL[] = [];
|
|
for (const entry of entries) {
|
|
for (const [field, dir] of Object.entries(entry as any)) {
|
|
if (field === "_count") {
|
|
for (const [f, d] of Object.entries(dir as any)) {
|
|
const col = f === "_all" ? undefined : cols[f];
|
|
const target = col ? sql`COUNT(${col})` : sql`COUNT(*)`;
|
|
orderSql.push(
|
|
d === "desc" ? desc(target as any) : asc(target as any),
|
|
);
|
|
}
|
|
} else {
|
|
const col = cols[field];
|
|
if (col) orderSql.push(dir === "desc" ? desc(col) : asc(col));
|
|
}
|
|
}
|
|
}
|
|
if (orderSql.length) stmt = stmt.orderBy(...orderSql);
|
|
}
|
|
|
|
if (args.take !== undefined) stmt = stmt.limit(args.take);
|
|
else if (args.skip)
|
|
// biome-ignore lint/correctness/noPrecisionLoss: SQL literal for BIGINT UNSIGNED max value (2^64-1 — exceeds JS number precision)
|
|
stmt = stmt.limit(18446744073709551615);
|
|
if (args.skip) stmt = stmt.offset(args.skip);
|
|
|
|
const rows = (await stmt) as any[];
|
|
return rows.map((row) => {
|
|
const out: any = {};
|
|
for (const col of byCols) out[colKey(col)] = row[colKey(col)];
|
|
for (const [agg, fields] of Object.entries(aggMeta)) {
|
|
out[agg] = {};
|
|
for (const [field, { alias, col, all }] of Object.entries(fields)) {
|
|
const raw = row[alias];
|
|
if (all) {
|
|
out[agg]._all = Number(raw);
|
|
} else if (agg === "_count") {
|
|
out[agg][field] = Number(raw);
|
|
} else if (col?.dataType === "bigint" && raw != null) {
|
|
out[agg][field] = BigInt(raw);
|
|
} else {
|
|
out[agg][field] = raw == null ? null : Number(raw);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
});
|
|
}
|
|
|
|
// ---------- Raw queries ----------
|
|
|
|
async function runQuery(client: Client, chunk: SQL): Promise<any[]> {
|
|
const [rows] = (await client.execute(chunk)) as any;
|
|
return rows;
|
|
}
|
|
|
|
async function runExecute(client: Client, chunk: SQL): Promise<number> {
|
|
const [result] = (await client.execute(chunk)) as any;
|
|
return Number(result?.affectedRows ?? 0);
|
|
}
|
|
|
|
function toSqlChunk(strings: TemplateStringsArray, values: unknown[]): SQL {
|
|
return sql(strings, ...values);
|
|
}
|
|
|
|
function unsafeSql(query: string, values: unknown[]): SQL {
|
|
if (values.length === 0) return sql.raw(query);
|
|
const parts = query.split("?");
|
|
if (parts.length !== values.length + 1) {
|
|
throw new Error(
|
|
`[prisma] $queryRawUnsafe: expected ${parts.length - 1} params, got ${values.length}`,
|
|
);
|
|
}
|
|
const chunks: SQL[] = [sql.raw(parts[0])];
|
|
for (let i = 0; i < values.length; i++) {
|
|
const value = values[i];
|
|
chunks.push(value instanceof SQL ? value : sql`${value}`);
|
|
chunks.push(sql.raw(parts[i + 1]));
|
|
}
|
|
return sql.join(chunks);
|
|
}
|
|
|
|
// ---------- PrismaPromise (lazy, batchable) ----------
|
|
|
|
class PrismaPromise<T> implements PromiseLike<T> {
|
|
constructor(
|
|
private readonly runner: (client: Client) => Promise<T>,
|
|
private readonly client: Client,
|
|
) {}
|
|
|
|
// biome-ignore lint/suspicious/noThenProperty: Custom thenable for $transaction chaining — routes through Drizzle at runtime
|
|
then<TResult1 = T, TResult2 = never>(
|
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
|
|
): PromiseLike<TResult1 | TResult2> {
|
|
return this.runner(this.client).then(onFulfilled, onRejected);
|
|
}
|
|
|
|
catch<TResult = never>(
|
|
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,
|
|
): PromiseLike<T | TResult> {
|
|
return this.runner(this.client).catch(onRejected);
|
|
}
|
|
|
|
finally(onFinally?: (() => void) | null): PromiseLike<T> {
|
|
return this.runner(this.client).finally(onFinally);
|
|
}
|
|
|
|
run(client: Client): Promise<T> {
|
|
return this.runner(client);
|
|
}
|
|
}
|
|
|
|
function prismaPromise<T>(
|
|
runner: (client: Client) => Promise<T>,
|
|
client: Client,
|
|
): PrismaPromise<T> {
|
|
return new PrismaPromise(runner, client);
|
|
}
|
|
|
|
// ---------- Model delegate ----------
|
|
|
|
function makeDelegate(model: string, client: Client) {
|
|
return {
|
|
findMany: (args: ReadArgs = {}) =>
|
|
prismaPromise((c) => runFindMany(c, model, args), client),
|
|
findUnique: (args: ReadArgs) =>
|
|
prismaPromise((c) => runFindOne(c, model, args, false), client),
|
|
findUniqueOrThrow: (args: ReadArgs) =>
|
|
prismaPromise((c) => runFindOne(c, model, args, true), client),
|
|
findFirst: (args: ReadArgs = {}) =>
|
|
prismaPromise((c) => runFindOne(c, model, args, false), client),
|
|
findFirstOrThrow: (args: ReadArgs = {}) =>
|
|
prismaPromise((c) => runFindOne(c, model, args, true), client),
|
|
create: (args: any) =>
|
|
prismaPromise((c) => runCreate(c, model, args), client),
|
|
createMany: (args: any) =>
|
|
prismaPromise((c) => runCreateMany(c, model, args), client),
|
|
update: (args: any) =>
|
|
prismaPromise((c) => runUpdate(c, model, args), client),
|
|
updateMany: (args: any) =>
|
|
prismaPromise((c) => runUpdateMany(c, model, args), client),
|
|
delete: (args: any) =>
|
|
prismaPromise((c) => runDelete(c, model, args), client),
|
|
deleteMany: (args: any) =>
|
|
prismaPromise((c) => runDeleteMany(c, model, args), client),
|
|
upsert: (args: any) =>
|
|
prismaPromise((c) => runUpsert(c, model, args), client),
|
|
count: (args: any = {}) =>
|
|
prismaPromise((c) => runCount(c, model, args), client),
|
|
aggregate: (args: any = {}) =>
|
|
prismaPromise((c) => runAggregate(c, model, args), client),
|
|
groupBy: (args: any) =>
|
|
prismaPromise((c) => runGroupBy(c, model, args), client),
|
|
};
|
|
}
|
|
|
|
// ---------- Facade ----------
|
|
|
|
interface ModelDelegate {
|
|
findMany(args?: any): PrismaPromise<any[]>;
|
|
findUnique(args: any): PrismaPromise<any>;
|
|
findUniqueOrThrow(args: any): PrismaPromise<any>;
|
|
findFirst(args?: any): PrismaPromise<any>;
|
|
findFirstOrThrow(args?: any): PrismaPromise<any>;
|
|
create(args: any): PrismaPromise<any>;
|
|
createMany(args: any): PrismaPromise<{ count: number }>;
|
|
update(args: any): PrismaPromise<any>;
|
|
updateMany(args: any): PrismaPromise<{ count: number }>;
|
|
delete(args: any): PrismaPromise<any>;
|
|
deleteMany(args: any): PrismaPromise<{ count: number }>;
|
|
upsert(args: any): PrismaPromise<any>;
|
|
count(args?: any): PrismaPromise<number>;
|
|
aggregate(args?: any): PrismaPromise<any>;
|
|
groupBy(args: any): PrismaPromise<any[]>;
|
|
}
|
|
|
|
interface PrismaFacadeSpecific {
|
|
$queryRaw: <T = any>(...args: any[]) => PrismaPromise<T>;
|
|
$executeRaw: (...args: any[]) => PrismaPromise<number>;
|
|
$queryRawUnsafe: <T = any>(...args: any[]) => PrismaPromise<T>;
|
|
$executeRawUnsafe: (...args: any[]) => PrismaPromise<number>;
|
|
$transaction: (arg: any, options?: any) => Promise<any>;
|
|
$connect: () => Promise<void>;
|
|
$disconnect: () => Promise<void>;
|
|
$on: (...args: any[]) => void;
|
|
$use: (...args: any[]) => void;
|
|
}
|
|
|
|
type PrismaFacade = PrismaFacadeSpecific & Record<string, ModelDelegate>;
|
|
|
|
function isSqlChunk(value: any): value is SQL {
|
|
return Boolean(value && typeof value === "object" && "queryChunks" in value);
|
|
}
|
|
|
|
export function makePrisma(client: Client): PrismaFacade {
|
|
const methods: Record<string, any> = {
|
|
$queryRaw: (strings: TemplateStringsArray | SQL, ...values: unknown[]) => {
|
|
const chunk = isSqlChunk(strings)
|
|
? strings
|
|
: toSqlChunk(strings as TemplateStringsArray, values);
|
|
return prismaPromise((c) => runQuery(c, chunk), client);
|
|
},
|
|
$executeRaw: (
|
|
strings: TemplateStringsArray | SQL,
|
|
...values: unknown[]
|
|
) => {
|
|
const chunk = isSqlChunk(strings)
|
|
? strings
|
|
: toSqlChunk(strings as TemplateStringsArray, values);
|
|
return prismaPromise((c) => runExecute(c, chunk), client);
|
|
},
|
|
$queryRawUnsafe: (query: string, ...values: unknown[]) =>
|
|
prismaPromise((c) => runQuery(c, unsafeSql(query, values)), client),
|
|
$executeRawUnsafe: (query: string, ...values: unknown[]) =>
|
|
prismaPromise((c) => runExecute(c, unsafeSql(query, values)), client),
|
|
$transaction: (arg: any, options?: any) => {
|
|
if (typeof arg === "function") {
|
|
return client.transaction(async (tx) => {
|
|
const txClient = tx as unknown as Client;
|
|
return arg(makePrisma(txClient));
|
|
}, options);
|
|
}
|
|
if (Array.isArray(arg)) {
|
|
return client.transaction(async (tx) => {
|
|
const txClient = tx as unknown as Client;
|
|
const results: any[] = [];
|
|
for (const op of arg) {
|
|
results.push(
|
|
op instanceof PrismaPromise
|
|
? await op.run(txClient)
|
|
: await (op as any),
|
|
);
|
|
}
|
|
return results;
|
|
}, options);
|
|
}
|
|
return Promise.resolve(arg);
|
|
},
|
|
$connect: async () => {},
|
|
$disconnect: async () => {},
|
|
$on: () => {},
|
|
$use: () => {},
|
|
};
|
|
|
|
return new Proxy(methods, {
|
|
get(target, prop) {
|
|
if (typeof prop !== "string") return undefined;
|
|
if (prop in target) return (target as any)[prop];
|
|
return makeDelegate(prop, client);
|
|
},
|
|
}) as PrismaFacade;
|
|
}
|
|
|
|
export const prisma = makePrisma(db);
|
|
|
|
export const sqlHelper = sql;
|
|
export const rawHelper = (query: string): SQL => sql.raw(query);
|
|
export const joinHelper = (values: unknown[]): SQL =>
|
|
sql.join(
|
|
values.map((v) => (v instanceof SQL ? v : sql`${v}`)),
|
|
sql`, `,
|
|
);
|
|
export const emptyHelper = sql.raw("");
|
|
|
|
export const Prisma: Record<string, any> = {
|
|
sql: sqlHelper,
|
|
raw: rawHelper,
|
|
join: joinHelper,
|
|
empty: emptyHelper,
|
|
};
|