turbine-orm 0.7.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 +99 -1
- package/dist/cjs/cli/index.js +72 -3
- package/dist/cjs/cli/loader.js +129 -0
- package/dist/cjs/cli/migrate.js +33 -9
- package/dist/cjs/client.js +49 -3
- package/dist/cjs/errors.js +135 -4
- package/dist/cjs/generate.js +120 -9
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/query.js +102 -6
- package/dist/cjs/schema-builder.js +57 -6
- package/dist/cjs/schema-sql.js +85 -19
- package/dist/cjs/serverless.js +8 -7
- package/dist/cli/index.js +72 -3
- package/dist/cli/loader.d.ts +45 -0
- package/dist/cli/loader.js +91 -0
- package/dist/cli/migrate.d.ts +7 -1
- package/dist/cli/migrate.js +33 -9
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +15 -0
- package/dist/client.js +50 -4
- package/dist/errors.d.ts +88 -1
- package/dist/errors.js +130 -3
- package/dist/generate.d.ts +6 -0
- package/dist/generate.js +120 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/query.d.ts +126 -11
- package/dist/query.js +102 -6
- package/dist/schema-builder.d.ts +36 -3
- package/dist/schema-builder.js +57 -6
- package/dist/schema-sql.js +85 -19
- package/dist/serverless.d.ts +8 -7
- package/dist/serverless.js +8 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -147,6 +147,30 @@ const deleted = await db.users.delete({
|
|
|
147
147
|
});
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### Atomic update operators
|
|
151
|
+
|
|
152
|
+
For race-free counter updates, pass an operator object instead of a literal. Turbine generates `col = col + $n` style SQL so concurrent updates are safe.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Atomic increment — no read-modify-write race
|
|
156
|
+
await db.posts.update({
|
|
157
|
+
where: { id: 1 },
|
|
158
|
+
data: { viewCount: { increment: 1 } },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Other supported operators on numeric columns
|
|
162
|
+
await db.posts.update({
|
|
163
|
+
where: { id: 1 },
|
|
164
|
+
data: {
|
|
165
|
+
viewCount: { increment: 5 },
|
|
166
|
+
likesCount: { decrement: 1 },
|
|
167
|
+
score: { multiply: 2 },
|
|
168
|
+
rank: { divide: 2 },
|
|
169
|
+
title: { set: 'New title' }, // explicit set, equivalent to a literal
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
150
174
|
### Transactions
|
|
151
175
|
|
|
152
176
|
```typescript
|
|
@@ -273,6 +297,73 @@ try {
|
|
|
273
297
|
|
|
274
298
|
Error codes: `TURBINE_E001` (NotFound), `TURBINE_E002` (Timeout), `TURBINE_E003` (Validation), `TURBINE_E004` (Connection), `TURBINE_E005` (Relation), `TURBINE_E006` (Migration), `TURBINE_E007` (CircularRelation).
|
|
275
299
|
|
|
300
|
+
## WHERE Operator Reference
|
|
301
|
+
|
|
302
|
+
Every operator supported by the `where` clause. Operators compose freely with `AND`, `OR`, `NOT`, and the relation filters `some` / `every` / `none`.
|
|
303
|
+
|
|
304
|
+
### Equality
|
|
305
|
+
|
|
306
|
+
| Operator | Description | Example |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| literal | Implicit equality | `where: { email: 'a@b.com' }` |
|
|
309
|
+
| `equals` | Explicit equality | `where: { email: { equals: 'a@b.com' } }` |
|
|
310
|
+
| `not` | Inequality (or `not: null` for `IS NOT NULL`) | `where: { role: { not: 'admin' } }` |
|
|
311
|
+
|
|
312
|
+
### Sets
|
|
313
|
+
|
|
314
|
+
| Operator | Description | Example |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| `in` | Match any value in the array | `where: { id: { in: [1, 2, 3] } }` |
|
|
317
|
+
| `notIn` | Match none of the values in the array | `where: { role: { notIn: ['banned', 'spam'] } }` |
|
|
318
|
+
|
|
319
|
+
### Comparison
|
|
320
|
+
|
|
321
|
+
| Operator | Description | Example |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| `gt` | Greater than | `where: { score: { gt: 100 } }` |
|
|
324
|
+
| `gte` | Greater than or equal | `where: { score: { gte: 100 } }` |
|
|
325
|
+
| `lt` | Less than | `where: { score: { lt: 100 } }` |
|
|
326
|
+
| `lte` | Less than or equal | `where: { score: { lte: 100 } }` |
|
|
327
|
+
|
|
328
|
+
### String
|
|
329
|
+
|
|
330
|
+
| Operator | Description | Example |
|
|
331
|
+
|---|---|---|
|
|
332
|
+
| `contains` | Substring match (`LIKE %v%`) | `where: { title: { contains: 'sql' } }` |
|
|
333
|
+
| `startsWith` | Prefix match (`LIKE v%`) | `where: { email: { startsWith: 'admin@' } }` |
|
|
334
|
+
| `endsWith` | Suffix match (`LIKE %v`) | `where: { email: { endsWith: '@acme.com' } }` |
|
|
335
|
+
| `mode: 'insensitive'` | Switch any string operator to `ILIKE` | `where: { title: { contains: 'SQL', mode: 'insensitive' } }` |
|
|
336
|
+
|
|
337
|
+
LIKE wildcards in user input are escaped automatically — `%`, `_`, and `\` are treated as literals.
|
|
338
|
+
|
|
339
|
+
### Relation filters
|
|
340
|
+
|
|
341
|
+
Filter parent rows by predicates against their related child rows. Available on `hasMany` and `hasOne` relations.
|
|
342
|
+
|
|
343
|
+
| Operator | Description | Example |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| `some` | At least one related row matches | `where: { posts: { some: { published: true } } }` |
|
|
346
|
+
| `every` | Every related row matches | `where: { posts: { every: { published: true } } }` |
|
|
347
|
+
| `none` | No related row matches | `where: { posts: { none: { published: false } } }` |
|
|
348
|
+
|
|
349
|
+
### Array columns
|
|
350
|
+
|
|
351
|
+
Operators for Postgres array columns (`text[]`, `int[]`, etc.).
|
|
352
|
+
|
|
353
|
+
| Operator | Description | Example |
|
|
354
|
+
|---|---|---|
|
|
355
|
+
| `has` | Array contains the given element | `where: { tags: { has: 'sql' } }` |
|
|
356
|
+
| `hasEvery` | Array contains every element in the list | `where: { tags: { hasEvery: ['sql', 'postgres'] } }` |
|
|
357
|
+
| `hasSome` | Array contains at least one element from the list | `where: { tags: { hasSome: ['sql', 'mysql'] } }` |
|
|
358
|
+
|
|
359
|
+
### Combinators
|
|
360
|
+
|
|
361
|
+
| Operator | Description | Example |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| `AND` | All sub-clauses must match | `where: { AND: [{ orgId: 1 }, { role: 'admin' }] }` |
|
|
364
|
+
| `OR` | Any sub-clause matches | `where: { OR: [{ role: 'admin' }, { role: 'owner' }] }` |
|
|
365
|
+
| `NOT` | Negate a sub-clause | `where: { NOT: { role: 'banned' } }` |
|
|
366
|
+
|
|
276
367
|
## CLI
|
|
277
368
|
|
|
278
369
|
```
|
|
@@ -492,7 +583,6 @@ All three ORMs now use single-query approaches for nested relations. Turbine use
|
|
|
492
583
|
Turbine is focused and opinionated. Here's what it doesn't do:
|
|
493
584
|
|
|
494
585
|
- **PostgreSQL only.** No MySQL, SQLite, or MSSQL. This is by design — the `json_agg` approach is PostgreSQL-specific, and going deep on one database enables the performance advantage.
|
|
495
|
-
- **No incremental updates.** Prisma's `{ count: { increment: 1 } }` syntax is not yet supported. Use raw SQL for atomic increments.
|
|
496
586
|
- **No full-text search operators.** TSVECTOR/TSQUERY are not exposed in the query builder. Use `db.raw` for full-text queries.
|
|
497
587
|
- **Large nested result sets.** `json_agg` builds the entire JSON array in PostgreSQL memory. For relations with 10K+ rows, always use `limit` in your `with` clause to cap the aggregation size.
|
|
498
588
|
- **No admin UI.** Turbine Studio is planned but not yet available.
|
|
@@ -500,6 +590,14 @@ Turbine is focused and opinionated. Here's what it doesn't do:
|
|
|
500
590
|
## Examples
|
|
501
591
|
|
|
502
592
|
- **[Next.js](./examples/nextjs/)** — Server-rendered app with nested relations, streaming, and live code demos
|
|
593
|
+
- **[Neon Edge](./examples/neon-edge/)** — Vercel Edge route handler talking to Neon over HTTP via `@neondatabase/serverless`
|
|
594
|
+
- **[Vercel Postgres](./examples/vercel-postgres/)** — Next.js app router route handler on `@vercel/postgres`
|
|
595
|
+
- **[Cloudflare Worker](./examples/cloudflare-worker/)** — Worker `fetch` handler with `pg` over Cloudflare Hyperdrive
|
|
596
|
+
- **[Supabase](./examples/supabase/)** — Standalone script over the standard `pg` driver against Supabase
|
|
597
|
+
|
|
598
|
+
## Guides
|
|
599
|
+
|
|
600
|
+
- **[Migrating from Prisma](./docs/migrate-from-prisma.md)** — API mapping table, side-by-side `findMany`, and notes on the differences
|
|
503
601
|
|
|
504
602
|
## Requirements
|
|
505
603
|
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -61,6 +61,7 @@ const generate_js_1 = require("../generate.js");
|
|
|
61
61
|
const introspect_js_1 = require("../introspect.js");
|
|
62
62
|
const schema_sql_js_1 = require("../schema-sql.js");
|
|
63
63
|
const config_js_1 = require("./config.js");
|
|
64
|
+
const loader_js_1 = require("./loader.js");
|
|
64
65
|
const migrate_js_1 = require("./migrate.js");
|
|
65
66
|
const ui_js_1 = require("./ui.js");
|
|
66
67
|
function parseArgs() {
|
|
@@ -113,6 +114,9 @@ function parseArgs() {
|
|
|
113
114
|
case '--auto':
|
|
114
115
|
result.auto = true;
|
|
115
116
|
break;
|
|
117
|
+
case '--allow-drift':
|
|
118
|
+
result.allowDrift = true;
|
|
119
|
+
break;
|
|
116
120
|
case '--force':
|
|
117
121
|
case '-f':
|
|
118
122
|
result.force = true;
|
|
@@ -135,6 +139,36 @@ function parseArgs() {
|
|
|
135
139
|
return result;
|
|
136
140
|
}
|
|
137
141
|
// ---------------------------------------------------------------------------
|
|
142
|
+
// TypeScript loader — user-facing error helper
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* Print a friendly error explaining how to install tsx, then exit.
|
|
146
|
+
* Called when we know we need to load a `.ts` file but the loader isn't available.
|
|
147
|
+
*/
|
|
148
|
+
function failMissingTsLoader(filePath, reason) {
|
|
149
|
+
(0, ui_js_1.newline)();
|
|
150
|
+
(0, ui_js_1.error)(`Cannot load TypeScript file: ${filePath}`);
|
|
151
|
+
(0, ui_js_1.newline)();
|
|
152
|
+
if (reason === 'unsupported') {
|
|
153
|
+
console.log(` ${(0, ui_js_1.dim)('Your Node.js version does not support')} ${(0, ui_js_1.cyan)('module.register()')}.`);
|
|
154
|
+
console.log(` ${(0, ui_js_1.dim)('Upgrade to Node.js')} ${(0, ui_js_1.cyan)('20.6+')} ${(0, ui_js_1.dim)('or use a')} ${(0, ui_js_1.cyan)('.js')} ${(0, ui_js_1.dim)('/')} ${(0, ui_js_1.cyan)('.mjs')} ${(0, ui_js_1.dim)('config file.')}`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log(` ${(0, ui_js_1.dim)('Loading .ts config / schema files requires')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to be installed.')}`);
|
|
158
|
+
(0, ui_js_1.newline)();
|
|
159
|
+
console.log(` ${(0, ui_js_1.dim)('Install it as a dev dependency:')}`);
|
|
160
|
+
console.log(` ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
|
|
161
|
+
console.log(` ${(0, ui_js_1.dim)('or')}`);
|
|
162
|
+
console.log(` ${(0, ui_js_1.cyan)('pnpm add -D tsx')}`);
|
|
163
|
+
console.log(` ${(0, ui_js_1.dim)('or')}`);
|
|
164
|
+
console.log(` ${(0, ui_js_1.cyan)('yarn add -D tsx')}`);
|
|
165
|
+
(0, ui_js_1.newline)();
|
|
166
|
+
console.log(` ${(0, ui_js_1.dim)('Alternatively, rename your file to')} ${(0, ui_js_1.cyan)('.js')} ${(0, ui_js_1.dim)('or')} ${(0, ui_js_1.cyan)('.mjs')}.`);
|
|
167
|
+
}
|
|
168
|
+
(0, ui_js_1.newline)();
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
138
172
|
// Helpers
|
|
139
173
|
// ---------------------------------------------------------------------------
|
|
140
174
|
function requireUrl(config) {
|
|
@@ -157,6 +191,15 @@ async function loadSchemaFile(schemaFile) {
|
|
|
157
191
|
console.log(` ${(0, ui_js_1.dim)('Create one with:')} ${(0, ui_js_1.cyan)('turbine init')}`);
|
|
158
192
|
process.exit(1);
|
|
159
193
|
}
|
|
194
|
+
// If this is a TypeScript file, ensure the tsx ESM loader is registered
|
|
195
|
+
// before we attempt the dynamic import. Without this, Node throws
|
|
196
|
+
// ERR_UNKNOWN_FILE_EXTENSION for `.ts`.
|
|
197
|
+
if ((0, loader_js_1.needsTsLoader)(absPath)) {
|
|
198
|
+
const status = await (0, loader_js_1.registerTsLoader)();
|
|
199
|
+
if (status === 'missing' || status === 'unsupported') {
|
|
200
|
+
failMissingTsLoader(schemaFile, status);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
160
203
|
try {
|
|
161
204
|
const fileUrl = (0, node_url_1.pathToFileURL)(absPath).href;
|
|
162
205
|
const mod = await Promise.resolve(`${fileUrl}`).then(s => __importStar(require(s)));
|
|
@@ -171,6 +214,11 @@ async function loadSchemaFile(schemaFile) {
|
|
|
171
214
|
(0, ui_js_1.error)(`Failed to load schema file: ${schemaFile}`);
|
|
172
215
|
if (err instanceof Error) {
|
|
173
216
|
console.log(` ${(0, ui_js_1.dim)(err.message)}`);
|
|
217
|
+
// If the error is the classic ERR_UNKNOWN_FILE_EXTENSION, give a hint.
|
|
218
|
+
if (err.message.includes('ERR_UNKNOWN_FILE_EXTENSION') || err.message.includes('Unknown file extension')) {
|
|
219
|
+
(0, ui_js_1.newline)();
|
|
220
|
+
console.log(` ${(0, ui_js_1.dim)('Hint: install')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to load .ts files:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
|
|
221
|
+
}
|
|
174
222
|
}
|
|
175
223
|
process.exit(1);
|
|
176
224
|
}
|
|
@@ -521,9 +569,10 @@ async function cmdMigrate(args, config) {
|
|
|
521
569
|
console.log(` ${(0, ui_js_1.cyan)('status')} Show migration status`);
|
|
522
570
|
(0, ui_js_1.newline)();
|
|
523
571
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
524
|
-
console.log(` ${(0, ui_js_1.cyan)('--auto')}
|
|
525
|
-
console.log(` ${(0, ui_js_1.cyan)('--step, -n')}
|
|
526
|
-
console.log(` ${(0, ui_js_1.cyan)('--dry-run')}
|
|
572
|
+
console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff`);
|
|
573
|
+
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} Number of migrations to apply/rollback`);
|
|
574
|
+
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
575
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation on ${(0, ui_js_1.cyan)('migrate up')} ${(0, ui_js_1.dim)('(advanced)')}`);
|
|
527
576
|
(0, ui_js_1.newline)();
|
|
528
577
|
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
529
578
|
console.log(` ${(0, ui_js_1.dim)('npx turbine migrate create add_users_table')}`);
|
|
@@ -639,9 +688,18 @@ async function cmdMigrateUp(args, config) {
|
|
|
639
688
|
(0, ui_js_1.newline)();
|
|
640
689
|
return;
|
|
641
690
|
}
|
|
691
|
+
// Big, loud warning when bypassing drift detection — this is a deliberately
|
|
692
|
+
// dangerous operation and the user should see it on every invocation.
|
|
693
|
+
if (args.allowDrift) {
|
|
694
|
+
(0, ui_js_1.warn)('--allow-drift is set — checksum validation is DISABLED for this run.');
|
|
695
|
+
console.log(` ${(0, ui_js_1.dim)('Applied migrations may have been modified or deleted on disk.')}`);
|
|
696
|
+
console.log(` ${(0, ui_js_1.dim)('Proceed only if you are intentionally rewriting migration history.')}`);
|
|
697
|
+
(0, ui_js_1.newline)();
|
|
698
|
+
}
|
|
642
699
|
const spinner = new ui_js_1.Spinner('Applying migrations').start();
|
|
643
700
|
const result = await (0, migrate_js_1.migrateUp)(url, config.migrationsDir, {
|
|
644
701
|
step: args.step,
|
|
702
|
+
allowDrift: args.allowDrift,
|
|
645
703
|
});
|
|
646
704
|
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
647
705
|
spinner.succeed('All migrations are up to date');
|
|
@@ -970,6 +1028,7 @@ function showMigrateHelp() {
|
|
|
970
1028
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
971
1029
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
972
1030
|
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
1031
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation ${(0, ui_js_1.dim)('(migrate up only — advanced)')}`);
|
|
973
1032
|
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
|
|
974
1033
|
(0, ui_js_1.newline)();
|
|
975
1034
|
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
@@ -1078,6 +1137,16 @@ async function main() {
|
|
|
1078
1137
|
showVersion();
|
|
1079
1138
|
return;
|
|
1080
1139
|
}
|
|
1140
|
+
// If the user has a TypeScript config file, register the tsx ESM loader
|
|
1141
|
+
// before we attempt to import it. Otherwise Node throws
|
|
1142
|
+
// ERR_UNKNOWN_FILE_EXTENSION for `.ts`.
|
|
1143
|
+
const configPath = (0, config_js_1.findConfigFile)();
|
|
1144
|
+
if ((0, loader_js_1.needsTsLoader)(configPath)) {
|
|
1145
|
+
const status = await (0, loader_js_1.registerTsLoader)();
|
|
1146
|
+
if (status === 'missing' || status === 'unsupported') {
|
|
1147
|
+
failMissingTsLoader(configPath ?? 'turbine.config.ts', status);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1081
1150
|
// Load config file
|
|
1082
1151
|
let fileConfig = {};
|
|
1083
1152
|
try {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm CLI — TypeScript loader registration
|
|
4
|
+
*
|
|
5
|
+
* The CLI loads user-supplied config and schema files via dynamic `import()`.
|
|
6
|
+
* Plain Node has no built-in `.ts` loader, so importing `turbine.config.ts`
|
|
7
|
+
* blows up with `ERR_UNKNOWN_FILE_EXTENSION` unless we register a TypeScript
|
|
8
|
+
* loader first.
|
|
9
|
+
*
|
|
10
|
+
* Strategy:
|
|
11
|
+
* 1. If the file we're about to import ends in `.ts` / `.mts` / `.cts`,
|
|
12
|
+
* probe whether `tsx/esm` is resolvable from the user's CWD.
|
|
13
|
+
* 2. If yes, call `module.register('tsx/esm', ...)` ONCE per process.
|
|
14
|
+
* 3. If no, surface an actionable error telling the user to install `tsx`.
|
|
15
|
+
*
|
|
16
|
+
* `tsx` is intentionally NOT a runtime dependency — many projects already
|
|
17
|
+
* have it, and adding a heavy dev tool to a 1-dependency ORM would be silly.
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.needsTsLoader = needsTsLoader;
|
|
54
|
+
exports.canResolveTsx = canResolveTsx;
|
|
55
|
+
exports.registerTsLoader = registerTsLoader;
|
|
56
|
+
exports._resetTsLoaderStateForTests = _resetTsLoaderStateForTests;
|
|
57
|
+
const node_module_1 = require("node:module");
|
|
58
|
+
const node_url_1 = require("node:url");
|
|
59
|
+
/**
|
|
60
|
+
* Detect whether a config / schema file path needs the tsx ESM loader.
|
|
61
|
+
* Returns true for `.ts`, `.mts`, and `.cts` files; false for `.js`, `.mjs`,
|
|
62
|
+
* `.cjs`, `.json`, missing paths, or anything else.
|
|
63
|
+
*/
|
|
64
|
+
function needsTsLoader(filePath) {
|
|
65
|
+
if (!filePath)
|
|
66
|
+
return false;
|
|
67
|
+
return /\.(ts|mts|cts)$/i.test(filePath);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Probe whether `tsx/esm` is resolvable from the user's current working
|
|
71
|
+
* directory. Returns true if `tsx` is installed in the user's project.
|
|
72
|
+
*
|
|
73
|
+
* Accepts an injected `resolver` so unit tests don't need a real filesystem.
|
|
74
|
+
*/
|
|
75
|
+
function canResolveTsx(resolver) {
|
|
76
|
+
try {
|
|
77
|
+
if (resolver) {
|
|
78
|
+
resolver('tsx/esm');
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
// Probe relative to the user's CWD, not Turbine's install location.
|
|
82
|
+
// This way we honour whatever `tsx` version the user has pinned.
|
|
83
|
+
const userRequire = (0, node_module_1.createRequire)(`${process.cwd()}/`);
|
|
84
|
+
userRequire.resolve('tsx/esm');
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let tsLoaderState = null;
|
|
92
|
+
/**
|
|
93
|
+
* Register the tsx ESM loader so subsequent dynamic imports of `.ts` files
|
|
94
|
+
* work. Safe to call multiple times — internal flag prevents double registration.
|
|
95
|
+
*
|
|
96
|
+
* Returns:
|
|
97
|
+
* - 'registered' loader was successfully registered this call
|
|
98
|
+
* - 'already' a loader was previously registered (idempotent)
|
|
99
|
+
* - 'unsupported' Node lacks `module.register()` (Node < 20.6)
|
|
100
|
+
* - 'missing' `tsx` is not installed in the user's project
|
|
101
|
+
*/
|
|
102
|
+
async function registerTsLoader() {
|
|
103
|
+
if (tsLoaderState === 'registered' || tsLoaderState === 'already') {
|
|
104
|
+
return 'already';
|
|
105
|
+
}
|
|
106
|
+
if (!canResolveTsx()) {
|
|
107
|
+
tsLoaderState = 'missing';
|
|
108
|
+
return 'missing';
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const mod = await Promise.resolve().then(() => __importStar(require('node:module')));
|
|
112
|
+
const register = mod.register;
|
|
113
|
+
if (typeof register !== 'function') {
|
|
114
|
+
tsLoaderState = 'unsupported';
|
|
115
|
+
return 'unsupported';
|
|
116
|
+
}
|
|
117
|
+
register('tsx/esm', (0, node_url_1.pathToFileURL)(`${process.cwd()}/`));
|
|
118
|
+
tsLoaderState = 'registered';
|
|
119
|
+
return 'registered';
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
tsLoaderState = 'missing';
|
|
123
|
+
return 'missing';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Reset the loader state — used by unit tests only. */
|
|
127
|
+
function _resetTsLoaderStateForTests() {
|
|
128
|
+
tsLoaderState = null;
|
|
129
|
+
}
|
package/dist/cjs/cli/migrate.js
CHANGED
|
@@ -276,12 +276,19 @@ async function validateChecksums(client, migrationsDir) {
|
|
|
276
276
|
* Features:
|
|
277
277
|
* - Idempotent: running twice is safe (already-applied migrations are skipped)
|
|
278
278
|
* - Advisory lock: prevents concurrent migration runs
|
|
279
|
-
* - Checksum validation: detects modified migration files
|
|
279
|
+
* - Checksum validation: detects modified migration files (BLOCKING — use
|
|
280
|
+
* `allowDrift: true` to bypass when intentionally rewriting history)
|
|
280
281
|
* - Each migration runs in its own transaction
|
|
282
|
+
*
|
|
283
|
+
* Throws `MigrationError` if any applied migration has been modified or deleted
|
|
284
|
+
* on disk, listing the offending files. Pass `{ allowDrift: true }` to bypass
|
|
285
|
+
* this check (the CLI exposes this as `--allow-drift`).
|
|
281
286
|
*/
|
|
282
287
|
async function migrateUp(connectionString, migrationsDir, options) {
|
|
283
288
|
const client = new pg_1.default.Client({ connectionString });
|
|
284
289
|
await client.connect();
|
|
290
|
+
// Treat `force` as an alias for `allowDrift` for backwards compatibility.
|
|
291
|
+
const allowDrift = options?.allowDrift === true || options?.force === true;
|
|
285
292
|
try {
|
|
286
293
|
// Acquire advisory lock to prevent concurrent migrations
|
|
287
294
|
const gotLock = await acquireLock(client);
|
|
@@ -290,18 +297,35 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
290
297
|
}
|
|
291
298
|
try {
|
|
292
299
|
await ensureTrackingTable(client);
|
|
293
|
-
// Validate checksums of already-applied migrations
|
|
294
|
-
|
|
300
|
+
// Validate checksums of already-applied migrations.
|
|
301
|
+
// Drift = an APPLIED migration's on-disk file has changed (or been deleted)
|
|
302
|
+
// since it was run. Either situation means the database state and the
|
|
303
|
+
// migration history no longer agree, so we BLOCK the run by default.
|
|
304
|
+
// Users can pass `allowDrift: true` (CLI: `--allow-drift`) to force past
|
|
305
|
+
// the block when they are intentionally rewriting history.
|
|
306
|
+
if (!allowDrift) {
|
|
295
307
|
const mismatches = await validateChecksums(client, migrationsDir);
|
|
296
308
|
if (mismatches.length > 0) {
|
|
297
309
|
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
298
310
|
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
const lines = [
|
|
312
|
+
'[turbine] Migration drift detected — refusing to apply pending migrations.',
|
|
313
|
+
'',
|
|
314
|
+
'Applied migrations should be immutable. The following files no longer match their applied state:',
|
|
315
|
+
'',
|
|
316
|
+
];
|
|
317
|
+
for (const m of modified) {
|
|
318
|
+
lines.push(` - ${m.name}.sql (modified on disk)`);
|
|
319
|
+
}
|
|
320
|
+
for (const m of missing) {
|
|
321
|
+
lines.push(` - ${m.name}.sql (deleted from disk)`);
|
|
322
|
+
}
|
|
323
|
+
lines.push('');
|
|
324
|
+
lines.push('Fix one of these:');
|
|
325
|
+
lines.push(' 1. Restore the file(s) to their original content, OR');
|
|
326
|
+
lines.push(' 2. Roll back the affected migrations with `npx turbine migrate down`, OR');
|
|
327
|
+
lines.push(' 3. Pass `--allow-drift` to bypass this check (advanced — make sure you know what you are doing).');
|
|
328
|
+
throw new errors_js_1.MigrationError(lines.join('\n'));
|
|
305
329
|
}
|
|
306
330
|
}
|
|
307
331
|
const applied = await getAppliedMigrations(client);
|
package/dist/cjs/client.js
CHANGED
|
@@ -195,6 +195,11 @@ class TurbineClient {
|
|
|
195
195
|
defaultLimit: config.defaultLimit,
|
|
196
196
|
warnOnUnlimited: config.warnOnUnlimited,
|
|
197
197
|
};
|
|
198
|
+
// Apply NotFoundError message redaction mode (default: safe — values are
|
|
199
|
+
// stripped from messages to avoid leaking PII into error logs).
|
|
200
|
+
if (config.errorMessages) {
|
|
201
|
+
(0, errors_js_1.setErrorMessageMode)(config.errorMessages);
|
|
202
|
+
}
|
|
198
203
|
if (config.pool) {
|
|
199
204
|
// External pool — use directly. Turbine doesn't manage its lifecycle.
|
|
200
205
|
this.pool = config.pool;
|
|
@@ -399,6 +404,24 @@ class TurbineClient {
|
|
|
399
404
|
async $transaction(fn, options) {
|
|
400
405
|
const client = await this.pool.connect();
|
|
401
406
|
const timeout = options?.timeout;
|
|
407
|
+
/**
|
|
408
|
+
* Track whether the connection has already been released so the finally
|
|
409
|
+
* block doesn't double-release. When a timeout fires we destroy the
|
|
410
|
+
* connection eagerly to abort the in-flight backend query.
|
|
411
|
+
*/
|
|
412
|
+
let released = false;
|
|
413
|
+
const releaseOnce = (err) => {
|
|
414
|
+
if (released)
|
|
415
|
+
return;
|
|
416
|
+
released = true;
|
|
417
|
+
try {
|
|
418
|
+
client.release(err);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// pg may throw if the client is already released — swallow.
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
let timedOut = false;
|
|
402
425
|
try {
|
|
403
426
|
// BEGIN with optional isolation level
|
|
404
427
|
let beginSQL = 'BEGIN';
|
|
@@ -422,10 +445,22 @@ class TurbineClient {
|
|
|
422
445
|
}
|
|
423
446
|
let result;
|
|
424
447
|
if (timeout) {
|
|
425
|
-
// Race between the function and a timeout
|
|
448
|
+
// Race between the function and a timeout. If the timeout fires we
|
|
449
|
+
// need to actually abort the in-flight query — otherwise the backend
|
|
450
|
+
// keeps running until pg's own timeout, holding a pool slot the whole
|
|
451
|
+
// time. The simplest reliable cancellation is to destroy the
|
|
452
|
+
// connection: passing a truthy argument to client.release() tells the
|
|
453
|
+
// pg pool to discard the client (its socket is closed, which causes
|
|
454
|
+
// Postgres to abort the active query and roll back the transaction).
|
|
455
|
+
// The pool will spin up a fresh connection on the next checkout.
|
|
426
456
|
let timer;
|
|
427
457
|
const timeoutPromise = new Promise((_, reject) => {
|
|
428
458
|
timer = setTimeout(() => {
|
|
459
|
+
timedOut = true;
|
|
460
|
+
// Destroy the connection to abort the in-flight backend query.
|
|
461
|
+
// We do this BEFORE rejecting so the socket is gone by the time
|
|
462
|
+
// the caller's catch block runs.
|
|
463
|
+
releaseOnce(new Error('[turbine] Transaction timeout — connection destroyed'));
|
|
429
464
|
reject(new errors_js_1.TimeoutError(timeout, 'Transaction'));
|
|
430
465
|
}, timeout);
|
|
431
466
|
});
|
|
@@ -446,14 +481,25 @@ class TurbineClient {
|
|
|
446
481
|
return result;
|
|
447
482
|
}
|
|
448
483
|
catch (err) {
|
|
449
|
-
|
|
484
|
+
// If the timeout fired we already destroyed the connection — issuing a
|
|
485
|
+
// ROLLBACK on a released client would throw "Client has already been
|
|
486
|
+
// released". Skip the rollback in that case (the backend rolled back
|
|
487
|
+
// when its socket was closed).
|
|
488
|
+
if (!timedOut && !released) {
|
|
489
|
+
try {
|
|
490
|
+
await client.query('ROLLBACK');
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Best-effort rollback — the connection may have died mid-query.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
450
496
|
if (this.logging) {
|
|
451
497
|
console.log('[turbine] Transaction rolled back');
|
|
452
498
|
}
|
|
453
499
|
throw err;
|
|
454
500
|
}
|
|
455
501
|
finally {
|
|
456
|
-
|
|
502
|
+
releaseOnce();
|
|
457
503
|
}
|
|
458
504
|
}
|
|
459
505
|
// -------------------------------------------------------------------------
|