turbine-orm 0.4.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 +243 -26
- package/dist/cjs/cli/config.js +151 -0
- package/dist/cjs/cli/index.js +1176 -0
- package/dist/cjs/cli/migrate.js +446 -0
- package/dist/cjs/cli/ui.js +233 -0
- package/dist/cjs/client.js +512 -0
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +321 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/introspect.js +287 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +78 -0
- package/dist/cjs/query.js +1891 -0
- package/dist/cjs/schema-builder.js +238 -0
- package/dist/cjs/schema-sql.js +509 -0
- package/dist/cjs/schema.js +140 -0
- package/dist/cjs/serverless.js +110 -0
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +256 -49
- package/dist/cli/migrate.d.ts +35 -6
- package/dist/cli/migrate.js +124 -76
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +87 -3
- package/dist/client.js +122 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.js +37 -11
- package/dist/index.d.ts +10 -8
- package/dist/index.js +15 -11
- package/dist/introspect.js +3 -5
- package/dist/pipeline.js +8 -1
- package/dist/query.d.ts +310 -45
- package/dist/query.js +565 -237
- package/dist/schema-builder.js +91 -23
- package/dist/schema-sql.d.ts +6 -2
- package/dist/schema-sql.js +180 -26
- package/dist/schema.js +4 -1
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +44 -21
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -126
package/dist/cli/migrate.js
CHANGED
|
@@ -11,15 +11,19 @@
|
|
|
11
11
|
* -- DOWN
|
|
12
12
|
* DROP TABLE users;
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
16
17
|
import pg from 'pg';
|
|
18
|
+
import { MigrationError } from '../errors.js';
|
|
19
|
+
import { quoteIdent } from '../query.js';
|
|
17
20
|
// ---------------------------------------------------------------------------
|
|
18
21
|
// Tracking table management
|
|
19
22
|
// ---------------------------------------------------------------------------
|
|
20
23
|
const TRACKING_TABLE = '_turbine_migrations';
|
|
24
|
+
const QUOTED_TRACKING_TABLE = quoteIdent(TRACKING_TABLE);
|
|
21
25
|
const CREATE_TRACKING_TABLE = `
|
|
22
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
26
|
+
CREATE TABLE IF NOT EXISTS ${QUOTED_TRACKING_TABLE} (
|
|
23
27
|
id SERIAL PRIMARY KEY,
|
|
24
28
|
name TEXT NOT NULL UNIQUE,
|
|
25
29
|
checksum TEXT NOT NULL,
|
|
@@ -31,7 +35,7 @@ async function ensureTrackingTable(client) {
|
|
|
31
35
|
}
|
|
32
36
|
async function getAppliedMigrations(client) {
|
|
33
37
|
await ensureTrackingTable(client);
|
|
34
|
-
const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${
|
|
38
|
+
const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${QUOTED_TRACKING_TABLE} ORDER BY id ASC`);
|
|
35
39
|
return result.rows;
|
|
36
40
|
}
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
@@ -39,10 +43,10 @@ async function getAppliedMigrations(client) {
|
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
40
44
|
/**
|
|
41
45
|
* Parse a migration filename into its components.
|
|
42
|
-
* Expected format:
|
|
46
|
+
* Expected format: YYYYMMDDHHMMSS_description.sql
|
|
43
47
|
*/
|
|
44
|
-
function parseMigrationFilename(filename) {
|
|
45
|
-
const match = filename.match(/^(\d{
|
|
48
|
+
export function parseMigrationFilename(filename) {
|
|
49
|
+
const match = filename.match(/^(\d{14})_(.+)\.sql$/);
|
|
46
50
|
if (!match)
|
|
47
51
|
return null;
|
|
48
52
|
return {
|
|
@@ -50,9 +54,39 @@ function parseMigrationFilename(filename) {
|
|
|
50
54
|
path: '', // Set by caller
|
|
51
55
|
name: filename.replace(/\.sql$/, ''),
|
|
52
56
|
timestamp: match[1],
|
|
53
|
-
sequence: match[2],
|
|
54
57
|
};
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
|
|
61
|
+
*/
|
|
62
|
+
export function sanitizeName(name) {
|
|
63
|
+
return name
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
66
|
+
.replace(/_+/g, '_')
|
|
67
|
+
.replace(/^_|_$/g, '');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generate a YYYYMMDDHHMMSS timestamp string from a Date.
|
|
71
|
+
*/
|
|
72
|
+
export function formatTimestamp(date) {
|
|
73
|
+
return [
|
|
74
|
+
date.getFullYear(),
|
|
75
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
76
|
+
String(date.getDate()).padStart(2, '0'),
|
|
77
|
+
String(date.getHours()).padStart(2, '0'),
|
|
78
|
+
String(date.getMinutes()).padStart(2, '0'),
|
|
79
|
+
String(date.getSeconds()).padStart(2, '0'),
|
|
80
|
+
].join('');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get pending migration files — those not yet applied.
|
|
84
|
+
* Returns files sorted by timestamp (ascending).
|
|
85
|
+
*/
|
|
86
|
+
export function getPendingMigrations(migrationsDir, applied) {
|
|
87
|
+
const appliedSet = new Set(applied);
|
|
88
|
+
return listMigrationFiles(migrationsDir).filter((f) => !appliedSet.has(f.name));
|
|
89
|
+
}
|
|
56
90
|
/**
|
|
57
91
|
* List all migration files in the migrations directory, sorted by name.
|
|
58
92
|
*/
|
|
@@ -73,10 +107,10 @@ export function listMigrationFiles(migrationsDir) {
|
|
|
73
107
|
return files;
|
|
74
108
|
}
|
|
75
109
|
/**
|
|
76
|
-
* Parse
|
|
110
|
+
* Parse migration content string into UP and DOWN sections.
|
|
111
|
+
* Exported for unit testing.
|
|
77
112
|
*/
|
|
78
|
-
export function
|
|
79
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
113
|
+
export function parseMigrationContent(content) {
|
|
80
114
|
const lines = content.split('\n');
|
|
81
115
|
let section = 'none';
|
|
82
116
|
const upLines = [];
|
|
@@ -102,58 +136,67 @@ export function parseMigrationSQL(filePath) {
|
|
|
102
136
|
};
|
|
103
137
|
}
|
|
104
138
|
/**
|
|
105
|
-
*
|
|
139
|
+
* Parse a migration file into UP and DOWN sections.
|
|
140
|
+
*/
|
|
141
|
+
export function parseMigrationSQL(filePath) {
|
|
142
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
143
|
+
return parseMigrationContent(content);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* SHA-256 checksum for migration drift detection.
|
|
147
|
+
* Returns a hex-encoded hash of the file content.
|
|
106
148
|
*/
|
|
107
149
|
function checksum(content) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return Math.abs(hash).toString(36);
|
|
150
|
+
return createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
151
|
+
}
|
|
152
|
+
/** Detect legacy djb2 checksums (short alphanumeric strings, pre-v0.6) */
|
|
153
|
+
function isLegacyChecksum(hash) {
|
|
154
|
+
return hash.length < 64;
|
|
114
155
|
}
|
|
115
156
|
// ---------------------------------------------------------------------------
|
|
116
157
|
// Commands
|
|
117
158
|
// ---------------------------------------------------------------------------
|
|
118
159
|
/**
|
|
119
160
|
* Create a new migration file.
|
|
161
|
+
* If `autoContent` is provided, the UP/DOWN sections are pre-populated with the given SQL.
|
|
120
162
|
*/
|
|
121
|
-
export function createMigration(migrationsDir, name) {
|
|
163
|
+
export function createMigration(migrationsDir, name, autoContent) {
|
|
122
164
|
mkdirSync(migrationsDir, { recursive: true });
|
|
123
|
-
// Get today's date as YYYYMMDD
|
|
124
165
|
const now = new Date();
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
String(now.getDate()).padStart(2, '0'),
|
|
129
|
-
].join('');
|
|
130
|
-
// Find the next sequence number for today
|
|
131
|
-
const existing = listMigrationFiles(migrationsDir);
|
|
132
|
-
const todayMigrations = existing.filter((f) => f.timestamp === datePart);
|
|
133
|
-
const nextSeq = String(todayMigrations.length + 1).padStart(3, '0');
|
|
134
|
-
// Sanitize name: lowercase, replace spaces/special chars with underscores
|
|
135
|
-
const safeName = name
|
|
136
|
-
.toLowerCase()
|
|
137
|
-
.replace(/[^a-z0-9_]/g, '_')
|
|
138
|
-
.replace(/_+/g, '_')
|
|
139
|
-
.replace(/^_|_$/g, '');
|
|
140
|
-
const filename = `${datePart}_${nextSeq}_${safeName}.sql`;
|
|
166
|
+
const ts = formatTimestamp(now);
|
|
167
|
+
const safeName = sanitizeName(name);
|
|
168
|
+
const filename = `${ts}_${safeName}.sql`;
|
|
141
169
|
const filePath = join(migrationsDir, filename);
|
|
142
|
-
|
|
143
|
-
|
|
170
|
+
let template;
|
|
171
|
+
if (autoContent) {
|
|
172
|
+
template = `-- Migration: ${name} (auto-generated from schema diff)
|
|
173
|
+
-- Created: ${now.toISOString()}
|
|
174
|
+
-- Review this file before running: npx turbine migrate up
|
|
144
175
|
|
|
176
|
+
-- UP
|
|
177
|
+
${autoContent.up}
|
|
145
178
|
|
|
146
179
|
-- DOWN
|
|
147
|
-
|
|
180
|
+
${autoContent.down}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
template = `-- Migration: ${name}
|
|
185
|
+
-- Created: ${now.toISOString()}
|
|
148
186
|
|
|
187
|
+
-- UP
|
|
188
|
+
-- Write your migration SQL here
|
|
189
|
+
|
|
190
|
+
-- DOWN
|
|
191
|
+
-- Write your rollback SQL here
|
|
149
192
|
`;
|
|
193
|
+
}
|
|
150
194
|
writeFileSync(filePath, template, 'utf-8');
|
|
151
195
|
return {
|
|
152
196
|
filename,
|
|
153
197
|
path: filePath,
|
|
154
198
|
name: filename.replace(/\.sql$/, ''),
|
|
155
|
-
timestamp:
|
|
156
|
-
sequence: nextSeq,
|
|
199
|
+
timestamp: ts,
|
|
157
200
|
};
|
|
158
201
|
}
|
|
159
202
|
// ---------------------------------------------------------------------------
|
|
@@ -162,17 +205,16 @@ export function createMigration(migrationsDir, name) {
|
|
|
162
205
|
/** Fixed lock ID for Turbine migrations — prevents concurrent migrate runs */
|
|
163
206
|
const MIGRATION_LOCK_ID = 8_347_291; // arbitrary but stable
|
|
164
207
|
async function acquireLock(client) {
|
|
165
|
-
const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [
|
|
208
|
+
const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [
|
|
209
|
+
MIGRATION_LOCK_ID,
|
|
210
|
+
]);
|
|
166
211
|
return result.rows[0]?.pg_try_advisory_lock ?? false;
|
|
167
212
|
}
|
|
168
213
|
async function releaseLock(client) {
|
|
169
214
|
await client.query(`SELECT pg_advisory_unlock($1)`, [MIGRATION_LOCK_ID]);
|
|
170
215
|
}
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
// Checksum validation
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
216
|
/**
|
|
175
|
-
* Validate that applied migration files have not been modified since they were run.
|
|
217
|
+
* Validate that applied migration files have not been modified or deleted since they were run.
|
|
176
218
|
* Returns an array of mismatched migrations (empty if all are clean).
|
|
177
219
|
*/
|
|
178
220
|
async function validateChecksums(client, migrationsDir) {
|
|
@@ -182,15 +224,31 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
182
224
|
const mismatches = [];
|
|
183
225
|
for (const migration of applied) {
|
|
184
226
|
const file = fileMap.get(migration.name);
|
|
185
|
-
if (!file)
|
|
186
|
-
|
|
227
|
+
if (!file) {
|
|
228
|
+
mismatches.push({
|
|
229
|
+
name: migration.name,
|
|
230
|
+
expected: migration.checksum,
|
|
231
|
+
actual: '',
|
|
232
|
+
type: 'missing',
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
187
236
|
const content = readFileSync(file.path, 'utf-8');
|
|
188
237
|
const currentHash = checksum(content);
|
|
189
238
|
if (currentHash !== migration.checksum) {
|
|
239
|
+
// Auto-upgrade legacy djb2 checksums to SHA-256 without flagging as modified
|
|
240
|
+
if (isLegacyChecksum(migration.checksum)) {
|
|
241
|
+
await client.query(`UPDATE ${QUOTED_TRACKING_TABLE} SET checksum = $1 WHERE name = $2`, [
|
|
242
|
+
currentHash,
|
|
243
|
+
migration.name,
|
|
244
|
+
]);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
190
247
|
mismatches.push({
|
|
191
248
|
name: migration.name,
|
|
192
249
|
expected: migration.checksum,
|
|
193
250
|
actual: currentHash,
|
|
251
|
+
type: 'modified',
|
|
194
252
|
});
|
|
195
253
|
}
|
|
196
254
|
}
|
|
@@ -212,27 +270,23 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
212
270
|
// Acquire advisory lock to prevent concurrent migrations
|
|
213
271
|
const gotLock = await acquireLock(client);
|
|
214
272
|
if (!gotLock) {
|
|
215
|
-
|
|
216
|
-
applied: [],
|
|
217
|
-
errors: [{
|
|
218
|
-
file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
|
|
219
|
-
error: 'Could not acquire migration lock — another migration is already running',
|
|
220
|
-
}],
|
|
221
|
-
};
|
|
273
|
+
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
222
274
|
}
|
|
223
275
|
try {
|
|
224
276
|
await ensureTrackingTable(client);
|
|
225
|
-
// Validate checksums of already-applied migrations
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
// Validate checksums of already-applied migrations (skip with --force)
|
|
278
|
+
if (!options?.force) {
|
|
279
|
+
const mismatches = await validateChecksums(client, migrationsDir);
|
|
280
|
+
if (mismatches.length > 0) {
|
|
281
|
+
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
282
|
+
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
283
|
+
const parts = [];
|
|
284
|
+
if (modified.length > 0)
|
|
285
|
+
parts.push(`modified: ${modified.map((m) => m.name).join(', ')}`);
|
|
286
|
+
if (missing.length > 0)
|
|
287
|
+
parts.push(`deleted: ${missing.map((m) => m.name).join(', ')}`);
|
|
288
|
+
throw new MigrationError(`[turbine] Migration integrity check failed — ${parts.join('; ')}. Applied migrations should be immutable. Use --force to skip this check.`);
|
|
289
|
+
}
|
|
236
290
|
}
|
|
237
291
|
const applied = await getAppliedMigrations(client);
|
|
238
292
|
const appliedNames = new Set(applied.map((m) => m.name));
|
|
@@ -254,7 +308,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
254
308
|
try {
|
|
255
309
|
await client.query('BEGIN');
|
|
256
310
|
await client.query(up);
|
|
257
|
-
await client.query(`INSERT INTO ${
|
|
311
|
+
await client.query(`INSERT INTO ${QUOTED_TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
|
|
258
312
|
await client.query('COMMIT');
|
|
259
313
|
results.push(file);
|
|
260
314
|
}
|
|
@@ -290,13 +344,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
290
344
|
try {
|
|
291
345
|
const gotLock = await acquireLock(client);
|
|
292
346
|
if (!gotLock) {
|
|
293
|
-
|
|
294
|
-
rolledBack: [],
|
|
295
|
-
errors: [{
|
|
296
|
-
file: { filename: '', path: '', name: '', timestamp: '', sequence: '' },
|
|
297
|
-
error: 'Could not acquire migration lock — another migration is already running',
|
|
298
|
-
}],
|
|
299
|
-
};
|
|
347
|
+
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
300
348
|
}
|
|
301
349
|
try {
|
|
302
350
|
await ensureTrackingTable(client);
|
|
@@ -314,7 +362,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
314
362
|
const file = fileMap.get(migration.name);
|
|
315
363
|
if (!file) {
|
|
316
364
|
errors.push({
|
|
317
|
-
file: { filename: migration.name
|
|
365
|
+
file: { filename: `${migration.name}.sql`, path: '', name: migration.name, timestamp: '' },
|
|
318
366
|
error: `Migration file not found for "${migration.name}"`,
|
|
319
367
|
});
|
|
320
368
|
continue;
|
|
@@ -327,7 +375,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
327
375
|
try {
|
|
328
376
|
await client.query('BEGIN');
|
|
329
377
|
await client.query(down);
|
|
330
|
-
await client.query(`DELETE FROM ${
|
|
378
|
+
await client.query(`DELETE FROM ${QUOTED_TRACKING_TABLE} WHERE name = $1`, [migration.name]);
|
|
331
379
|
await client.query('COMMIT');
|
|
332
380
|
results.push(file);
|
|
333
381
|
}
|
package/dist/cli/ui.js
CHANGED
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// ANSI escape codes
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
const isColorSupported = process.env
|
|
11
|
-
process.env['TERM'] !== 'dumb' &&
|
|
12
|
-
(process.stdout.isTTY ?? false);
|
|
10
|
+
const isColorSupported = process.env.NO_COLOR == null && process.env.TERM !== 'dumb' && (process.stdout.isTTY ?? false);
|
|
13
11
|
function code(open, close) {
|
|
14
12
|
if (!isColorSupported)
|
|
15
13
|
return (s) => s;
|
|
@@ -96,9 +94,7 @@ export function table(headers, rows) {
|
|
|
96
94
|
return ` ${bold(h)}${' '.repeat(Math.max(0, w - stripAnsi(h).length))} `;
|
|
97
95
|
})
|
|
98
96
|
.join(dim(symbols.vertLine));
|
|
99
|
-
const separator = colWidths
|
|
100
|
-
.map((w) => symbols.line.repeat(w + 2))
|
|
101
|
-
.join(dim(symbols.line));
|
|
97
|
+
const separator = colWidths.map((w) => symbols.line.repeat(w + 2)).join(dim(symbols.line));
|
|
102
98
|
const bodyLines = rows.map((row) => row
|
|
103
99
|
.map((cell, i) => {
|
|
104
100
|
const w = colWidths[i];
|
|
@@ -177,7 +173,7 @@ export function info(msg) {
|
|
|
177
173
|
console.log(` ${blue(symbols.info)} ${msg}`);
|
|
178
174
|
}
|
|
179
175
|
export function label(key, value) {
|
|
180
|
-
console.log(` ${dim(key
|
|
176
|
+
console.log(` ${dim(`${key}:`)} ${value}`);
|
|
181
177
|
}
|
|
182
178
|
export function newline() {
|
|
183
179
|
console.log('');
|
|
@@ -191,7 +187,7 @@ export function divider() {
|
|
|
191
187
|
// ---------------------------------------------------------------------------
|
|
192
188
|
export function banner() {
|
|
193
189
|
console.log('');
|
|
194
|
-
console.log(` ${bold(cyan('
|
|
190
|
+
console.log(` ${bold(cyan('turbine-orm'))}`);
|
|
195
191
|
console.log(` ${dim('TypeScript ORM with json_agg nested queries')}`);
|
|
196
192
|
console.log('');
|
|
197
193
|
}
|
|
@@ -208,7 +204,7 @@ export function elapsed(startMs) {
|
|
|
208
204
|
// Strip ANSI codes (for width calculation)
|
|
209
205
|
// ---------------------------------------------------------------------------
|
|
210
206
|
export function stripAnsi(s) {
|
|
211
|
-
//
|
|
207
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes require control characters
|
|
212
208
|
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
213
209
|
}
|
|
214
210
|
// ---------------------------------------------------------------------------
|
package/dist/client.d.ts
CHANGED
|
@@ -22,10 +22,63 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
-
import { QueryInterface, type DeferredQuery } from './query.js';
|
|
26
25
|
import { type PipelineResults } from './pipeline.js';
|
|
26
|
+
import { type DeferredQuery, QueryInterface, type QueryInterfaceOptions } from './query.js';
|
|
27
27
|
import type { SchemaMetadata } from './schema.js';
|
|
28
|
+
/**
|
|
29
|
+
* Minimal pg-compatible query result.
|
|
30
|
+
* `pg.Pool`, `@neondatabase/serverless` Pool, `@vercel/postgres` Pool and
|
|
31
|
+
* any driver speaking the node-postgres API all satisfy this shape.
|
|
32
|
+
*/
|
|
33
|
+
export interface PgCompatQueryResult<R = Record<string, unknown>> {
|
|
34
|
+
rows: R[];
|
|
35
|
+
rowCount: number | null;
|
|
36
|
+
fields?: Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
dataTypeID: number;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Minimal pg-compatible client used by TurbineClient for transactions.
|
|
43
|
+
* `pg.PoolClient` satisfies this; so do Neon and Vercel's equivalents.
|
|
44
|
+
*/
|
|
45
|
+
export interface PgCompatPoolClient {
|
|
46
|
+
query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
|
|
47
|
+
release(err?: Error | boolean): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Minimal pg-compatible pool. Pass any driver that satisfies this interface
|
|
51
|
+
* via `TurbineConfig.pool` — lets Turbine run on Neon HTTP, Vercel Postgres,
|
|
52
|
+
* Cloudflare Hyperdrive, or any other serverless Postgres driver.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { Pool } from '@neondatabase/serverless';
|
|
57
|
+
* import { TurbineClient } from 'turbine-orm';
|
|
58
|
+
*
|
|
59
|
+
* const neonPool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
60
|
+
* const db = new TurbineClient({ pool: neonPool }, schema);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export interface PgCompatPool {
|
|
64
|
+
query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
|
|
65
|
+
connect(): Promise<PgCompatPoolClient>;
|
|
66
|
+
end(): Promise<void>;
|
|
67
|
+
/** Optional — pools that expose stats (pg.Pool does; Neon HTTP does not) */
|
|
68
|
+
readonly totalCount?: number;
|
|
69
|
+
readonly idleCount?: number;
|
|
70
|
+
readonly waitingCount?: number;
|
|
71
|
+
/** Optional — pg.Pool supports 'error' event; HTTP drivers typically do not */
|
|
72
|
+
on?(event: 'error', listener: (err: Error) => void): this;
|
|
73
|
+
}
|
|
28
74
|
export interface TurbineConfig {
|
|
75
|
+
/**
|
|
76
|
+
* An external pg-compatible pool. Use this to plug in serverless drivers
|
|
77
|
+
* like `@neondatabase/serverless`, `@vercel/postgres`, or any other pg-API
|
|
78
|
+
* compatible pool. When provided, all connection-string fields are ignored
|
|
79
|
+
* and Turbine will NOT create its own pg.Pool.
|
|
80
|
+
*/
|
|
81
|
+
pool?: PgCompatPool;
|
|
29
82
|
/** Postgres connection string (e.g. postgres://user:pass@host:5432/db) */
|
|
30
83
|
connectionString?: string;
|
|
31
84
|
/** Host (used if connectionString is not set) */
|
|
@@ -38,6 +91,13 @@ export interface TurbineConfig {
|
|
|
38
91
|
user?: string;
|
|
39
92
|
/** Password */
|
|
40
93
|
password?: string;
|
|
94
|
+
/** SSL/TLS options for the connection (required for most cloud providers) */
|
|
95
|
+
ssl?: boolean | {
|
|
96
|
+
rejectUnauthorized?: boolean;
|
|
97
|
+
ca?: string;
|
|
98
|
+
key?: string;
|
|
99
|
+
cert?: string;
|
|
100
|
+
};
|
|
41
101
|
/** Maximum number of connections in the pool (default: 10) */
|
|
42
102
|
poolSize?: number;
|
|
43
103
|
/** Idle timeout in ms before a connection is closed (default: 30000) */
|
|
@@ -46,6 +106,10 @@ export interface TurbineConfig {
|
|
|
46
106
|
connectionTimeoutMs?: number;
|
|
47
107
|
/** Enable query logging to console (default: false) */
|
|
48
108
|
logging?: boolean;
|
|
109
|
+
/** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
|
|
110
|
+
defaultLimit?: number;
|
|
111
|
+
/** Log a warning when findMany() is called without a limit (default: false) */
|
|
112
|
+
warnOnUnlimited?: boolean;
|
|
49
113
|
}
|
|
50
114
|
/** Parameters passed to middleware functions */
|
|
51
115
|
export interface MiddlewareParams {
|
|
@@ -75,9 +139,10 @@ export declare class TransactionClient {
|
|
|
75
139
|
private readonly client;
|
|
76
140
|
readonly schema: SchemaMetadata;
|
|
77
141
|
private readonly middlewares;
|
|
142
|
+
private readonly queryOptions?;
|
|
78
143
|
private readonly tableCache;
|
|
79
144
|
private savepointCounter;
|
|
80
|
-
constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[]);
|
|
145
|
+
constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined);
|
|
81
146
|
/**
|
|
82
147
|
* Get a QueryInterface for a table within this transaction.
|
|
83
148
|
* Uses the dedicated transaction connection instead of the pool.
|
|
@@ -96,6 +161,10 @@ export declare class TransactionClient {
|
|
|
96
161
|
* Create a pool-like wrapper around the transaction client.
|
|
97
162
|
* This allows QueryInterface to work with the transaction connection
|
|
98
163
|
* without knowing it's in a transaction.
|
|
164
|
+
*
|
|
165
|
+
* pg driver errors thrown by queries are translated into typed Turbine
|
|
166
|
+
* errors via wrapPgError so transaction-scoped queries surface the same
|
|
167
|
+
* typed errors as pool-scoped queries.
|
|
99
168
|
*/
|
|
100
169
|
private createTxPool;
|
|
101
170
|
}
|
|
@@ -104,13 +173,22 @@ export declare class TurbineClient {
|
|
|
104
173
|
readonly pool: pg.Pool;
|
|
105
174
|
/** The schema metadata this client was built from */
|
|
106
175
|
readonly schema: SchemaMetadata;
|
|
176
|
+
private static int8ParserRegistered;
|
|
107
177
|
private readonly logging;
|
|
108
178
|
private readonly tableCache;
|
|
109
179
|
private readonly middlewares;
|
|
180
|
+
private readonly queryOptions;
|
|
181
|
+
/** True when Turbine created the pool and is responsible for tearing it down */
|
|
182
|
+
private readonly ownsPool;
|
|
110
183
|
constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
|
|
111
184
|
/**
|
|
112
185
|
* Register a middleware function that runs before/after every query.
|
|
113
186
|
*
|
|
187
|
+
* Middleware can inspect and log query parameters, modify results after execution,
|
|
188
|
+
* and measure timing. Note: query SQL is generated before middleware runs, so
|
|
189
|
+
* modifying params.args in middleware will NOT affect the executed SQL.
|
|
190
|
+
* To intercept queries before SQL generation, use the raw() method instead.
|
|
191
|
+
*
|
|
114
192
|
* @example
|
|
115
193
|
* ```ts
|
|
116
194
|
* // Query timing middleware
|
|
@@ -198,11 +276,17 @@ export declare class TurbineClient {
|
|
|
198
276
|
connect(): Promise<void>;
|
|
199
277
|
/**
|
|
200
278
|
* Gracefully shut down the connection pool.
|
|
279
|
+
*
|
|
280
|
+
* If Turbine was given an external pool via `TurbineConfig.pool`, this
|
|
281
|
+
* method is a no-op — the caller is responsible for the pool's lifecycle.
|
|
201
282
|
*/
|
|
202
283
|
disconnect(): Promise<void>;
|
|
203
284
|
/** Alias for disconnect() */
|
|
204
285
|
end(): Promise<void>;
|
|
205
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* Pool statistics for monitoring. Returns zeros for pools that don't
|
|
288
|
+
* expose connection counts (e.g., stateless HTTP drivers like Neon).
|
|
289
|
+
*/
|
|
206
290
|
get stats(): {
|
|
207
291
|
totalCount: number;
|
|
208
292
|
idleCount: number;
|