turbine-orm 0.5.0 → 0.7.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/README.md +194 -26
- package/dist/cjs/cli/config.js +5 -15
- package/dist/cjs/cli/index.js +240 -41
- package/dist/cjs/cli/migrate.js +71 -46
- package/dist/cjs/cli/ui.js +5 -9
- package/dist/cjs/client.js +109 -46
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +33 -13
- package/dist/cjs/index.js +39 -20
- package/dist/cjs/introspect.js +3 -5
- package/dist/cjs/pipeline.js +9 -2
- package/dist/cjs/query.js +442 -109
- package/dist/cjs/schema-builder.js +93 -24
- package/dist/cjs/schema-sql.js +157 -19
- package/dist/cjs/schema.js +5 -2
- package/dist/cjs/serverless.js +87 -176
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +245 -46
- package/dist/cli/migrate.d.ts +6 -1
- package/dist/cli/migrate.js +72 -47
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +77 -4
- package/dist/client.js +109 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +36 -16
- package/dist/index.d.ts +11 -9
- package/dist/index.js +16 -12
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +4 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +9 -2
- package/dist/query.d.ts +257 -36
- package/dist/query.js +443 -110
- package/dist/schema-builder.d.ts +2 -2
- package/dist/schema-builder.js +93 -25
- package/dist/schema-sql.d.ts +7 -3
- package/dist/schema-sql.js +157 -19
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +5 -2
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +33 -16
- package/dist/types.d.ts +0 -93
- package/dist/types.js +0 -126
package/dist/cjs/cli/migrate.js
CHANGED
|
@@ -27,15 +27,19 @@ exports.createMigration = createMigration;
|
|
|
27
27
|
exports.migrateUp = migrateUp;
|
|
28
28
|
exports.migrateDown = migrateDown;
|
|
29
29
|
exports.migrateStatus = migrateStatus;
|
|
30
|
+
const node_crypto_1 = require("node:crypto");
|
|
30
31
|
const node_fs_1 = require("node:fs");
|
|
31
32
|
const node_path_1 = require("node:path");
|
|
32
33
|
const pg_1 = __importDefault(require("pg"));
|
|
34
|
+
const errors_js_1 = require("../errors.js");
|
|
35
|
+
const query_js_1 = require("../query.js");
|
|
33
36
|
// ---------------------------------------------------------------------------
|
|
34
37
|
// Tracking table management
|
|
35
38
|
// ---------------------------------------------------------------------------
|
|
36
39
|
const TRACKING_TABLE = '_turbine_migrations';
|
|
40
|
+
const QUOTED_TRACKING_TABLE = (0, query_js_1.quoteIdent)(TRACKING_TABLE);
|
|
37
41
|
const CREATE_TRACKING_TABLE = `
|
|
38
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
42
|
+
CREATE TABLE IF NOT EXISTS ${QUOTED_TRACKING_TABLE} (
|
|
39
43
|
id SERIAL PRIMARY KEY,
|
|
40
44
|
name TEXT NOT NULL UNIQUE,
|
|
41
45
|
checksum TEXT NOT NULL,
|
|
@@ -47,7 +51,7 @@ async function ensureTrackingTable(client) {
|
|
|
47
51
|
}
|
|
48
52
|
async function getAppliedMigrations(client) {
|
|
49
53
|
await ensureTrackingTable(client);
|
|
50
|
-
const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${
|
|
54
|
+
const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${QUOTED_TRACKING_TABLE} ORDER BY id ASC`);
|
|
51
55
|
return result.rows;
|
|
52
56
|
}
|
|
53
57
|
// ---------------------------------------------------------------------------
|
|
@@ -155,30 +159,45 @@ function parseMigrationSQL(filePath) {
|
|
|
155
159
|
return parseMigrationContent(content);
|
|
156
160
|
}
|
|
157
161
|
/**
|
|
158
|
-
*
|
|
162
|
+
* SHA-256 checksum for migration drift detection.
|
|
163
|
+
* Returns a hex-encoded hash of the file content.
|
|
159
164
|
*/
|
|
160
165
|
function checksum(content) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return Math.abs(hash).toString(36);
|
|
166
|
+
return (0, node_crypto_1.createHash)('sha256').update(content, 'utf-8').digest('hex');
|
|
167
|
+
}
|
|
168
|
+
/** Detect legacy djb2 checksums (short alphanumeric strings, pre-v0.6) */
|
|
169
|
+
function isLegacyChecksum(hash) {
|
|
170
|
+
return hash.length < 64;
|
|
167
171
|
}
|
|
168
172
|
// ---------------------------------------------------------------------------
|
|
169
173
|
// Commands
|
|
170
174
|
// ---------------------------------------------------------------------------
|
|
171
175
|
/**
|
|
172
176
|
* Create a new migration file.
|
|
177
|
+
* If `autoContent` is provided, the UP/DOWN sections are pre-populated with the given SQL.
|
|
173
178
|
*/
|
|
174
|
-
function createMigration(migrationsDir, name) {
|
|
179
|
+
function createMigration(migrationsDir, name, autoContent) {
|
|
175
180
|
(0, node_fs_1.mkdirSync)(migrationsDir, { recursive: true });
|
|
176
181
|
const now = new Date();
|
|
177
182
|
const ts = formatTimestamp(now);
|
|
178
183
|
const safeName = sanitizeName(name);
|
|
179
184
|
const filename = `${ts}_${safeName}.sql`;
|
|
180
185
|
const filePath = (0, node_path_1.join)(migrationsDir, filename);
|
|
181
|
-
|
|
186
|
+
let template;
|
|
187
|
+
if (autoContent) {
|
|
188
|
+
template = `-- Migration: ${name} (auto-generated from schema diff)
|
|
189
|
+
-- Created: ${now.toISOString()}
|
|
190
|
+
-- Review this file before running: npx turbine migrate up
|
|
191
|
+
|
|
192
|
+
-- UP
|
|
193
|
+
${autoContent.up}
|
|
194
|
+
|
|
195
|
+
-- DOWN
|
|
196
|
+
${autoContent.down}
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
template = `-- Migration: ${name}
|
|
182
201
|
-- Created: ${now.toISOString()}
|
|
183
202
|
|
|
184
203
|
-- UP
|
|
@@ -187,6 +206,7 @@ function createMigration(migrationsDir, name) {
|
|
|
187
206
|
-- DOWN
|
|
188
207
|
-- Write your rollback SQL here
|
|
189
208
|
`;
|
|
209
|
+
}
|
|
190
210
|
(0, node_fs_1.writeFileSync)(filePath, template, 'utf-8');
|
|
191
211
|
return {
|
|
192
212
|
filename,
|
|
@@ -201,17 +221,16 @@ function createMigration(migrationsDir, name) {
|
|
|
201
221
|
/** Fixed lock ID for Turbine migrations — prevents concurrent migrate runs */
|
|
202
222
|
const MIGRATION_LOCK_ID = 8_347_291; // arbitrary but stable
|
|
203
223
|
async function acquireLock(client) {
|
|
204
|
-
const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [
|
|
224
|
+
const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [
|
|
225
|
+
MIGRATION_LOCK_ID,
|
|
226
|
+
]);
|
|
205
227
|
return result.rows[0]?.pg_try_advisory_lock ?? false;
|
|
206
228
|
}
|
|
207
229
|
async function releaseLock(client) {
|
|
208
230
|
await client.query(`SELECT pg_advisory_unlock($1)`, [MIGRATION_LOCK_ID]);
|
|
209
231
|
}
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// Checksum validation
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
232
|
/**
|
|
214
|
-
* Validate that applied migration files have not been modified since they were run.
|
|
233
|
+
* Validate that applied migration files have not been modified or deleted since they were run.
|
|
215
234
|
* Returns an array of mismatched migrations (empty if all are clean).
|
|
216
235
|
*/
|
|
217
236
|
async function validateChecksums(client, migrationsDir) {
|
|
@@ -221,15 +240,31 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
221
240
|
const mismatches = [];
|
|
222
241
|
for (const migration of applied) {
|
|
223
242
|
const file = fileMap.get(migration.name);
|
|
224
|
-
if (!file)
|
|
225
|
-
|
|
243
|
+
if (!file) {
|
|
244
|
+
mismatches.push({
|
|
245
|
+
name: migration.name,
|
|
246
|
+
expected: migration.checksum,
|
|
247
|
+
actual: '',
|
|
248
|
+
type: 'missing',
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
226
252
|
const content = (0, node_fs_1.readFileSync)(file.path, 'utf-8');
|
|
227
253
|
const currentHash = checksum(content);
|
|
228
254
|
if (currentHash !== migration.checksum) {
|
|
255
|
+
// Auto-upgrade legacy djb2 checksums to SHA-256 without flagging as modified
|
|
256
|
+
if (isLegacyChecksum(migration.checksum)) {
|
|
257
|
+
await client.query(`UPDATE ${QUOTED_TRACKING_TABLE} SET checksum = $1 WHERE name = $2`, [
|
|
258
|
+
currentHash,
|
|
259
|
+
migration.name,
|
|
260
|
+
]);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
229
263
|
mismatches.push({
|
|
230
264
|
name: migration.name,
|
|
231
265
|
expected: migration.checksum,
|
|
232
266
|
actual: currentHash,
|
|
267
|
+
type: 'modified',
|
|
233
268
|
});
|
|
234
269
|
}
|
|
235
270
|
}
|
|
@@ -251,27 +286,23 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
251
286
|
// Acquire advisory lock to prevent concurrent migrations
|
|
252
287
|
const gotLock = await acquireLock(client);
|
|
253
288
|
if (!gotLock) {
|
|
254
|
-
|
|
255
|
-
applied: [],
|
|
256
|
-
errors: [{
|
|
257
|
-
file: { filename: '', path: '', name: '', timestamp: '' },
|
|
258
|
-
error: 'Could not acquire migration lock — another migration is already running',
|
|
259
|
-
}],
|
|
260
|
-
};
|
|
289
|
+
throw new errors_js_1.MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
261
290
|
}
|
|
262
291
|
try {
|
|
263
292
|
await ensureTrackingTable(client);
|
|
264
|
-
// Validate checksums of already-applied migrations
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
293
|
+
// Validate checksums of already-applied migrations (skip with --force)
|
|
294
|
+
if (!options?.force) {
|
|
295
|
+
const mismatches = await validateChecksums(client, migrationsDir);
|
|
296
|
+
if (mismatches.length > 0) {
|
|
297
|
+
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
298
|
+
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
299
|
+
const parts = [];
|
|
300
|
+
if (modified.length > 0)
|
|
301
|
+
parts.push(`modified: ${modified.map((m) => m.name).join(', ')}`);
|
|
302
|
+
if (missing.length > 0)
|
|
303
|
+
parts.push(`deleted: ${missing.map((m) => m.name).join(', ')}`);
|
|
304
|
+
throw new errors_js_1.MigrationError(`[turbine] Migration integrity check failed — ${parts.join('; ')}. Applied migrations should be immutable. Use --force to skip this check.`);
|
|
305
|
+
}
|
|
275
306
|
}
|
|
276
307
|
const applied = await getAppliedMigrations(client);
|
|
277
308
|
const appliedNames = new Set(applied.map((m) => m.name));
|
|
@@ -293,7 +324,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
293
324
|
try {
|
|
294
325
|
await client.query('BEGIN');
|
|
295
326
|
await client.query(up);
|
|
296
|
-
await client.query(`INSERT INTO ${
|
|
327
|
+
await client.query(`INSERT INTO ${QUOTED_TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
|
|
297
328
|
await client.query('COMMIT');
|
|
298
329
|
results.push(file);
|
|
299
330
|
}
|
|
@@ -329,13 +360,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
329
360
|
try {
|
|
330
361
|
const gotLock = await acquireLock(client);
|
|
331
362
|
if (!gotLock) {
|
|
332
|
-
|
|
333
|
-
rolledBack: [],
|
|
334
|
-
errors: [{
|
|
335
|
-
file: { filename: '', path: '', name: '', timestamp: '' },
|
|
336
|
-
error: 'Could not acquire migration lock — another migration is already running',
|
|
337
|
-
}],
|
|
338
|
-
};
|
|
363
|
+
throw new errors_js_1.MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
339
364
|
}
|
|
340
365
|
try {
|
|
341
366
|
await ensureTrackingTable(client);
|
|
@@ -353,7 +378,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
353
378
|
const file = fileMap.get(migration.name);
|
|
354
379
|
if (!file) {
|
|
355
380
|
errors.push({
|
|
356
|
-
file: { filename: migration.name
|
|
381
|
+
file: { filename: `${migration.name}.sql`, path: '', name: migration.name, timestamp: '' },
|
|
357
382
|
error: `Migration file not found for "${migration.name}"`,
|
|
358
383
|
});
|
|
359
384
|
continue;
|
|
@@ -366,7 +391,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
366
391
|
try {
|
|
367
392
|
await client.query('BEGIN');
|
|
368
393
|
await client.query(down);
|
|
369
|
-
await client.query(`DELETE FROM ${
|
|
394
|
+
await client.query(`DELETE FROM ${QUOTED_TRACKING_TABLE} WHERE name = $1`, [migration.name]);
|
|
370
395
|
await client.query('COMMIT');
|
|
371
396
|
results.push(file);
|
|
372
397
|
}
|
package/dist/cjs/cli/ui.js
CHANGED
|
@@ -24,9 +24,7 @@ exports.redactUrl = redactUrl;
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// ANSI escape codes
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
|
-
const isColorSupported = process.env
|
|
28
|
-
process.env['TERM'] !== 'dumb' &&
|
|
29
|
-
(process.stdout.isTTY ?? false);
|
|
27
|
+
const isColorSupported = process.env.NO_COLOR == null && process.env.TERM !== 'dumb' && (process.stdout.isTTY ?? false);
|
|
30
28
|
function code(open, close) {
|
|
31
29
|
if (!isColorSupported)
|
|
32
30
|
return (s) => s;
|
|
@@ -113,9 +111,7 @@ function table(headers, rows) {
|
|
|
113
111
|
return ` ${(0, exports.bold)(h)}${' '.repeat(Math.max(0, w - stripAnsi(h).length))} `;
|
|
114
112
|
})
|
|
115
113
|
.join((0, exports.dim)(exports.symbols.vertLine));
|
|
116
|
-
const separator = colWidths
|
|
117
|
-
.map((w) => exports.symbols.line.repeat(w + 2))
|
|
118
|
-
.join((0, exports.dim)(exports.symbols.line));
|
|
114
|
+
const separator = colWidths.map((w) => exports.symbols.line.repeat(w + 2)).join((0, exports.dim)(exports.symbols.line));
|
|
119
115
|
const bodyLines = rows.map((row) => row
|
|
120
116
|
.map((cell, i) => {
|
|
121
117
|
const w = colWidths[i];
|
|
@@ -195,7 +191,7 @@ function info(msg) {
|
|
|
195
191
|
console.log(` ${(0, exports.blue)(exports.symbols.info)} ${msg}`);
|
|
196
192
|
}
|
|
197
193
|
function label(key, value) {
|
|
198
|
-
console.log(` ${(0, exports.dim)(key
|
|
194
|
+
console.log(` ${(0, exports.dim)(`${key}:`)} ${value}`);
|
|
199
195
|
}
|
|
200
196
|
function newline() {
|
|
201
197
|
console.log('');
|
|
@@ -209,7 +205,7 @@ function divider() {
|
|
|
209
205
|
// ---------------------------------------------------------------------------
|
|
210
206
|
function banner() {
|
|
211
207
|
console.log('');
|
|
212
|
-
console.log(` ${(0, exports.bold)((0, exports.cyan)('turbine'))}
|
|
208
|
+
console.log(` ${(0, exports.bold)((0, exports.cyan)('turbine-orm'))}`);
|
|
213
209
|
console.log(` ${(0, exports.dim)('TypeScript ORM with json_agg nested queries')}`);
|
|
214
210
|
console.log('');
|
|
215
211
|
}
|
|
@@ -226,7 +222,7 @@ function elapsed(startMs) {
|
|
|
226
222
|
// Strip ANSI codes (for width calculation)
|
|
227
223
|
// ---------------------------------------------------------------------------
|
|
228
224
|
function stripAnsi(s) {
|
|
229
|
-
//
|
|
225
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes require control characters
|
|
230
226
|
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
231
227
|
}
|
|
232
228
|
// ---------------------------------------------------------------------------
|
package/dist/cjs/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* turbine-orm — TurbineClient
|
|
4
4
|
*
|
|
5
5
|
* The main entry point for the Turbine TypeScript SDK.
|
|
6
6
|
* Manages connection pooling and provides typed table accessors.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* const user = await db.users.findUnique({ where: { id: 1 } });
|
|
18
18
|
*
|
|
19
19
|
* // With base client (dynamic):
|
|
20
|
-
* import { TurbineClient } from '
|
|
20
|
+
* import { TurbineClient } from 'turbine-orm';
|
|
21
21
|
* const db = new TurbineClient({ connectionString: '...' }, schema);
|
|
22
22
|
* const users = db.table<User>('users');
|
|
23
23
|
* ```
|
|
@@ -28,22 +28,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.TurbineClient = exports.TransactionClient = void 0;
|
|
30
30
|
const pg_1 = __importDefault(require("pg"));
|
|
31
|
-
const
|
|
31
|
+
const errors_js_1 = require("./errors.js");
|
|
32
32
|
const pipeline_js_1 = require("./pipeline.js");
|
|
33
|
-
|
|
34
|
-
* Parse int8 (bigint, OID 20) as JavaScript number instead of string.
|
|
35
|
-
* Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
|
|
36
|
-
*
|
|
37
|
-
* NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
|
|
38
|
-
* to returning the raw string to avoid precision loss. The generated TypeScript
|
|
39
|
-
* type maps int8/bigint to `number`, which is correct for the vast majority of
|
|
40
|
-
* use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
|
|
41
|
-
* bigint column, the runtime return type will be `string` for those rows.
|
|
42
|
-
*/
|
|
43
|
-
pg_1.default.types.setTypeParser(20, (val) => {
|
|
44
|
-
const n = Number(val);
|
|
45
|
-
return Number.isSafeInteger(n) ? n : val; // fall back to string for huge values
|
|
46
|
-
});
|
|
33
|
+
const query_js_1 = require("./query.js");
|
|
47
34
|
/** Maps isolation level names to SQL */
|
|
48
35
|
const ISOLATION_LEVELS = {
|
|
49
36
|
ReadUncommitted: 'READ UNCOMMITTED',
|
|
@@ -125,20 +112,36 @@ class TransactionClient {
|
|
|
125
112
|
sql += `$${i + 1}`;
|
|
126
113
|
}
|
|
127
114
|
});
|
|
128
|
-
|
|
129
|
-
|
|
115
|
+
try {
|
|
116
|
+
const result = await this.client.query(sql, values);
|
|
117
|
+
return result.rows;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
121
|
+
}
|
|
130
122
|
}
|
|
131
123
|
/**
|
|
132
124
|
* Create a pool-like wrapper around the transaction client.
|
|
133
125
|
* This allows QueryInterface to work with the transaction connection
|
|
134
126
|
* without knowing it's in a transaction.
|
|
127
|
+
*
|
|
128
|
+
* pg driver errors thrown by queries are translated into typed Turbine
|
|
129
|
+
* errors via wrapPgError so transaction-scoped queries surface the same
|
|
130
|
+
* typed errors as pool-scoped queries.
|
|
135
131
|
*/
|
|
136
132
|
createTxPool() {
|
|
137
133
|
const client = this.client;
|
|
138
134
|
// Return a minimal pool-compatible object that routes queries
|
|
139
135
|
// through the transaction client
|
|
140
136
|
return {
|
|
141
|
-
query: (text, values) =>
|
|
137
|
+
query: async (text, values) => {
|
|
138
|
+
try {
|
|
139
|
+
return await client.query(text, values);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
142
145
|
connect: () => Promise.resolve(client),
|
|
143
146
|
};
|
|
144
147
|
}
|
|
@@ -152,38 +155,81 @@ class TurbineClient {
|
|
|
152
155
|
pool;
|
|
153
156
|
/** The schema metadata this client was built from */
|
|
154
157
|
schema;
|
|
158
|
+
static int8ParserRegistered = false;
|
|
155
159
|
logging;
|
|
156
160
|
tableCache = new Map();
|
|
157
161
|
middlewares = [];
|
|
158
162
|
queryOptions;
|
|
163
|
+
/** True when Turbine created the pool and is responsible for tearing it down */
|
|
164
|
+
ownsPool = true;
|
|
159
165
|
constructor(config = {}, schema) {
|
|
166
|
+
/**
|
|
167
|
+
* Parse int8 (bigint, OID 20) as JavaScript number instead of string.
|
|
168
|
+
* Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
|
|
169
|
+
*
|
|
170
|
+
* NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
|
|
171
|
+
* to returning the raw string to avoid precision loss. The generated TypeScript
|
|
172
|
+
* type maps int8/bigint to `number`, which is correct for the vast majority of
|
|
173
|
+
* use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
|
|
174
|
+
* bigint column, the runtime return type will be `string` for those rows.
|
|
175
|
+
*
|
|
176
|
+
* NOTE: We intentionally do NOT register a parser for numeric (OID 1700).
|
|
177
|
+
* Postgres numeric is arbitrary-precision, so the default pg driver behavior
|
|
178
|
+
* of returning a string is correct and matches the generated TypeScript type
|
|
179
|
+
* (numeric → string). Users who want number can cast explicitly in SQL.
|
|
180
|
+
*/
|
|
181
|
+
// Only register the int8 parser when we own the pg driver. External
|
|
182
|
+
// pools (Neon HTTP, Vercel Postgres) may ship their own pg-types fork
|
|
183
|
+
// and rely on their own parser configuration — don't mutate global state
|
|
184
|
+
// we don't own.
|
|
185
|
+
if (!config.pool && !TurbineClient.int8ParserRegistered) {
|
|
186
|
+
pg_1.default.types.setTypeParser(20, (val) => {
|
|
187
|
+
const n = Number(val);
|
|
188
|
+
return Number.isSafeInteger(n) ? n : val;
|
|
189
|
+
});
|
|
190
|
+
TurbineClient.int8ParserRegistered = true;
|
|
191
|
+
}
|
|
160
192
|
this.logging = config.logging ?? false;
|
|
161
193
|
this.schema = schema;
|
|
162
194
|
this.queryOptions = {
|
|
163
195
|
defaultLimit: config.defaultLimit,
|
|
164
196
|
warnOnUnlimited: config.warnOnUnlimited,
|
|
165
197
|
};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
198
|
+
if (config.pool) {
|
|
199
|
+
// External pool — use directly. Turbine doesn't manage its lifecycle.
|
|
200
|
+
this.pool = config.pool;
|
|
201
|
+
this.ownsPool = false;
|
|
202
|
+
if (this.logging) {
|
|
203
|
+
console.log(`[turbine] Using external pool — ${Object.keys(schema.tables).length} tables`);
|
|
204
|
+
}
|
|
173
205
|
}
|
|
174
206
|
else {
|
|
175
|
-
poolConfig
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
207
|
+
const poolConfig = {
|
|
208
|
+
max: config.poolSize ?? 10,
|
|
209
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
|
|
210
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
211
|
+
};
|
|
212
|
+
if (config.connectionString) {
|
|
213
|
+
poolConfig.connectionString = config.connectionString;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
poolConfig.host = config.host ?? 'localhost';
|
|
217
|
+
poolConfig.port = config.port ?? 5432;
|
|
218
|
+
poolConfig.database = config.database ?? 'postgres';
|
|
219
|
+
poolConfig.user = config.user ?? 'postgres';
|
|
220
|
+
poolConfig.password = config.password;
|
|
221
|
+
}
|
|
222
|
+
if (config.ssl !== undefined) {
|
|
223
|
+
poolConfig.ssl = config.ssl;
|
|
224
|
+
}
|
|
225
|
+
this.pool = new pg_1.default.Pool(poolConfig);
|
|
226
|
+
this.ownsPool = true;
|
|
227
|
+
this.pool.on('error', (err) => {
|
|
228
|
+
console.error('[turbine] Unexpected pool error:', err.message);
|
|
229
|
+
});
|
|
230
|
+
if (this.logging) {
|
|
231
|
+
console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
|
|
232
|
+
}
|
|
187
233
|
}
|
|
188
234
|
// Auto-create typed table accessors for all tables in the schema
|
|
189
235
|
for (const tableName of Object.keys(schema.tables)) {
|
|
@@ -290,8 +336,13 @@ class TurbineClient {
|
|
|
290
336
|
if (this.logging) {
|
|
291
337
|
console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
|
|
292
338
|
}
|
|
293
|
-
|
|
294
|
-
|
|
339
|
+
try {
|
|
340
|
+
const result = await this.pool.query(sql, values);
|
|
341
|
+
return result.rows;
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
345
|
+
}
|
|
295
346
|
}
|
|
296
347
|
// -------------------------------------------------------------------------
|
|
297
348
|
// Transaction support (raw — legacy)
|
|
@@ -375,7 +426,7 @@ class TurbineClient {
|
|
|
375
426
|
let timer;
|
|
376
427
|
const timeoutPromise = new Promise((_, reject) => {
|
|
377
428
|
timer = setTimeout(() => {
|
|
378
|
-
reject(new
|
|
429
|
+
reject(new errors_js_1.TimeoutError(timeout, 'Transaction'));
|
|
379
430
|
}, timeout);
|
|
380
431
|
});
|
|
381
432
|
try {
|
|
@@ -426,8 +477,17 @@ class TurbineClient {
|
|
|
426
477
|
}
|
|
427
478
|
/**
|
|
428
479
|
* Gracefully shut down the connection pool.
|
|
480
|
+
*
|
|
481
|
+
* If Turbine was given an external pool via `TurbineConfig.pool`, this
|
|
482
|
+
* method is a no-op — the caller is responsible for the pool's lifecycle.
|
|
429
483
|
*/
|
|
430
484
|
async disconnect() {
|
|
485
|
+
if (!this.ownsPool) {
|
|
486
|
+
if (this.logging) {
|
|
487
|
+
console.log('[turbine] disconnect() skipped — external pool is not owned by Turbine');
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
431
491
|
await this.pool.end();
|
|
432
492
|
if (this.logging) {
|
|
433
493
|
console.log('[turbine] Pool disconnected');
|
|
@@ -437,12 +497,15 @@ class TurbineClient {
|
|
|
437
497
|
async end() {
|
|
438
498
|
return this.disconnect();
|
|
439
499
|
}
|
|
440
|
-
/**
|
|
500
|
+
/**
|
|
501
|
+
* Pool statistics for monitoring. Returns zeros for pools that don't
|
|
502
|
+
* expose connection counts (e.g., stateless HTTP drivers like Neon).
|
|
503
|
+
*/
|
|
441
504
|
get stats() {
|
|
442
505
|
return {
|
|
443
|
-
totalCount: this.pool.totalCount,
|
|
444
|
-
idleCount: this.pool.idleCount,
|
|
445
|
-
waitingCount: this.pool.waitingCount,
|
|
506
|
+
totalCount: this.pool.totalCount ?? 0,
|
|
507
|
+
idleCount: this.pool.idleCount ?? 0,
|
|
508
|
+
waitingCount: this.pool.waitingCount ?? 0,
|
|
446
509
|
};
|
|
447
510
|
}
|
|
448
511
|
}
|