Skip to content

Instantly share code, notes, and snippets.

@public
Created July 7, 2026 11:57
Show Gist options
  • Select an option

  • Save public/4a596b6ad625448366157d55ba2742d1 to your computer and use it in GitHub Desktop.

Select an option

Save public/4a596b6ad625448366157d55ba2742d1 to your computer and use it in GitHub Desktop.
Prisma to Knex bridge
// ============================================================================
// USAGE
// ----------------------------------------------------------------------------
// Write ONE backend-agnostic function. `asKnex` accepts either a Knex trx or a
// Prisma interactive-tx client and hands back a callable Knex builder bound to
// that transaction. All statements run on the SAME connection => atomic.
//
// const { asKnex } = require('./knexForTx');
//
// async function recordSignup(handle, userId) {
// const { k, owned } = asKnex(handle); // owned=true only for the Prisma path
// try {
// await k('Audit').insert({ userId, action: 'created' });
// return await k('Audit').where({ userId }).orderBy('id'); // await BEFORE finally
// } finally {
// if (owned) await k.destroy(); // destroy only the instance we created
// }
// }
//
// // Caller A — Knex owns the transaction:
// await knex.transaction((trx) => recordSignup(trx, userId));
//
// // Caller B — Prisma owns the transaction (mix with Prisma-native calls freely):
// await prisma.$transaction(async (t) => {
// const u = await t.user.create({ data: { name: 'Alice' } }); // Prisma-native
// await recordSignup(t, u.id); // Knex on the same tx
// }, { timeout: 30000, maxWait: 5000 }); // raise from 5s default for long work
//
// Both paths return identical JS types on all core columns (int, bigint->string,
// numeric->string, text, bool, timestamptz, jsonb, uuid, bytea->Buffer, arrays).
// EXCEPTIONS: bare `time` (Prisma Date vs pg string) and `date` under a non-UTC
// server timezone. Fix by casting in SQL: k.select(k.raw('mycol::text as mycol'))
// Use builder methods (select/insert().returning()); `.raw()` container shape differs.
// ============================================================================
const KnexBuilder = require('knex');
const PgClient = require('knex/lib/dialects/postgres');
// Coerce a single Prisma-raw value to node-pg's default shape.
function normalizeCell(v) {
if (v === null || v === undefined) return v;
if (typeof v === 'bigint') return v.toString(); // int8 -> string
if (typeof v === 'object') {
if (v.constructor && typeof v.constructor.isDecimal === 'function' && v.constructor.isDecimal(v))
return v.toString(); // numeric -> string
if (v instanceof Uint8Array && !Buffer.isBuffer(v)) return Buffer.from(v); // bytea -> Buffer
if (Array.isArray(v)) return v.map(normalizeCell); // bigint[]/numeric[]/...
// Date, Buffer, parsed json objects: already match node-pg
}
return v;
}
function normalizeRow(row) {
if (!row || typeof row !== 'object') return row;
const out = {};
for (const key in row) out[key] = normalizeCell(row[key]);
return out;
}
class PrismaTxClient extends PgClient {
constructor(config) { super(config); this._tx = config.__tx; }
_driver() { return {}; }
async checkVersion() { return '16.0'; }
async acquireRawConnection() { return { __knexUid: 'ptx', __tx: this._tx }; }
validateConnection() { return true; }
async destroyRawConnection() {}
async _query(connection, obj) {
if (!obj.sql) throw new Error('empty query');
const tx = connection.__tx;
const bindings = obj.bindings || [];
const wantsRows = obj.method === 'select' || obj.method === 'first' ||
obj.method === 'pluck' || obj.method === 'raw' || !!obj.returning;
obj.response = wantsRows
? await tx.$queryRawUnsafe(obj.sql, ...bindings)
: await tx.$executeRawUnsafe(obj.sql, ...bindings);
return obj;
}
processResponse(obj, runner) {
const resp = obj.response;
if (obj.output) return obj.output.call(runner, resp);
if (obj.method === 'raw') return Array.isArray(resp) ? resp.map(normalizeRow) : resp;
if (obj.method === 'first') return normalizeRow(resp[0]);
if (obj.method === 'pluck') return resp.map((r) => normalizeCell(r[obj.pluck]));
if (obj.method === 'select') return resp.map(normalizeRow);
if (obj.returning) return resp.map(normalizeRow);
return resp; // count
}
}
function knexForTx(tx) {
return KnexBuilder({ client: PrismaTxClient, __tx: tx,
connection: { unused: true }, pool: { min: 0, max: 1, propagateCreateError: true } });
}
function asKnex(handle) {
if (handle && typeof handle.$queryRawUnsafe === 'function') return { k: knexForTx(handle), owned: true };
if (typeof handle === 'function' && handle.client) return { k: handle, owned: false };
throw new Error('asKnex: expected a Knex trx/instance or a Prisma tx client');
}
module.exports = { knexForTx, asKnex, normalizeCell };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment