@vibeorm/adapter-pg 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.ts +97 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeorm/adapter-pg",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "node-postgres adapter for VibeORM — use VibeORM with pg on Bun or Node.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"url": "https://github.com/vibeorm/vibeorm/issues"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@vibeorm/runtime": "1.
|
|
34
|
+
"@vibeorm/runtime": "1.2.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"pg": ">=8"
|
package/src/index.ts
CHANGED
|
@@ -116,16 +116,29 @@ class PgArrayParam {
|
|
|
116
116
|
// ─── Value Serialization ──────────────────────────────────────────
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Pre-process query parameter values for the pg driver.
|
|
119
|
+
* Pre-process query parameter values for the pg driver — ORM hot path.
|
|
120
120
|
*
|
|
121
121
|
* The pg driver auto-converts JS arrays to PostgreSQL array literals (`{1,2,3}`)
|
|
122
|
-
* and JS objects to `[object Object]`.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
122
|
+
* and JS objects to `[object Object]`. The ORM doesn't know which column a
|
|
123
|
+
* value lands in (it doesn't carry typed-parameter hints), and for `json` /
|
|
124
|
+
* `jsonb` columns it wants any JS object OR JS array passed through as JSON.
|
|
125
|
+
* Therefore on the ORM path we JSON.stringify both objects and arrays —
|
|
126
|
+
* scalar-list array values (`= ANY()` / `String[]` / `Int[]`) are already
|
|
127
|
+
* tagged with `PgArrayParam` by `formatArrayParam` and unwrapped here without
|
|
128
|
+
* stringification.
|
|
125
129
|
*
|
|
126
|
-
* -
|
|
127
|
-
*
|
|
128
|
-
*
|
|
130
|
+
* Raw-query paths use `serializeParamsRaw` instead (see below) which does NOT
|
|
131
|
+
* stringify plain JS arrays so that `db.$queryRawUnsafe('… WHERE "id" = ANY($1)',
|
|
132
|
+
* [1, 2, 3])` actually matches rows.
|
|
133
|
+
*
|
|
134
|
+
* - PgArrayParam wrappers are unwrapped to raw arrays (for = ANY() queries
|
|
135
|
+
* and for scalar-list array operations the ORM emits).
|
|
136
|
+
* - Plain objects and arrays are JSON.stringify'd (for json/jsonb columns).
|
|
137
|
+
* - Dates, Buffers, null, and primitives pass through unchanged.
|
|
138
|
+
*
|
|
139
|
+
* TODO: PgArrayParam nesting is not currently reachable through the public API,
|
|
140
|
+
* so the unwrap is intentionally non-recursive. If PgArrayParam ever becomes
|
|
141
|
+
* user-constructible and nestable inside other arrays/objects, make recursive.
|
|
129
142
|
*/
|
|
130
143
|
function serializeParams(params: { values: unknown[] }): unknown[] {
|
|
131
144
|
return params.values.map((v) => {
|
|
@@ -148,6 +161,49 @@ function serializeParams(params: { values: unknown[] }): unknown[] {
|
|
|
148
161
|
});
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Pre-process query parameter values for the pg driver — RAW QUERY path.
|
|
166
|
+
*
|
|
167
|
+
* Identical to `serializeParams` EXCEPT that plain JS arrays are passed through
|
|
168
|
+
* to `pg` instead of being JSON.stringify'd. node-postgres natively converts a
|
|
169
|
+
* JS array to a PostgreSQL array literal, which is what users expect when they
|
|
170
|
+
* write raw queries like:
|
|
171
|
+
*
|
|
172
|
+
* db.$queryRawUnsafe('SELECT * FROM "User" WHERE "id" = ANY($1)', [1, 2, 3])
|
|
173
|
+
* db.$queryRaw`SELECT * FROM "User" WHERE "id" = ANY(${[1, 2, 3]})`
|
|
174
|
+
*
|
|
175
|
+
* (Previously the adapter JSON.stringify'd the array, producing `"[1,2,3]"` —
|
|
176
|
+
* a JSON string that PG could not match against any row.)
|
|
177
|
+
*
|
|
178
|
+
* Tradeoff: a user who wants to bind a JS array INTO a `json` / `jsonb` column
|
|
179
|
+
* from a raw query must `JSON.stringify(...)` it themselves. Plain objects are
|
|
180
|
+
* still auto-stringified here, so the common `{ foo: "bar" }` case continues
|
|
181
|
+
* to work.
|
|
182
|
+
*/
|
|
183
|
+
function serializeParamsRaw(params: { values: unknown[] }): unknown[] {
|
|
184
|
+
return params.values.map((v) => {
|
|
185
|
+
if (v instanceof PgArrayParam) {
|
|
186
|
+
return v.values;
|
|
187
|
+
}
|
|
188
|
+
// Plain JS arrays pass through — pg converts to PG array literal natively.
|
|
189
|
+
if (Array.isArray(v)) {
|
|
190
|
+
return v;
|
|
191
|
+
}
|
|
192
|
+
// JSON.stringify plain non-array objects for json/jsonb columns.
|
|
193
|
+
if (
|
|
194
|
+
v !== null &&
|
|
195
|
+
v !== undefined &&
|
|
196
|
+
typeof v === "object" &&
|
|
197
|
+
!(v instanceof Date) &&
|
|
198
|
+
!Buffer.isBuffer(v) &&
|
|
199
|
+
!(v instanceof Uint8Array)
|
|
200
|
+
) {
|
|
201
|
+
return JSON.stringify(v);
|
|
202
|
+
}
|
|
203
|
+
return v;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
151
207
|
/**
|
|
152
208
|
* Create a VibeORM database adapter using node-postgres (pg).
|
|
153
209
|
*
|
|
@@ -266,34 +322,48 @@ export function pgAdapter(options?: PgAdapterOptions): DatabaseAdapter {
|
|
|
266
322
|
};
|
|
267
323
|
}
|
|
268
324
|
|
|
269
|
-
|
|
270
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Build a client-scoped adapter (already inside a transaction). Nested
|
|
327
|
+
* `.transaction(fn)` calls open SAVEPOINTs on the same client.
|
|
328
|
+
*
|
|
329
|
+
* `savepointCounter` is shared by reference across every adapter created
|
|
330
|
+
* for one top-level transaction so sibling and deeply-nested savepoints
|
|
331
|
+
* never collide (Bug #11 — counter previously reset at each nesting level,
|
|
332
|
+
* so two sibling sub-transactions both got `vibeorm_sp_0`).
|
|
333
|
+
*/
|
|
334
|
+
function createClientAdapter(params: {
|
|
335
|
+
client: PgPoolClient;
|
|
336
|
+
savepointCounter: { n: number };
|
|
337
|
+
}): DatabaseAdapter {
|
|
338
|
+
const { client, savepointCounter } = params;
|
|
271
339
|
|
|
272
340
|
return {
|
|
273
|
-
async execute(
|
|
274
|
-
const config = buildQueryConfig(
|
|
341
|
+
async execute(execParams) {
|
|
342
|
+
const config = buildQueryConfig(execParams);
|
|
275
343
|
const result = await client.query(config);
|
|
276
344
|
return result.rows;
|
|
277
345
|
},
|
|
278
346
|
|
|
279
|
-
async executeUnsafe(
|
|
280
|
-
const values =
|
|
281
|
-
const result = await client.query(
|
|
347
|
+
async executeUnsafe(execParams) {
|
|
348
|
+
const values = execParams.values ? serializeParamsRaw({ values: execParams.values }) : undefined;
|
|
349
|
+
const result = await client.query(execParams.text, values);
|
|
282
350
|
const affectedRows = result.rowCount ?? 0;
|
|
283
351
|
return { rows: result.rows, affectedRows };
|
|
284
352
|
},
|
|
285
353
|
|
|
286
354
|
async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>): Promise<T> {
|
|
287
|
-
// Nested transactions use unique SAVEPOINTs
|
|
288
|
-
const spName = `vibeorm_sp_${savepointCounter++}`;
|
|
355
|
+
// Nested transactions use unique SAVEPOINTs.
|
|
356
|
+
const spName = `vibeorm_sp_${savepointCounter.n++}`;
|
|
289
357
|
await client.query(`SAVEPOINT ${spName}`);
|
|
290
358
|
try {
|
|
291
|
-
const nestedAdapter = createClientAdapter(client);
|
|
359
|
+
const nestedAdapter = createClientAdapter({ client, savepointCounter });
|
|
292
360
|
const result = await fn(nestedAdapter);
|
|
293
361
|
await client.query(`RELEASE SAVEPOINT ${spName}`);
|
|
294
362
|
return result;
|
|
295
363
|
} catch (err) {
|
|
296
|
-
|
|
364
|
+
// Bug #7: best-effort rollback — never mask the original error if
|
|
365
|
+
// `ROLLBACK TO SAVEPOINT` itself fails (e.g. the client died).
|
|
366
|
+
try { await client.query(`ROLLBACK TO SAVEPOINT ${spName}`); } catch { /* rollback best-effort */ }
|
|
297
367
|
throw err;
|
|
298
368
|
}
|
|
299
369
|
},
|
|
@@ -331,7 +401,7 @@ export function pgAdapter(options?: PgAdapterOptions): DatabaseAdapter {
|
|
|
331
401
|
|
|
332
402
|
async executeUnsafe(params) {
|
|
333
403
|
const p = getPool();
|
|
334
|
-
const values = params.values ?
|
|
404
|
+
const values = params.values ? serializeParamsRaw({ values: params.values }) : undefined;
|
|
335
405
|
const result = await p.query(params.text, values);
|
|
336
406
|
const affectedRows = result.rowCount ?? 0;
|
|
337
407
|
return { rows: result.rows, affectedRows };
|
|
@@ -340,6 +410,9 @@ export function pgAdapter(options?: PgAdapterOptions): DatabaseAdapter {
|
|
|
340
410
|
async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>, options?: TransactionOptions): Promise<T> {
|
|
341
411
|
const p = getPool();
|
|
342
412
|
const client = await p.connect();
|
|
413
|
+
// Per top-level transaction: one shared savepoint counter for all
|
|
414
|
+
// sibling/nested SAVEPOINTs (Bug #11).
|
|
415
|
+
const savepointCounter = { n: 0 };
|
|
343
416
|
try {
|
|
344
417
|
const isolation = options?.isolationLevel
|
|
345
418
|
? ` ISOLATION LEVEL ${isolationLevelToSql({ level: options.isolationLevel })}`
|
|
@@ -350,12 +423,15 @@ export function pgAdapter(options?: PgAdapterOptions): DatabaseAdapter {
|
|
|
350
423
|
await client.query(`SET LOCAL statement_timeout = ${Number(options.timeout)}`);
|
|
351
424
|
}
|
|
352
425
|
|
|
353
|
-
const txAdapter = createClientAdapter(client);
|
|
426
|
+
const txAdapter = createClientAdapter({ client, savepointCounter });
|
|
354
427
|
const result = await fn(txAdapter);
|
|
355
428
|
await client.query("COMMIT");
|
|
356
429
|
return result;
|
|
357
430
|
} catch (err) {
|
|
358
|
-
|
|
431
|
+
// Bug #7: best-effort ROLLBACK — never mask the original error if
|
|
432
|
+
// ROLLBACK itself fails (e.g. the connection was killed). pg's pool
|
|
433
|
+
// auto-discards the client on `release()` if its state is broken.
|
|
434
|
+
try { await client.query("ROLLBACK"); } catch { /* rollback best-effort */ }
|
|
359
435
|
throw err;
|
|
360
436
|
} finally {
|
|
361
437
|
client.release();
|