Created
July 7, 2026 11:57
-
-
Save public/4a596b6ad625448366157d55ba2742d1 to your computer and use it in GitHub Desktop.
Prisma to Knex bridge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================================ | |
| // 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