turbine-orm 0.5.0 → 0.7.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/README.md +292 -26
- package/dist/cjs/cli/config.js +5 -15
- package/dist/cjs/cli/index.js +311 -43
- package/dist/cjs/cli/loader.js +129 -0
- package/dist/cjs/cli/migrate.js +96 -47
- package/dist/cjs/cli/ui.js +5 -9
- package/dist/cjs/client.js +158 -49
- package/dist/cjs/errors.js +424 -0
- package/dist/cjs/generate.js +145 -14
- package/dist/cjs/index.js +43 -20
- package/dist/cjs/introspect.js +3 -5
- package/dist/cjs/pipeline.js +9 -2
- package/dist/cjs/query.js +544 -115
- package/dist/cjs/schema-builder.js +150 -30
- package/dist/cjs/schema-sql.js +241 -37
- package/dist/cjs/schema.js +5 -2
- package/dist/cjs/serverless.js +88 -176
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +316 -48
- package/dist/cli/loader.d.ts +45 -0
- package/dist/cli/loader.js +91 -0
- package/dist/cli/migrate.d.ts +13 -2
- package/dist/cli/migrate.js +97 -48
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +92 -4
- package/dist/client.js +158 -49
- package/dist/errors.d.ts +225 -0
- package/dist/errors.js +405 -0
- package/dist/generate.d.ts +7 -1
- package/dist/generate.js +148 -18
- 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 +374 -38
- package/dist/query.js +545 -116
- package/dist/schema-builder.d.ts +38 -5
- package/dist/schema-builder.js +150 -31
- package/dist/schema-sql.d.ts +7 -3
- package/dist/schema-sql.js +241 -37
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +5 -2
- package/dist/serverless.d.ts +92 -139
- package/dist/serverless.js +87 -173
- package/package.json +33 -16
- package/dist/types.d.ts +0 -93
- package/dist/types.js +0 -126
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm CLI — TypeScript loader registration
|
|
3
|
+
*
|
|
4
|
+
* The CLI loads user-supplied config and schema files via dynamic `import()`.
|
|
5
|
+
* Plain Node has no built-in `.ts` loader, so importing `turbine.config.ts`
|
|
6
|
+
* blows up with `ERR_UNKNOWN_FILE_EXTENSION` unless we register a TypeScript
|
|
7
|
+
* loader first.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* 1. If the file we're about to import ends in `.ts` / `.mts` / `.cts`,
|
|
11
|
+
* probe whether `tsx/esm` is resolvable from the user's CWD.
|
|
12
|
+
* 2. If yes, call `module.register('tsx/esm', ...)` ONCE per process.
|
|
13
|
+
* 3. If no, surface an actionable error telling the user to install `tsx`.
|
|
14
|
+
*
|
|
15
|
+
* `tsx` is intentionally NOT a runtime dependency — many projects already
|
|
16
|
+
* have it, and adding a heavy dev tool to a 1-dependency ORM would be silly.
|
|
17
|
+
*/
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { pathToFileURL } from 'node:url';
|
|
20
|
+
/**
|
|
21
|
+
* Detect whether a config / schema file path needs the tsx ESM loader.
|
|
22
|
+
* Returns true for `.ts`, `.mts`, and `.cts` files; false for `.js`, `.mjs`,
|
|
23
|
+
* `.cjs`, `.json`, missing paths, or anything else.
|
|
24
|
+
*/
|
|
25
|
+
export function needsTsLoader(filePath) {
|
|
26
|
+
if (!filePath)
|
|
27
|
+
return false;
|
|
28
|
+
return /\.(ts|mts|cts)$/i.test(filePath);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Probe whether `tsx/esm` is resolvable from the user's current working
|
|
32
|
+
* directory. Returns true if `tsx` is installed in the user's project.
|
|
33
|
+
*
|
|
34
|
+
* Accepts an injected `resolver` so unit tests don't need a real filesystem.
|
|
35
|
+
*/
|
|
36
|
+
export function canResolveTsx(resolver) {
|
|
37
|
+
try {
|
|
38
|
+
if (resolver) {
|
|
39
|
+
resolver('tsx/esm');
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// Probe relative to the user's CWD, not Turbine's install location.
|
|
43
|
+
// This way we honour whatever `tsx` version the user has pinned.
|
|
44
|
+
const userRequire = createRequire(`${process.cwd()}/`);
|
|
45
|
+
userRequire.resolve('tsx/esm');
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let tsLoaderState = null;
|
|
53
|
+
/**
|
|
54
|
+
* Register the tsx ESM loader so subsequent dynamic imports of `.ts` files
|
|
55
|
+
* work. Safe to call multiple times — internal flag prevents double registration.
|
|
56
|
+
*
|
|
57
|
+
* Returns:
|
|
58
|
+
* - 'registered' loader was successfully registered this call
|
|
59
|
+
* - 'already' a loader was previously registered (idempotent)
|
|
60
|
+
* - 'unsupported' Node lacks `module.register()` (Node < 20.6)
|
|
61
|
+
* - 'missing' `tsx` is not installed in the user's project
|
|
62
|
+
*/
|
|
63
|
+
export async function registerTsLoader() {
|
|
64
|
+
if (tsLoaderState === 'registered' || tsLoaderState === 'already') {
|
|
65
|
+
return 'already';
|
|
66
|
+
}
|
|
67
|
+
if (!canResolveTsx()) {
|
|
68
|
+
tsLoaderState = 'missing';
|
|
69
|
+
return 'missing';
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const mod = await import('node:module');
|
|
73
|
+
const register = mod.register;
|
|
74
|
+
if (typeof register !== 'function') {
|
|
75
|
+
tsLoaderState = 'unsupported';
|
|
76
|
+
return 'unsupported';
|
|
77
|
+
}
|
|
78
|
+
register('tsx/esm', pathToFileURL(`${process.cwd()}/`));
|
|
79
|
+
tsLoaderState = 'registered';
|
|
80
|
+
return 'registered';
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
tsLoaderState = 'missing';
|
|
84
|
+
return 'missing';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Reset the loader state — used by unit tests only. */
|
|
88
|
+
export function _resetTsLoaderStateForTests() {
|
|
89
|
+
tsLoaderState = null;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=loader.js.map
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -73,19 +73,30 @@ export declare function parseMigrationSQL(filePath: string): {
|
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
75
|
* Create a new migration file.
|
|
76
|
+
* If `autoContent` is provided, the UP/DOWN sections are pre-populated with the given SQL.
|
|
76
77
|
*/
|
|
77
|
-
export declare function createMigration(migrationsDir: string, name: string
|
|
78
|
+
export declare function createMigration(migrationsDir: string, name: string, autoContent?: {
|
|
79
|
+
up: string;
|
|
80
|
+
down: string;
|
|
81
|
+
}): MigrationFile;
|
|
78
82
|
/**
|
|
79
83
|
* Apply all pending migrations (UP).
|
|
80
84
|
*
|
|
81
85
|
* Features:
|
|
82
86
|
* - Idempotent: running twice is safe (already-applied migrations are skipped)
|
|
83
87
|
* - Advisory lock: prevents concurrent migration runs
|
|
84
|
-
* - Checksum validation: detects modified migration files
|
|
88
|
+
* - Checksum validation: detects modified migration files (BLOCKING — use
|
|
89
|
+
* `allowDrift: true` to bypass when intentionally rewriting history)
|
|
85
90
|
* - Each migration runs in its own transaction
|
|
91
|
+
*
|
|
92
|
+
* Throws `MigrationError` if any applied migration has been modified or deleted
|
|
93
|
+
* on disk, listing the offending files. Pass `{ allowDrift: true }` to bypass
|
|
94
|
+
* this check (the CLI exposes this as `--allow-drift`).
|
|
86
95
|
*/
|
|
87
96
|
export declare function migrateUp(connectionString: string, migrationsDir: string, options?: {
|
|
88
97
|
step?: number;
|
|
98
|
+
allowDrift?: boolean /** @deprecated use allowDrift */;
|
|
99
|
+
force?: boolean;
|
|
89
100
|
}): Promise<{
|
|
90
101
|
applied: MigrationFile[];
|
|
91
102
|
errors: Array<{
|
package/dist/cli/migrate.js
CHANGED
|
@@ -11,15 +11,19 @@
|
|
|
11
11
|
* -- DOWN
|
|
12
12
|
* DROP TABLE users;
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
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
|
// ---------------------------------------------------------------------------
|
|
@@ -139,30 +143,45 @@ export function parseMigrationSQL(filePath) {
|
|
|
139
143
|
return parseMigrationContent(content);
|
|
140
144
|
}
|
|
141
145
|
/**
|
|
142
|
-
*
|
|
146
|
+
* SHA-256 checksum for migration drift detection.
|
|
147
|
+
* Returns a hex-encoded hash of the file content.
|
|
143
148
|
*/
|
|
144
149
|
function checksum(content) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
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;
|
|
151
155
|
}
|
|
152
156
|
// ---------------------------------------------------------------------------
|
|
153
157
|
// Commands
|
|
154
158
|
// ---------------------------------------------------------------------------
|
|
155
159
|
/**
|
|
156
160
|
* Create a new migration file.
|
|
161
|
+
* If `autoContent` is provided, the UP/DOWN sections are pre-populated with the given SQL.
|
|
157
162
|
*/
|
|
158
|
-
export function createMigration(migrationsDir, name) {
|
|
163
|
+
export function createMigration(migrationsDir, name, autoContent) {
|
|
159
164
|
mkdirSync(migrationsDir, { recursive: true });
|
|
160
165
|
const now = new Date();
|
|
161
166
|
const ts = formatTimestamp(now);
|
|
162
167
|
const safeName = sanitizeName(name);
|
|
163
168
|
const filename = `${ts}_${safeName}.sql`;
|
|
164
169
|
const filePath = join(migrationsDir, filename);
|
|
165
|
-
|
|
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
|
|
175
|
+
|
|
176
|
+
-- UP
|
|
177
|
+
${autoContent.up}
|
|
178
|
+
|
|
179
|
+
-- DOWN
|
|
180
|
+
${autoContent.down}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
template = `-- Migration: ${name}
|
|
166
185
|
-- Created: ${now.toISOString()}
|
|
167
186
|
|
|
168
187
|
-- UP
|
|
@@ -171,6 +190,7 @@ export function createMigration(migrationsDir, name) {
|
|
|
171
190
|
-- DOWN
|
|
172
191
|
-- Write your rollback SQL here
|
|
173
192
|
`;
|
|
193
|
+
}
|
|
174
194
|
writeFileSync(filePath, template, 'utf-8');
|
|
175
195
|
return {
|
|
176
196
|
filename,
|
|
@@ -185,17 +205,16 @@ export function createMigration(migrationsDir, name) {
|
|
|
185
205
|
/** Fixed lock ID for Turbine migrations — prevents concurrent migrate runs */
|
|
186
206
|
const MIGRATION_LOCK_ID = 8_347_291; // arbitrary but stable
|
|
187
207
|
async function acquireLock(client) {
|
|
188
|
-
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
|
+
]);
|
|
189
211
|
return result.rows[0]?.pg_try_advisory_lock ?? false;
|
|
190
212
|
}
|
|
191
213
|
async function releaseLock(client) {
|
|
192
214
|
await client.query(`SELECT pg_advisory_unlock($1)`, [MIGRATION_LOCK_ID]);
|
|
193
215
|
}
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
// Checksum validation
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
216
|
/**
|
|
198
|
-
* 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.
|
|
199
218
|
* Returns an array of mismatched migrations (empty if all are clean).
|
|
200
219
|
*/
|
|
201
220
|
async function validateChecksums(client, migrationsDir) {
|
|
@@ -205,15 +224,31 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
205
224
|
const mismatches = [];
|
|
206
225
|
for (const migration of applied) {
|
|
207
226
|
const file = fileMap.get(migration.name);
|
|
208
|
-
if (!file)
|
|
209
|
-
|
|
227
|
+
if (!file) {
|
|
228
|
+
mismatches.push({
|
|
229
|
+
name: migration.name,
|
|
230
|
+
expected: migration.checksum,
|
|
231
|
+
actual: '',
|
|
232
|
+
type: 'missing',
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
210
236
|
const content = readFileSync(file.path, 'utf-8');
|
|
211
237
|
const currentHash = checksum(content);
|
|
212
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
|
+
}
|
|
213
247
|
mismatches.push({
|
|
214
248
|
name: migration.name,
|
|
215
249
|
expected: migration.checksum,
|
|
216
250
|
actual: currentHash,
|
|
251
|
+
type: 'modified',
|
|
217
252
|
});
|
|
218
253
|
}
|
|
219
254
|
}
|
|
@@ -225,37 +260,57 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
225
260
|
* Features:
|
|
226
261
|
* - Idempotent: running twice is safe (already-applied migrations are skipped)
|
|
227
262
|
* - Advisory lock: prevents concurrent migration runs
|
|
228
|
-
* - Checksum validation: detects modified migration files
|
|
263
|
+
* - Checksum validation: detects modified migration files (BLOCKING — use
|
|
264
|
+
* `allowDrift: true` to bypass when intentionally rewriting history)
|
|
229
265
|
* - Each migration runs in its own transaction
|
|
266
|
+
*
|
|
267
|
+
* Throws `MigrationError` if any applied migration has been modified or deleted
|
|
268
|
+
* on disk, listing the offending files. Pass `{ allowDrift: true }` to bypass
|
|
269
|
+
* this check (the CLI exposes this as `--allow-drift`).
|
|
230
270
|
*/
|
|
231
271
|
export async function migrateUp(connectionString, migrationsDir, options) {
|
|
232
272
|
const client = new pg.Client({ connectionString });
|
|
233
273
|
await client.connect();
|
|
274
|
+
// Treat `force` as an alias for `allowDrift` for backwards compatibility.
|
|
275
|
+
const allowDrift = options?.allowDrift === true || options?.force === true;
|
|
234
276
|
try {
|
|
235
277
|
// Acquire advisory lock to prevent concurrent migrations
|
|
236
278
|
const gotLock = await acquireLock(client);
|
|
237
279
|
if (!gotLock) {
|
|
238
|
-
|
|
239
|
-
applied: [],
|
|
240
|
-
errors: [{
|
|
241
|
-
file: { filename: '', path: '', name: '', timestamp: '' },
|
|
242
|
-
error: 'Could not acquire migration lock — another migration is already running',
|
|
243
|
-
}],
|
|
244
|
-
};
|
|
280
|
+
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
245
281
|
}
|
|
246
282
|
try {
|
|
247
283
|
await ensureTrackingTable(client);
|
|
248
|
-
// Validate checksums of already-applied migrations
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
284
|
+
// Validate checksums of already-applied migrations.
|
|
285
|
+
// Drift = an APPLIED migration's on-disk file has changed (or been deleted)
|
|
286
|
+
// since it was run. Either situation means the database state and the
|
|
287
|
+
// migration history no longer agree, so we BLOCK the run by default.
|
|
288
|
+
// Users can pass `allowDrift: true` (CLI: `--allow-drift`) to force past
|
|
289
|
+
// the block when they are intentionally rewriting history.
|
|
290
|
+
if (!allowDrift) {
|
|
291
|
+
const mismatches = await validateChecksums(client, migrationsDir);
|
|
292
|
+
if (mismatches.length > 0) {
|
|
293
|
+
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
294
|
+
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
295
|
+
const lines = [
|
|
296
|
+
'[turbine] Migration drift detected — refusing to apply pending migrations.',
|
|
297
|
+
'',
|
|
298
|
+
'Applied migrations should be immutable. The following files no longer match their applied state:',
|
|
299
|
+
'',
|
|
300
|
+
];
|
|
301
|
+
for (const m of modified) {
|
|
302
|
+
lines.push(` - ${m.name}.sql (modified on disk)`);
|
|
303
|
+
}
|
|
304
|
+
for (const m of missing) {
|
|
305
|
+
lines.push(` - ${m.name}.sql (deleted from disk)`);
|
|
306
|
+
}
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push('Fix one of these:');
|
|
309
|
+
lines.push(' 1. Restore the file(s) to their original content, OR');
|
|
310
|
+
lines.push(' 2. Roll back the affected migrations with `npx turbine migrate down`, OR');
|
|
311
|
+
lines.push(' 3. Pass `--allow-drift` to bypass this check (advanced — make sure you know what you are doing).');
|
|
312
|
+
throw new MigrationError(lines.join('\n'));
|
|
313
|
+
}
|
|
259
314
|
}
|
|
260
315
|
const applied = await getAppliedMigrations(client);
|
|
261
316
|
const appliedNames = new Set(applied.map((m) => m.name));
|
|
@@ -277,7 +332,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
277
332
|
try {
|
|
278
333
|
await client.query('BEGIN');
|
|
279
334
|
await client.query(up);
|
|
280
|
-
await client.query(`INSERT INTO ${
|
|
335
|
+
await client.query(`INSERT INTO ${QUOTED_TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
|
|
281
336
|
await client.query('COMMIT');
|
|
282
337
|
results.push(file);
|
|
283
338
|
}
|
|
@@ -313,13 +368,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
313
368
|
try {
|
|
314
369
|
const gotLock = await acquireLock(client);
|
|
315
370
|
if (!gotLock) {
|
|
316
|
-
|
|
317
|
-
rolledBack: [],
|
|
318
|
-
errors: [{
|
|
319
|
-
file: { filename: '', path: '', name: '', timestamp: '' },
|
|
320
|
-
error: 'Could not acquire migration lock — another migration is already running',
|
|
321
|
-
}],
|
|
322
|
-
};
|
|
371
|
+
throw new MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
|
|
323
372
|
}
|
|
324
373
|
try {
|
|
325
374
|
await ensureTrackingTable(client);
|
|
@@ -337,7 +386,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
337
386
|
const file = fileMap.get(migration.name);
|
|
338
387
|
if (!file) {
|
|
339
388
|
errors.push({
|
|
340
|
-
file: { filename: migration.name
|
|
389
|
+
file: { filename: `${migration.name}.sql`, path: '', name: migration.name, timestamp: '' },
|
|
341
390
|
error: `Migration file not found for "${migration.name}"`,
|
|
342
391
|
});
|
|
343
392
|
continue;
|
|
@@ -350,7 +399,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
350
399
|
try {
|
|
351
400
|
await client.query('BEGIN');
|
|
352
401
|
await client.query(down);
|
|
353
|
-
await client.query(`DELETE FROM ${
|
|
402
|
+
await client.query(`DELETE FROM ${QUOTED_TRACKING_TABLE} WHERE name = $1`, [migration.name]);
|
|
354
403
|
await client.query('COMMIT');
|
|
355
404
|
results.push(file);
|
|
356
405
|
}
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare const symbols: {
|
|
|
34
34
|
readonly warning: "⚠" | "!";
|
|
35
35
|
readonly dot: "." | "∙";
|
|
36
36
|
readonly line: "─" | "-";
|
|
37
|
-
readonly vertLine: "
|
|
37
|
+
readonly vertLine: "|" | "│";
|
|
38
38
|
readonly topLeft: "╭" | "+";
|
|
39
39
|
readonly topRight: "+" | "╮";
|
|
40
40
|
readonly bottomLeft: "+" | "╰";
|
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('turbine'))}
|
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* turbine-orm — TurbineClient
|
|
3
3
|
*
|
|
4
4
|
* The main entry point for the Turbine TypeScript SDK.
|
|
5
5
|
* Manages connection pooling and provides typed table accessors.
|
|
@@ -16,16 +16,70 @@
|
|
|
16
16
|
* const user = await db.users.findUnique({ where: { id: 1 } });
|
|
17
17
|
*
|
|
18
18
|
* // With base client (dynamic):
|
|
19
|
-
* import { TurbineClient } from '
|
|
19
|
+
* import { TurbineClient } from 'turbine-orm';
|
|
20
20
|
* const db = new TurbineClient({ connectionString: '...' }, schema);
|
|
21
21
|
* const users = db.table<User>('users');
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
-
import {
|
|
25
|
+
import { type ErrorMessageMode } from './errors.js';
|
|
26
26
|
import { type PipelineResults } from './pipeline.js';
|
|
27
|
+
import { type DeferredQuery, QueryInterface, type QueryInterfaceOptions } from './query.js';
|
|
27
28
|
import type { SchemaMetadata } from './schema.js';
|
|
29
|
+
/**
|
|
30
|
+
* Minimal pg-compatible query result.
|
|
31
|
+
* `pg.Pool`, `@neondatabase/serverless` Pool, `@vercel/postgres` Pool and
|
|
32
|
+
* any driver speaking the node-postgres API all satisfy this shape.
|
|
33
|
+
*/
|
|
34
|
+
export interface PgCompatQueryResult<R = Record<string, unknown>> {
|
|
35
|
+
rows: R[];
|
|
36
|
+
rowCount: number | null;
|
|
37
|
+
fields?: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
dataTypeID: number;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Minimal pg-compatible client used by TurbineClient for transactions.
|
|
44
|
+
* `pg.PoolClient` satisfies this; so do Neon and Vercel's equivalents.
|
|
45
|
+
*/
|
|
46
|
+
export interface PgCompatPoolClient {
|
|
47
|
+
query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
|
|
48
|
+
release(err?: Error | boolean): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Minimal pg-compatible pool. Pass any driver that satisfies this interface
|
|
52
|
+
* via `TurbineConfig.pool` — lets Turbine run on Neon HTTP, Vercel Postgres,
|
|
53
|
+
* Cloudflare Hyperdrive, or any other serverless Postgres driver.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { Pool } from '@neondatabase/serverless';
|
|
58
|
+
* import { TurbineClient } from 'turbine-orm';
|
|
59
|
+
*
|
|
60
|
+
* const neonPool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
61
|
+
* const db = new TurbineClient({ pool: neonPool }, schema);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export interface PgCompatPool {
|
|
65
|
+
query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
|
|
66
|
+
connect(): Promise<PgCompatPoolClient>;
|
|
67
|
+
end(): Promise<void>;
|
|
68
|
+
/** Optional — pools that expose stats (pg.Pool does; Neon HTTP does not) */
|
|
69
|
+
readonly totalCount?: number;
|
|
70
|
+
readonly idleCount?: number;
|
|
71
|
+
readonly waitingCount?: number;
|
|
72
|
+
/** Optional — pg.Pool supports 'error' event; HTTP drivers typically do not */
|
|
73
|
+
on?(event: 'error', listener: (err: Error) => void): this;
|
|
74
|
+
}
|
|
28
75
|
export interface TurbineConfig {
|
|
76
|
+
/**
|
|
77
|
+
* An external pg-compatible pool. Use this to plug in serverless drivers
|
|
78
|
+
* like `@neondatabase/serverless`, `@vercel/postgres`, or any other pg-API
|
|
79
|
+
* compatible pool. When provided, all connection-string fields are ignored
|
|
80
|
+
* and Turbine will NOT create its own pg.Pool.
|
|
81
|
+
*/
|
|
82
|
+
pool?: PgCompatPool;
|
|
29
83
|
/** Postgres connection string (e.g. postgres://user:pass@host:5432/db) */
|
|
30
84
|
connectionString?: string;
|
|
31
85
|
/** Host (used if connectionString is not set) */
|
|
@@ -38,6 +92,13 @@ export interface TurbineConfig {
|
|
|
38
92
|
user?: string;
|
|
39
93
|
/** Password */
|
|
40
94
|
password?: string;
|
|
95
|
+
/** SSL/TLS options for the connection (required for most cloud providers) */
|
|
96
|
+
ssl?: boolean | {
|
|
97
|
+
rejectUnauthorized?: boolean;
|
|
98
|
+
ca?: string;
|
|
99
|
+
key?: string;
|
|
100
|
+
cert?: string;
|
|
101
|
+
};
|
|
41
102
|
/** Maximum number of connections in the pool (default: 10) */
|
|
42
103
|
poolSize?: number;
|
|
43
104
|
/** Idle timeout in ms before a connection is closed (default: 30000) */
|
|
@@ -50,6 +111,20 @@ export interface TurbineConfig {
|
|
|
50
111
|
defaultLimit?: number;
|
|
51
112
|
/** Log a warning when findMany() is called without a limit (default: false) */
|
|
52
113
|
warnOnUnlimited?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Controls how `NotFoundError` (and other where-aware errors) format their
|
|
116
|
+
* messages.
|
|
117
|
+
*
|
|
118
|
+
* - `'safe'` (default): the message includes only the keys of the where
|
|
119
|
+
* clause (e.g. `where: { id, email }`). Values are redacted to avoid
|
|
120
|
+
* leaking PII into error logs (Sentry, Datadog, etc.).
|
|
121
|
+
* - `'verbose'`: the message includes the full JSON-serialized where
|
|
122
|
+
* clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
|
|
123
|
+
*
|
|
124
|
+
* The full `where` object is always available as `err.where` for
|
|
125
|
+
* programmatic access regardless of mode.
|
|
126
|
+
*/
|
|
127
|
+
errorMessages?: ErrorMessageMode;
|
|
53
128
|
}
|
|
54
129
|
/** Parameters passed to middleware functions */
|
|
55
130
|
export interface MiddlewareParams {
|
|
@@ -101,6 +176,10 @@ export declare class TransactionClient {
|
|
|
101
176
|
* Create a pool-like wrapper around the transaction client.
|
|
102
177
|
* This allows QueryInterface to work with the transaction connection
|
|
103
178
|
* without knowing it's in a transaction.
|
|
179
|
+
*
|
|
180
|
+
* pg driver errors thrown by queries are translated into typed Turbine
|
|
181
|
+
* errors via wrapPgError so transaction-scoped queries surface the same
|
|
182
|
+
* typed errors as pool-scoped queries.
|
|
104
183
|
*/
|
|
105
184
|
private createTxPool;
|
|
106
185
|
}
|
|
@@ -109,10 +188,13 @@ export declare class TurbineClient {
|
|
|
109
188
|
readonly pool: pg.Pool;
|
|
110
189
|
/** The schema metadata this client was built from */
|
|
111
190
|
readonly schema: SchemaMetadata;
|
|
191
|
+
private static int8ParserRegistered;
|
|
112
192
|
private readonly logging;
|
|
113
193
|
private readonly tableCache;
|
|
114
194
|
private readonly middlewares;
|
|
115
195
|
private readonly queryOptions;
|
|
196
|
+
/** True when Turbine created the pool and is responsible for tearing it down */
|
|
197
|
+
private readonly ownsPool;
|
|
116
198
|
constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
|
|
117
199
|
/**
|
|
118
200
|
* Register a middleware function that runs before/after every query.
|
|
@@ -209,11 +291,17 @@ export declare class TurbineClient {
|
|
|
209
291
|
connect(): Promise<void>;
|
|
210
292
|
/**
|
|
211
293
|
* Gracefully shut down the connection pool.
|
|
294
|
+
*
|
|
295
|
+
* If Turbine was given an external pool via `TurbineConfig.pool`, this
|
|
296
|
+
* method is a no-op — the caller is responsible for the pool's lifecycle.
|
|
212
297
|
*/
|
|
213
298
|
disconnect(): Promise<void>;
|
|
214
299
|
/** Alias for disconnect() */
|
|
215
300
|
end(): Promise<void>;
|
|
216
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* Pool statistics for monitoring. Returns zeros for pools that don't
|
|
303
|
+
* expose connection counts (e.g., stateless HTTP drivers like Neon).
|
|
304
|
+
*/
|
|
217
305
|
get stats(): {
|
|
218
306
|
totalCount: number;
|
|
219
307
|
idleCount: number;
|