@nextlyhq/adapter-postgres 0.0.1
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/LICENSE +22 -0
- package/README.md +97 -0
- package/dist/index.cjs +679 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.mjs +674 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +78 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var adapterDrizzle = require('@nextlyhq/adapter-drizzle');
|
|
4
|
+
var types = require('@nextlyhq/adapter-drizzle/types');
|
|
5
|
+
var versionCheck = require('@nextlyhq/adapter-drizzle/version-check');
|
|
6
|
+
var nodePostgres = require('drizzle-orm/node-postgres');
|
|
7
|
+
var pg = require('pg');
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
|
|
11
|
+
// src/provider.ts
|
|
12
|
+
function detectPostgresProvider(url, explicitProvider) {
|
|
13
|
+
if (explicitProvider) {
|
|
14
|
+
const normalized = explicitProvider.toLowerCase();
|
|
15
|
+
if (normalized === "neon" || normalized === "supabase" || normalized === "standard") {
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (url.includes(".neon.tech") || url.includes("neon.")) {
|
|
20
|
+
return "neon";
|
|
21
|
+
}
|
|
22
|
+
if (url.includes(".supabase.") || url.includes("supabase.")) {
|
|
23
|
+
return "supabase";
|
|
24
|
+
}
|
|
25
|
+
return "standard";
|
|
26
|
+
}
|
|
27
|
+
function getProviderDefaults(provider) {
|
|
28
|
+
switch (provider) {
|
|
29
|
+
case "neon":
|
|
30
|
+
return {
|
|
31
|
+
ssl: true,
|
|
32
|
+
poolMax: 5,
|
|
33
|
+
poolMin: 0,
|
|
34
|
+
idleTimeoutMs: 1e4,
|
|
35
|
+
connectionTimeoutMs: 2e4,
|
|
36
|
+
statementTimeoutMs: 3e4,
|
|
37
|
+
retryAttempts: 5
|
|
38
|
+
};
|
|
39
|
+
case "supabase":
|
|
40
|
+
return {
|
|
41
|
+
ssl: true,
|
|
42
|
+
poolMax: 5,
|
|
43
|
+
poolMin: 0,
|
|
44
|
+
idleTimeoutMs: 3e4,
|
|
45
|
+
connectionTimeoutMs: 15e3,
|
|
46
|
+
statementTimeoutMs: 15e3,
|
|
47
|
+
retryAttempts: 3
|
|
48
|
+
};
|
|
49
|
+
case "standard":
|
|
50
|
+
default:
|
|
51
|
+
return {
|
|
52
|
+
ssl: false,
|
|
53
|
+
poolMax: 10,
|
|
54
|
+
poolMin: 0,
|
|
55
|
+
idleTimeoutMs: 3e4,
|
|
56
|
+
connectionTimeoutMs: 15e3,
|
|
57
|
+
statementTimeoutMs: 15e3,
|
|
58
|
+
retryAttempts: 3
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/index.ts
|
|
64
|
+
var VERSION = "0.1.0";
|
|
65
|
+
var DEFAULT_POOL_CONFIG = {
|
|
66
|
+
min: 0,
|
|
67
|
+
max: 5,
|
|
68
|
+
idleTimeoutMs: 3e4,
|
|
69
|
+
connectionTimeoutMs: 15e3
|
|
70
|
+
};
|
|
71
|
+
var PG_ERROR_CODES = {
|
|
72
|
+
// Class 08 - Connection Exception
|
|
73
|
+
"08000": "connection",
|
|
74
|
+
"08003": "connection",
|
|
75
|
+
"08006": "connection",
|
|
76
|
+
"08001": "connection",
|
|
77
|
+
"08004": "connection",
|
|
78
|
+
"08007": "connection",
|
|
79
|
+
"08P01": "connection",
|
|
80
|
+
// Class 23 - Integrity Constraint Violation
|
|
81
|
+
"23000": "constraint",
|
|
82
|
+
"23001": "constraint",
|
|
83
|
+
"23502": "not_null_violation",
|
|
84
|
+
"23503": "foreign_key_violation",
|
|
85
|
+
"23505": "unique_violation",
|
|
86
|
+
"23514": "check_violation",
|
|
87
|
+
"23P01": "constraint",
|
|
88
|
+
// Class 40 - Transaction Rollback
|
|
89
|
+
"40000": "query",
|
|
90
|
+
"40001": "serialization_failure",
|
|
91
|
+
"40002": "constraint",
|
|
92
|
+
"40003": "query",
|
|
93
|
+
"40P01": "deadlock",
|
|
94
|
+
// Class 57 - Operator Intervention
|
|
95
|
+
"57014": "timeout",
|
|
96
|
+
"57P01": "connection",
|
|
97
|
+
"57P02": "connection",
|
|
98
|
+
"57P03": "connection"
|
|
99
|
+
};
|
|
100
|
+
function delay(ms) {
|
|
101
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
102
|
+
}
|
|
103
|
+
var PostgresAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
104
|
+
/**
|
|
105
|
+
* The database dialect - always 'postgresql' for this adapter.
|
|
106
|
+
*/
|
|
107
|
+
dialect = "postgresql";
|
|
108
|
+
/**
|
|
109
|
+
* Adapter configuration.
|
|
110
|
+
*/
|
|
111
|
+
config;
|
|
112
|
+
/**
|
|
113
|
+
* Connection pool instance.
|
|
114
|
+
*/
|
|
115
|
+
pool = null;
|
|
116
|
+
/**
|
|
117
|
+
* Connection state flag.
|
|
118
|
+
*/
|
|
119
|
+
connected = false;
|
|
120
|
+
/**
|
|
121
|
+
* Auto-detected provider (Neon, Supabase, or standard).
|
|
122
|
+
* Set during connect() from DATABASE_URL pattern or DB_PROVIDER env var.
|
|
123
|
+
*/
|
|
124
|
+
detectedProvider = "standard";
|
|
125
|
+
/**
|
|
126
|
+
* Provider-specific connection defaults. Applied as fallbacks when
|
|
127
|
+
* user config doesn't specify a value.
|
|
128
|
+
*/
|
|
129
|
+
providerDefaults = getProviderDefaults("standard");
|
|
130
|
+
/**
|
|
131
|
+
* Creates a new PostgreSQL adapter instance.
|
|
132
|
+
*
|
|
133
|
+
* @param config - Adapter configuration
|
|
134
|
+
*/
|
|
135
|
+
constructor(config) {
|
|
136
|
+
super();
|
|
137
|
+
this.config = config;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Establishes a connection to the PostgreSQL database.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* This method initializes the connection pool and verifies connectivity
|
|
144
|
+
* by executing a simple query. It is idempotent - calling it multiple
|
|
145
|
+
* times will not create multiple pools.
|
|
146
|
+
*
|
|
147
|
+
* @throws {DatabaseError} If connection fails
|
|
148
|
+
*/
|
|
149
|
+
async connect() {
|
|
150
|
+
if (this.connected && this.pool) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const url = this.config.url || "";
|
|
154
|
+
this.detectedProvider = detectPostgresProvider(
|
|
155
|
+
url,
|
|
156
|
+
process.env.DB_PROVIDER
|
|
157
|
+
);
|
|
158
|
+
this.providerDefaults = getProviderDefaults(this.detectedProvider);
|
|
159
|
+
if (this.config.logger?.info) {
|
|
160
|
+
const source = process.env.DB_PROVIDER ? "(explicit)" : "(auto-detected)";
|
|
161
|
+
this.config.logger.info(
|
|
162
|
+
`PostgreSQL provider: ${this.detectedProvider} ${source}`,
|
|
163
|
+
{}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const retryableNodeCodes = /* @__PURE__ */ new Set([
|
|
167
|
+
"ETIMEDOUT",
|
|
168
|
+
"ECONNREFUSED",
|
|
169
|
+
"ECONNRESET",
|
|
170
|
+
"ENOTFOUND",
|
|
171
|
+
"EAI_AGAIN"
|
|
172
|
+
]);
|
|
173
|
+
const maxAttempts = this.providerDefaults.retryAttempts;
|
|
174
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
175
|
+
try {
|
|
176
|
+
const poolConfig = this.buildPoolConfig();
|
|
177
|
+
this.pool = new pg.Pool(poolConfig);
|
|
178
|
+
this.pool.on("error", (err) => {
|
|
179
|
+
if (this.config.logger?.error) {
|
|
180
|
+
this.config.logger.error(err, {
|
|
181
|
+
context: "pool_error",
|
|
182
|
+
message: "Unexpected error on idle client"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const client = await this.pool.connect();
|
|
187
|
+
try {
|
|
188
|
+
await client.query("SELECT 1");
|
|
189
|
+
await versionCheck.checkDialectVersion(client, "postgresql", {
|
|
190
|
+
// Why: route any future variant warnings through the adapter's
|
|
191
|
+
// logger. PG has no recognized variants today, but this keeps
|
|
192
|
+
// the integration symmetric with MySQL.
|
|
193
|
+
onWarning: (msg) => this.config.logger?.warn?.(msg)
|
|
194
|
+
});
|
|
195
|
+
this.connected = true;
|
|
196
|
+
if (this.config.logger?.info) {
|
|
197
|
+
this.config.logger.info("PostgreSQL connection established", {
|
|
198
|
+
host: this.config.host ?? "from URL",
|
|
199
|
+
database: this.config.database ?? "from URL"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
} finally {
|
|
204
|
+
client.release();
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (this.pool) {
|
|
208
|
+
await this.pool.end().catch(() => {
|
|
209
|
+
});
|
|
210
|
+
this.pool = null;
|
|
211
|
+
}
|
|
212
|
+
const nodeError = error;
|
|
213
|
+
const isRetryable = nodeError.code != null && retryableNodeCodes.has(nodeError.code);
|
|
214
|
+
if (isRetryable && attempt < maxAttempts) {
|
|
215
|
+
const waitMs = 1e3 * attempt;
|
|
216
|
+
const msg = `PostgreSQL connection attempt ${attempt}/${maxAttempts} failed with ${nodeError.code}, retrying in ${waitMs}ms...`;
|
|
217
|
+
if (this.config.logger?.warn) {
|
|
218
|
+
this.config.logger.warn(msg);
|
|
219
|
+
} else {
|
|
220
|
+
console.warn(`[PostgresAdapter] ${msg}`);
|
|
221
|
+
}
|
|
222
|
+
await delay(waitMs);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
throw this.classifyError(error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Closes the database connection and releases all pool resources.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* This method is idempotent - calling it multiple times is safe.
|
|
234
|
+
* It waits for all checked-out clients to be returned before shutting down.
|
|
235
|
+
*/
|
|
236
|
+
async disconnect() {
|
|
237
|
+
if (!this.pool) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
await this.pool.end();
|
|
242
|
+
if (this.config.logger?.info) {
|
|
243
|
+
this.config.logger.info("PostgreSQL connection closed");
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
this.pool = null;
|
|
247
|
+
this.connected = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Checks if the adapter is currently connected.
|
|
252
|
+
*
|
|
253
|
+
* @returns True if connected and pool is available
|
|
254
|
+
*/
|
|
255
|
+
isConnected() {
|
|
256
|
+
return this.connected && this.pool !== null;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Executes a raw SQL query.
|
|
260
|
+
*
|
|
261
|
+
* @param sql - SQL statement with $1, $2, ... placeholders
|
|
262
|
+
* @param params - Query parameters
|
|
263
|
+
* @returns Array of result rows
|
|
264
|
+
*
|
|
265
|
+
* @throws {DatabaseError} If query execution fails
|
|
266
|
+
*/
|
|
267
|
+
async executeQuery(sql, params = []) {
|
|
268
|
+
const pool = this.ensurePool();
|
|
269
|
+
const startTime = Date.now();
|
|
270
|
+
const retryableNodeCodes = /* @__PURE__ */ new Set([
|
|
271
|
+
"ETIMEDOUT",
|
|
272
|
+
"ECONNRESET",
|
|
273
|
+
"ECONNREFUSED"
|
|
274
|
+
]);
|
|
275
|
+
const maxQueryAttempts = 3;
|
|
276
|
+
for (let attempt = 1; attempt <= maxQueryAttempts; attempt++) {
|
|
277
|
+
try {
|
|
278
|
+
const result = await pool.query(sql, params);
|
|
279
|
+
if (this.config.logger?.query) {
|
|
280
|
+
const durationMs = Date.now() - startTime;
|
|
281
|
+
this.config.logger.query(sql, params, durationMs);
|
|
282
|
+
}
|
|
283
|
+
return result.rows;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const nodeError = error;
|
|
286
|
+
const isRetryable = nodeError.code != null && retryableNodeCodes.has(nodeError.code);
|
|
287
|
+
if (isRetryable && attempt < maxQueryAttempts) {
|
|
288
|
+
const waitMs = 500 * attempt;
|
|
289
|
+
console.warn(
|
|
290
|
+
`[PostgresAdapter] Query attempt ${attempt}/${maxQueryAttempts} failed with ${nodeError.code}, retrying in ${waitMs}ms...`
|
|
291
|
+
);
|
|
292
|
+
await delay(waitMs);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
throw this.classifyError(error, sql);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
throw this.classifyError(new Error("executeQuery: exhausted retries"));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Executes a callback within a database transaction.
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* PostgreSQL supports full ACID transactions with savepoints.
|
|
305
|
+
* If the callback throws, the transaction is rolled back.
|
|
306
|
+
*
|
|
307
|
+
* Supports automatic retry for serialization failures (40001) and
|
|
308
|
+
* deadlocks (40P01) when `retryCount` is specified in options.
|
|
309
|
+
*
|
|
310
|
+
* @param callback - Function to execute within the transaction
|
|
311
|
+
* @param options - Transaction options (isolation level, timeout, retry)
|
|
312
|
+
* @returns The result of the callback
|
|
313
|
+
*
|
|
314
|
+
* @throws {DatabaseError} If transaction fails after all retries
|
|
315
|
+
*/
|
|
316
|
+
async transaction(callback, options) {
|
|
317
|
+
const pool = this.ensurePool();
|
|
318
|
+
const maxAttempts = (options?.retryCount ?? 0) + 1;
|
|
319
|
+
const retryDelayMs = options?.retryDelayMs ?? 100;
|
|
320
|
+
let lastError;
|
|
321
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
322
|
+
const client = await pool.connect();
|
|
323
|
+
const startTime = Date.now();
|
|
324
|
+
try {
|
|
325
|
+
await this.beginTransaction(client, options);
|
|
326
|
+
const ctx = this.createTransactionContext(client);
|
|
327
|
+
const result = await callback(ctx);
|
|
328
|
+
await client.query("COMMIT");
|
|
329
|
+
if (this.config.logger?.debug) {
|
|
330
|
+
const durationMs = Date.now() - startTime;
|
|
331
|
+
this.config.logger.debug("Transaction committed", {
|
|
332
|
+
attempt,
|
|
333
|
+
durationMs
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
await client.query("ROLLBACK").catch(() => {
|
|
339
|
+
});
|
|
340
|
+
lastError = error;
|
|
341
|
+
const pgError = error;
|
|
342
|
+
const isRetryable = pgError.code === "40001" || // serialization_failure
|
|
343
|
+
pgError.code === "40P01";
|
|
344
|
+
if (isRetryable && attempt < maxAttempts) {
|
|
345
|
+
if (this.config.logger?.warn) {
|
|
346
|
+
this.config.logger.warn(
|
|
347
|
+
`Transaction failed with ${pgError.code}, retrying (${attempt}/${maxAttempts})`,
|
|
348
|
+
{ code: pgError.code, attempt }
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
await delay(retryDelayMs * attempt);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
throw this.classifyError(error);
|
|
355
|
+
} finally {
|
|
356
|
+
client.release();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
throw this.classifyError(lastError);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Returns the database capabilities for PostgreSQL.
|
|
363
|
+
*
|
|
364
|
+
* @remarks
|
|
365
|
+
* PostgreSQL has the most comprehensive feature set of all supported
|
|
366
|
+
* databases, including JSONB, arrays, full-text search, and more.
|
|
367
|
+
*/
|
|
368
|
+
getCapabilities() {
|
|
369
|
+
return {
|
|
370
|
+
dialect: "postgresql",
|
|
371
|
+
supportsJsonb: true,
|
|
372
|
+
supportsJson: true,
|
|
373
|
+
supportsArrays: true,
|
|
374
|
+
supportsGeneratedColumns: true,
|
|
375
|
+
supportsFts: true,
|
|
376
|
+
supportsIlike: true,
|
|
377
|
+
supportsReturning: true,
|
|
378
|
+
supportsSavepoints: true,
|
|
379
|
+
supportsOnConflict: true,
|
|
380
|
+
maxParamsPerQuery: 65535,
|
|
381
|
+
// PostgreSQL limit
|
|
382
|
+
maxIdentifierLength: 63
|
|
383
|
+
// PostgreSQL default
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Returns connection pool statistics.
|
|
388
|
+
*
|
|
389
|
+
* @returns Pool stats or null if not connected
|
|
390
|
+
*/
|
|
391
|
+
getPoolStats() {
|
|
392
|
+
if (!this.pool) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
total: this.pool.totalCount,
|
|
397
|
+
idle: this.pool.idleCount,
|
|
398
|
+
waiting: this.pool.waitingCount,
|
|
399
|
+
active: this.pool.totalCount - this.pool.idleCount
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Override insertMany for bulk insert optimization.
|
|
404
|
+
*
|
|
405
|
+
* @remarks
|
|
406
|
+
* Uses a single multi-row INSERT statement for better performance
|
|
407
|
+
* when inserting multiple records.
|
|
408
|
+
*/
|
|
409
|
+
async insertMany(table, data, options) {
|
|
410
|
+
if (data.length === 0) {
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
if (data.length === 1) {
|
|
414
|
+
const result = await this.insert(table, data[0], options);
|
|
415
|
+
return [result];
|
|
416
|
+
}
|
|
417
|
+
const columns = Object.keys(data[0]);
|
|
418
|
+
const params = [];
|
|
419
|
+
const valuesClauses = [];
|
|
420
|
+
for (let i = 0; i < data.length; i++) {
|
|
421
|
+
const record = data[i];
|
|
422
|
+
const placeholders = [];
|
|
423
|
+
for (const col of columns) {
|
|
424
|
+
params.push(record[col]);
|
|
425
|
+
placeholders.push(`$${params.length}`);
|
|
426
|
+
}
|
|
427
|
+
valuesClauses.push(`(${placeholders.join(", ")})`);
|
|
428
|
+
}
|
|
429
|
+
const columnList = columns.map((col) => this.escapeIdentifier(col)).join(", ");
|
|
430
|
+
let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columnList}) VALUES ${valuesClauses.join(", ")}`;
|
|
431
|
+
if (options?.returning) {
|
|
432
|
+
const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
|
|
433
|
+
sql += ` RETURNING ${returning}`;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
return await this.executeQuery(sql, params);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
throw this.handleQueryError(error, "insertMany", table);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// ============================================================
|
|
442
|
+
// Protected Helper Methods
|
|
443
|
+
// ============================================================
|
|
444
|
+
/**
|
|
445
|
+
* Ensures pool is connected and returns it.
|
|
446
|
+
*
|
|
447
|
+
* @throws {DatabaseError} If not connected
|
|
448
|
+
*/
|
|
449
|
+
ensurePool() {
|
|
450
|
+
if (!this.pool) {
|
|
451
|
+
throw types.createDatabaseError({
|
|
452
|
+
kind: "connection",
|
|
453
|
+
message: "PostgresAdapter is not connected. Call connect() first."
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return this.pool;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Return the typed Drizzle instance for PostgreSQL.
|
|
460
|
+
* Guarded for server-only usage and requires an active connection.
|
|
461
|
+
*
|
|
462
|
+
* @param schema - Optional schema for relational queries (db.query.*)
|
|
463
|
+
* @returns Drizzle ORM instance wrapping the pg pool connection
|
|
464
|
+
* @throws {Error} If called in browser or not connected
|
|
465
|
+
*/
|
|
466
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
467
|
+
getDrizzle(schema) {
|
|
468
|
+
if (typeof window !== "undefined") {
|
|
469
|
+
throw new Error("getDrizzle() is server-only");
|
|
470
|
+
}
|
|
471
|
+
const pool = this.ensurePool();
|
|
472
|
+
return schema ? nodePostgres.drizzle(pool, { schema }) : nodePostgres.drizzle(pool);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Builds pg Pool configuration from adapter config.
|
|
476
|
+
*/
|
|
477
|
+
buildPoolConfig() {
|
|
478
|
+
const config = {};
|
|
479
|
+
if (this.config.url) {
|
|
480
|
+
config.connectionString = this.config.url;
|
|
481
|
+
} else {
|
|
482
|
+
if (this.config.host) config.host = this.config.host;
|
|
483
|
+
if (this.config.port) config.port = this.config.port;
|
|
484
|
+
if (this.config.database) config.database = this.config.database;
|
|
485
|
+
if (this.config.user) config.user = this.config.user;
|
|
486
|
+
if (this.config.password) config.password = this.config.password;
|
|
487
|
+
}
|
|
488
|
+
config.min = this.config.pool?.min ?? this.providerDefaults.poolMin ?? DEFAULT_POOL_CONFIG.min;
|
|
489
|
+
config.max = this.config.pool?.max ?? this.providerDefaults.poolMax ?? DEFAULT_POOL_CONFIG.max;
|
|
490
|
+
config.idleTimeoutMillis = this.config.pool?.idleTimeoutMs ?? this.providerDefaults.idleTimeoutMs ?? DEFAULT_POOL_CONFIG.idleTimeoutMs;
|
|
491
|
+
config.connectionTimeoutMillis = this.config.pool?.connectionTimeoutMs ?? this.providerDefaults.connectionTimeoutMs ?? DEFAULT_POOL_CONFIG.connectionTimeoutMs;
|
|
492
|
+
config.keepAlive = true;
|
|
493
|
+
config.keepAliveInitialDelayMillis = 1e4;
|
|
494
|
+
if (this.config.ssl) {
|
|
495
|
+
if (typeof this.config.ssl === "boolean") {
|
|
496
|
+
config.ssl = this.config.ssl;
|
|
497
|
+
} else {
|
|
498
|
+
if (this.config.ssl.rejectUnauthorized === false) {
|
|
499
|
+
console.warn(
|
|
500
|
+
"[nextly/adapter-postgres] ssl.rejectUnauthorized is set to false \u2014 TLS certificates will not be validated. This is unsafe on untrusted networks. Provide a trusted `ca` cert instead, or remove the rejectUnauthorized override."
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
config.ssl = {
|
|
504
|
+
rejectUnauthorized: this.config.ssl.rejectUnauthorized,
|
|
505
|
+
ca: this.config.ssl.ca,
|
|
506
|
+
cert: this.config.ssl.cert,
|
|
507
|
+
key: this.config.ssl.key
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
} else if (this.providerDefaults.ssl) {
|
|
511
|
+
config.ssl = { rejectUnauthorized: true };
|
|
512
|
+
}
|
|
513
|
+
if (this.config.applicationName) {
|
|
514
|
+
config.application_name = this.config.applicationName;
|
|
515
|
+
}
|
|
516
|
+
if (this.config.statementTimeout) {
|
|
517
|
+
config.statement_timeout = this.config.statementTimeout;
|
|
518
|
+
}
|
|
519
|
+
if (this.config.queryTimeout) {
|
|
520
|
+
config.query_timeout = this.config.queryTimeout;
|
|
521
|
+
}
|
|
522
|
+
return config;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Begins a transaction with the specified options.
|
|
526
|
+
*/
|
|
527
|
+
async beginTransaction(client, options) {
|
|
528
|
+
let beginSql = "BEGIN";
|
|
529
|
+
if (options?.isolationLevel) {
|
|
530
|
+
const isolationMap = {
|
|
531
|
+
"read uncommitted": "READ UNCOMMITTED",
|
|
532
|
+
"read committed": "READ COMMITTED",
|
|
533
|
+
"repeatable read": "REPEATABLE READ",
|
|
534
|
+
serializable: "SERIALIZABLE"
|
|
535
|
+
};
|
|
536
|
+
const level = isolationMap[options.isolationLevel];
|
|
537
|
+
if (level) {
|
|
538
|
+
beginSql += ` ISOLATION LEVEL ${level}`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (options?.readOnly) {
|
|
542
|
+
beginSql += " READ ONLY";
|
|
543
|
+
}
|
|
544
|
+
await client.query(beginSql);
|
|
545
|
+
if (options?.timeoutMs) {
|
|
546
|
+
await client.query(`SET LOCAL statement_timeout = ${options.timeoutMs}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Creates a TransactionContext for the given client.
|
|
551
|
+
*/
|
|
552
|
+
createTransactionContext(client) {
|
|
553
|
+
return {
|
|
554
|
+
execute: async (sql, params = []) => {
|
|
555
|
+
const result = await client.query(sql, params);
|
|
556
|
+
return result.rows;
|
|
557
|
+
},
|
|
558
|
+
insert: async (table, data, options) => {
|
|
559
|
+
const columns = Object.keys(data);
|
|
560
|
+
const values = Object.values(data);
|
|
561
|
+
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
562
|
+
let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES (${placeholders})`;
|
|
563
|
+
if (options?.returning) {
|
|
564
|
+
const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
|
|
565
|
+
sql += ` RETURNING ${returning}`;
|
|
566
|
+
} else {
|
|
567
|
+
sql += " RETURNING *";
|
|
568
|
+
}
|
|
569
|
+
const result = await client.query(sql, values);
|
|
570
|
+
return result.rows[0];
|
|
571
|
+
},
|
|
572
|
+
insertMany: async (table, data, options) => {
|
|
573
|
+
if (data.length === 0) return [];
|
|
574
|
+
const columns = Object.keys(data[0]);
|
|
575
|
+
const params = [];
|
|
576
|
+
const valuesClauses = [];
|
|
577
|
+
for (const record of data) {
|
|
578
|
+
const placeholders = [];
|
|
579
|
+
for (const col of columns) {
|
|
580
|
+
params.push(record[col]);
|
|
581
|
+
placeholders.push(`$${params.length}`);
|
|
582
|
+
}
|
|
583
|
+
valuesClauses.push(`(${placeholders.join(", ")})`);
|
|
584
|
+
}
|
|
585
|
+
let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES ${valuesClauses.join(", ")}`;
|
|
586
|
+
if (options?.returning) {
|
|
587
|
+
const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
|
|
588
|
+
sql += ` RETURNING ${returning}`;
|
|
589
|
+
} else {
|
|
590
|
+
sql += " RETURNING *";
|
|
591
|
+
}
|
|
592
|
+
const result = await client.query(sql, params);
|
|
593
|
+
return result.rows;
|
|
594
|
+
},
|
|
595
|
+
// TransactionContext CRUD methods delegate to the adapter's CRUD
|
|
596
|
+
// which uses Drizzle query API via the TableResolver.
|
|
597
|
+
// The Drizzle transaction is handled at a higher level.
|
|
598
|
+
select: async (table, options) => {
|
|
599
|
+
return this.select(table, options);
|
|
600
|
+
},
|
|
601
|
+
selectOne: async (table, options) => {
|
|
602
|
+
return this.selectOne(table, options);
|
|
603
|
+
},
|
|
604
|
+
update: async (table, data, where, options) => {
|
|
605
|
+
return this.update(table, data, where, options);
|
|
606
|
+
},
|
|
607
|
+
delete: async (table, where, _options) => {
|
|
608
|
+
return this.delete(table, where);
|
|
609
|
+
},
|
|
610
|
+
upsert: async (table, data, options) => {
|
|
611
|
+
return this.upsert(table, data, options);
|
|
612
|
+
},
|
|
613
|
+
savepoint: async (name) => {
|
|
614
|
+
await client.query(`SAVEPOINT ${this.escapeIdentifier(name)}`);
|
|
615
|
+
},
|
|
616
|
+
rollbackToSavepoint: async (name) => {
|
|
617
|
+
await client.query(
|
|
618
|
+
`ROLLBACK TO SAVEPOINT ${this.escapeIdentifier(name)}`
|
|
619
|
+
);
|
|
620
|
+
},
|
|
621
|
+
releaseSavepoint: async (name) => {
|
|
622
|
+
await client.query(`RELEASE SAVEPOINT ${this.escapeIdentifier(name)}`);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Classifies a PostgreSQL error into a DatabaseError.
|
|
628
|
+
*
|
|
629
|
+
* @param error - Original error from pg
|
|
630
|
+
* @param sql - SQL statement that caused the error (optional)
|
|
631
|
+
* @returns DatabaseError with proper classification
|
|
632
|
+
*/
|
|
633
|
+
classifyError(error, sql) {
|
|
634
|
+
if (types.isDatabaseError(error)) return error;
|
|
635
|
+
const pgError = error;
|
|
636
|
+
const kind = pgError.code && PG_ERROR_CODES[pgError.code] || "unknown";
|
|
637
|
+
let message = pgError.message ?? String(error);
|
|
638
|
+
if (sql && kind === "query") {
|
|
639
|
+
message = `Query failed: ${message}`;
|
|
640
|
+
}
|
|
641
|
+
return types.createDatabaseError({
|
|
642
|
+
kind,
|
|
643
|
+
message,
|
|
644
|
+
code: pgError.code,
|
|
645
|
+
constraint: pgError.constraint,
|
|
646
|
+
table: pgError.table,
|
|
647
|
+
column: pgError.column,
|
|
648
|
+
detail: pgError.detail,
|
|
649
|
+
hint: pgError.hint,
|
|
650
|
+
cause: error instanceof Error ? error : void 0
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Override handleQueryError to use PostgreSQL-specific classification.
|
|
655
|
+
*/
|
|
656
|
+
handleQueryError(error, operation, table) {
|
|
657
|
+
const dbError = this.classifyError(error);
|
|
658
|
+
if (!dbError.message.includes(operation)) {
|
|
659
|
+
dbError.message = `${operation} operation failed on table '${table}': ${dbError.message}`;
|
|
660
|
+
}
|
|
661
|
+
if (!dbError.table) {
|
|
662
|
+
dbError.table = table;
|
|
663
|
+
}
|
|
664
|
+
return dbError;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
function createPostgresAdapter(config) {
|
|
668
|
+
return new PostgresAdapter(config);
|
|
669
|
+
}
|
|
670
|
+
function isPostgresAdapter(value) {
|
|
671
|
+
return value instanceof PostgresAdapter;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
exports.PostgresAdapter = PostgresAdapter;
|
|
675
|
+
exports.VERSION = VERSION;
|
|
676
|
+
exports.createPostgresAdapter = createPostgresAdapter;
|
|
677
|
+
exports.isPostgresAdapter = isPostgresAdapter;
|
|
678
|
+
//# sourceMappingURL=index.cjs.map
|
|
679
|
+
//# sourceMappingURL=index.cjs.map
|