Files
EpicNext-Cms/src/lib/prisma-facade.ts
T

1095 lines
31 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[],
mode: "one" | "many",
): Promise<Map<string, any>> {
const join = resolveJoin(table, rel);
const byCol = join.fkCols[0];
const parts = [
inArray(byCol, coerceArray(byCol, keyValues)),
buildCondition(rel.referencedTable, opts.where),
].filter(Boolean);
const condition = parts.length === 1 ? parts[0] : and(...parts);
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);
if (condition) 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 = String(row.__fk ?? row[colKey(byCol)]);
const cleaned = sel ? pickRow(row, opts.select) : row;
if (mode === "many") {
const list = map.get(key);
if (Array.isArray(list)) list.push(cleaned);
else map.set(key, [cleaned]);
} else if (!map.has(key)) {
map.set(key, cleaned);
}
}
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,
"one",
);
const join = resolveJoin(table, rel);
const refCol = join.refCols[0];
const related: any[] = [];
for (const row of rows) {
const refValue = row[colKey(refCol)];
const hit = refValue == null ? null : (map.get(String(refValue)) ?? null);
row[rel.fieldName] = hit;
if (hit) related.push(hit);
}
if (opts.include && related.length > 0) {
await attachIncludes(client, rel.referencedTable, related, opts.include);
}
}
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,
"many",
);
const join = resolveJoin(table, rel);
const refCol = join.refCols[0];
const related: any[] = [];
for (const row of rows) {
const refValue = row[colKey(refCol)];
const list = refValue == null ? [] : (map.get(String(refValue)) ?? []);
row[rel.fieldName] = list;
for (const item of list) related.push(item);
}
if (opts.include && related.length > 0) {
await attachIncludes(client, rel.referencedTable, related, opts.include);
}
}
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,
};