turbine-orm 0.19.0 → 0.19.2
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 +83 -15
- package/dist/adapters/index.d.ts +3 -2
- package/dist/cjs/cli/index.js +43 -13
- package/dist/cjs/cli/loader.js +62 -7
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +25 -35
- package/dist/cjs/client.js +20 -13
- package/dist/cjs/query/builder.js +342 -104
- package/dist/cjs/query/utils.js +1 -0
- package/dist/cli/index.js +45 -15
- package/dist/cli/loader.d.ts +22 -5
- package/dist/cli/loader.js +61 -7
- package/dist/cli/migrate.d.ts +2 -2
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +9 -14
- package/dist/cli/studio.js +25 -34
- package/dist/client.d.ts +12 -13
- package/dist/client.js +20 -13
- package/dist/index.d.ts +1 -1
- package/dist/query/builder.d.ts +43 -6
- package/dist/query/builder.js +342 -104
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +62 -12
- package/dist/query/utils.js +1 -0
- package/package.json +4 -4
- package/dist/cjs/query.js +0 -2711
- package/dist/query.d.ts +0 -878
- package/dist/query.js +0 -2705
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ npm install turbine-orm
|
|
|
12
12
|
|
|
13
13
|
Prisma ships a 1.6 MB WASM query engine. Drizzle ships zero runtime but no Studio, no typed errors, no migration checksums. Turbine ships **one dependency (`pg`) and no engine binary**, and bundles six things no other TS ORM has together:
|
|
14
14
|
|
|
15
|
-
1. **One runtime dependency (`pg`).** No engine binary, no WASM adapter, no adapter packages to keep in lockstep. The main entry bundles to ~
|
|
16
|
-
2. **Built-in read-only Studio.** `npx turbine studio` spins up a loopback-bound web UI with 192-bit auth tokens, `BEGIN READ ONLY` transactions, and
|
|
15
|
+
1. **One runtime dependency (`pg`).** No engine binary, no WASM adapter, no adapter packages to keep in lockstep. The main entry bundles to ~31 kB brotli (~109 KB minified); the edge entry to ~22 kB brotli. Prisma's WASM query engine alone is 1.6 MB.
|
|
16
|
+
2. **Built-in read-only Studio.** `npx turbine studio` spins up a loopback-bound web UI with 192-bit auth tokens, `BEGIN READ ONLY` transactions, and — since v0.19 — no raw-SQL surface at all: queries are composed in the ORM's own validated builder. The only TS ORM Studio that physically cannot mutate your database. DBA-approvable.
|
|
17
17
|
3. **PII-safe error messages.** Turbine errors show WHERE keys, not values. A `UniqueConstraintError` says which column violated the constraint — never the actual user data. Safe to log, safe to surface to monitoring, no scrubbing needed.
|
|
18
18
|
4. **SQL-first migrations with drift detection.** Write real SQL. SHA-256 checksums catch modified migration files. `pg_try_advisory_lock()` prevents concurrent runs. Each migration in its own transaction. No shadow database, no magic DSL.
|
|
19
19
|
5. **Edge-native — one import swap.** `turbineHttp(pool, schema)` — same API on Neon, Vercel Postgres, Cloudflare Hyperdrive, Supabase. No WASM bundle, no adapter package, no separate serverless build.
|
|
@@ -343,6 +343,8 @@ const db = turbine({
|
|
|
343
343
|
|
|
344
344
|
### Middleware
|
|
345
345
|
|
|
346
|
+
Middleware wraps every query. It runs **after SQL generation**, so it can observe what's about to execute (`params.model`, `params.action`, `params.args`), measure timing, and transform the result returned by `next()` — but it cannot change the query itself.
|
|
347
|
+
|
|
346
348
|
```typescript
|
|
347
349
|
// Query timing
|
|
348
350
|
db.$use(async (params, next) => {
|
|
@@ -352,15 +354,33 @@ db.$use(async (params, next) => {
|
|
|
352
354
|
return result;
|
|
353
355
|
});
|
|
354
356
|
|
|
355
|
-
//
|
|
357
|
+
// Result transformation — redact a field on the way out
|
|
356
358
|
db.$use(async (params, next) => {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
+
const result = await next(params);
|
|
360
|
+
if (params.model === 'users' && Array.isArray(result)) {
|
|
361
|
+
for (const row of result as { email?: string }[]) row.email = '[redacted]';
|
|
359
362
|
}
|
|
360
|
-
return
|
|
363
|
+
return result;
|
|
361
364
|
});
|
|
362
365
|
```
|
|
363
366
|
|
|
367
|
+
> **Warning:** `params.args` is a read-only snapshot — mutating it does not change the executed SQL. The query is fully built and parameterized before middleware runs.
|
|
368
|
+
|
|
369
|
+
Because middleware can't rewrite queries, cross-cutting filters like **soft deletes** belong in the query itself — either explicitly or via a small scoped helper:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import type { WhereClause } from 'turbine-orm';
|
|
373
|
+
|
|
374
|
+
// Explicit filter
|
|
375
|
+
const users = await db.users.findMany({ where: { deletedAt: null } });
|
|
376
|
+
|
|
377
|
+
// Scoped helper that always applies the filter
|
|
378
|
+
const activeUsers = (where: WhereClause<User> = {}) =>
|
|
379
|
+
db.users.findMany({ where: { ...where, deletedAt: null } });
|
|
380
|
+
|
|
381
|
+
const rows = await activeUsers({ orgId: 1 });
|
|
382
|
+
```
|
|
383
|
+
|
|
364
384
|
### Error handling
|
|
365
385
|
|
|
366
386
|
Turbine throws typed errors you can catch programmatically:
|
|
@@ -557,6 +577,7 @@ Commands:
|
|
|
557
577
|
seed Run seed file
|
|
558
578
|
status Show database schema summary
|
|
559
579
|
studio Launch local read-only Studio web UI
|
|
580
|
+
observe Launch local metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
560
581
|
|
|
561
582
|
Options:
|
|
562
583
|
--url, -u <url> Postgres connection string
|
|
@@ -614,7 +635,7 @@ npx turbine migrate status
|
|
|
614
635
|
|
|
615
636
|
## Studio
|
|
616
637
|
|
|
617
|
-
The only Postgres ORM with a Studio your DBA will approve. `turbine studio` launches a local, read-only web UI for exploring your database — no mutations, no writes, no
|
|
638
|
+
The only Postgres ORM with a Studio your DBA will approve. `turbine studio` launches a local, read-only web UI for exploring your database — no mutations, no writes, and since v0.19 **no raw-SQL surface at all**: every query is composed visually in the ORM and compiled by the same validated query builder your application uses.
|
|
618
639
|
|
|
619
640
|
```bash
|
|
620
641
|
DATABASE_URL=postgres://user:pass@localhost:5432/mydb npx turbine studio
|
|
@@ -624,19 +645,55 @@ npx turbine studio --port 5173 --host 127.0.0.1 --no-open
|
|
|
624
645
|
|
|
625
646
|
**Features**
|
|
626
647
|
|
|
627
|
-
- **Data / Schema
|
|
628
|
-
- **
|
|
648
|
+
- **Query / Data / Schema tabs.** Compose queries visually, browse rows, and inspect tables and relations.
|
|
649
|
+
- **ORM-native query composer.** The Query tab builds a real `findMany` — drill into relations (`with`) to any depth, pick fields (`select`/`omit`), add filters (`where`), `orderBy`, and `limit` at every level — with a live TypeScript preview of the exact call to copy into your codebase.
|
|
650
|
+
- **Saved queries.** Named builder queries persisted to `.turbine/studio-queries.json` — share them across runs without committing them.
|
|
629
651
|
- **Cmd+K command palette.** Jump to any table, tab, or saved query in one keystroke.
|
|
630
652
|
- **Full-text search across rows.** The Data tab supports substring search across every text column of the current table.
|
|
631
|
-
- **Visual query composer.** The Builder tab lets you click together `where` / `orderBy` / `with` / `limit` clauses and renders the matching `db.table.findMany(...)` TypeScript in real time — copy it into your codebase.
|
|
632
653
|
|
|
633
654
|
**Security posture (read-only by design)**
|
|
634
655
|
|
|
656
|
+
- **No SQL input surface.** There is nothing to inject into — builder requests are validated identifier-by-identifier against the introspected schema, and every value is bound as a `$N` parameter.
|
|
635
657
|
- **Loopback by default** (`127.0.0.1`) with a loud warning if you bind to a non-loopback address.
|
|
636
658
|
- **Per-process auth token** — 24 random bytes of hex, stored in a `SameSite=Strict` `HttpOnly` cookie.
|
|
637
|
-
- **Every query runs inside `BEGIN READ ONLY
|
|
638
|
-
- **
|
|
639
|
-
|
|
659
|
+
- **Every query runs inside `BEGIN READ ONLY`** with a 30s transaction-local statement timeout (parameterized `set_config`). Writes are physically impossible at the transaction level.
|
|
660
|
+
- **Security headers on every response** — CSP, `X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer` — plus per-session rate limiting and cross-origin refusal.
|
|
661
|
+
|
|
662
|
+
## Observability
|
|
663
|
+
|
|
664
|
+
Built-in query metrics with zero new dependencies. `$observe` buffers per-query timings in memory and flushes **per-minute aggregates** — count, avg, p50, p95, p99, and error count per `model:action` — to a `_turbine_metrics` table in a **separate database**, over its own 1-connection pool so metrics writes never contend with your application pool.
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
const handle = await db.$observe({
|
|
668
|
+
connectionString: process.env.TURBINE_OBSERVE_URL!, // metrics DB (not your app DB)
|
|
669
|
+
flushIntervalMs: 60_000, // default: 60s
|
|
670
|
+
retentionDays: 30, // default: 30 — older buckets are pruned on flush
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Later, to flush remaining metrics and close the metrics pool
|
|
674
|
+
await handle.stop();
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
`$observe` creates the `_turbine_metrics` table if it doesn't exist. Flushes are fire-and-forget (`INSERT ... ON CONFLICT` additive merge) and never throw into your application. If the `TURBINE_OBSERVE_URL` environment variable is set, the client starts observing automatically on construction — no code needed.
|
|
678
|
+
|
|
679
|
+
For your own instrumentation, subscribe to query events with `$on('query')` — each event carries `sql`, `params`, `duration` (ms), `model`, `action`, `rows`, `timestamp`, and `error` (if the query failed):
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
db.$on('query', (e) => {
|
|
683
|
+
if (e.duration > 200) {
|
|
684
|
+
console.warn(`slow query: ${e.model}.${e.action} (${e.duration.toFixed(1)}ms, ${e.rows} rows)`);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
View the collected metrics in a local dashboard:
|
|
690
|
+
|
|
691
|
+
```bash
|
|
692
|
+
TURBINE_OBSERVE_URL=postgres://... npx turbine observe
|
|
693
|
+
# Flags: --port (default 4984), --host (default 127.0.0.1), --no-open
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Same security model as Studio: loopback binding by default, per-process random auth token in an `HttpOnly` cookie, CSP headers, and read-only access to the metrics table.
|
|
640
697
|
|
|
641
698
|
## Serverless / Edge
|
|
642
699
|
|
|
@@ -761,11 +818,11 @@ Turbine maps Postgres types to TypeScript:
|
|
|
761
818
|
|---|---|---|---|---|
|
|
762
819
|
| **Engine / runtime** | No engine binary (`pg` only) | Client + 1.6 MB WASM engine | No engine | No engine |
|
|
763
820
|
| **Runtime deps** | 1 (`pg`) | `@prisma/client` + adapter | 0 | 0 |
|
|
764
|
-
| **Main bundle (
|
|
821
|
+
| **Main bundle (brotli)** | ~31 kB | dominated by 1.6 MB WASM | ~7 KB core | small |
|
|
765
822
|
| **Studio** | Read-only, 192-bit auth | Full CRUD, cloud-hosted | Paid tier | None |
|
|
766
823
|
| **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
|
|
767
824
|
| **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None |
|
|
768
|
-
| **Edge runtime** | One import swap, ~
|
|
825
|
+
| **Edge runtime** | One import swap, ~22 kB brotli | 1.6 MB WASM adapter | Native | Native |
|
|
769
826
|
| **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
|
|
770
827
|
| **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
|
|
771
828
|
| **Nested relations** | 1 query, deep type inference | 1 query, shallow inference | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
|
|
@@ -818,6 +875,17 @@ Turbine is focused and opinionated. Here's what it doesn't do:
|
|
|
818
875
|
- PostgreSQL >= 14
|
|
819
876
|
- Works with both ESM (`import`) and CommonJS (`require`)
|
|
820
877
|
|
|
878
|
+
## Contributing
|
|
879
|
+
|
|
880
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, the test strategy, and the PR checklist. The unit suite runs without a database:
|
|
881
|
+
|
|
882
|
+
```bash
|
|
883
|
+
npm install
|
|
884
|
+
npm run test:unit
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
Integration tests need a PostgreSQL instance via `DATABASE_URL` (see CONTRIBUTING.md for a one-command seeded setup).
|
|
888
|
+
|
|
821
889
|
## License
|
|
822
890
|
|
|
823
891
|
MIT
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -55,8 +55,9 @@ export interface DatabaseAdapter {
|
|
|
55
55
|
introspectionOverrides?: Partial<IntrospectionOverrides>;
|
|
56
56
|
/**
|
|
57
57
|
* Generate the SQL to set a statement timeout within a transaction.
|
|
58
|
-
* PostgreSQL uses `
|
|
59
|
-
* CockroachDB uses `
|
|
58
|
+
* PostgreSQL uses `SELECT set_config('statement_timeout', $1, true)`.
|
|
59
|
+
* CockroachDB uses `SELECT set_config('transaction_timeout', $1, true)` (v23.1+).
|
|
60
|
+
* (`SET LOCAL ... = $1` is a syntax error — SET takes no bind params.)
|
|
60
61
|
*
|
|
61
62
|
* @param seconds — timeout in seconds
|
|
62
63
|
* @returns an object with the parameterized SQL and its bound values
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -167,6 +167,15 @@ function failMissingTsLoader(filePath, reason) {
|
|
|
167
167
|
console.log(` ${(0, ui_js_1.dim)('Your Node.js version does not support')} ${(0, ui_js_1.cyan)('module.register()')}.`);
|
|
168
168
|
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.')}`);
|
|
169
169
|
}
|
|
170
|
+
else if (reason === 'failed') {
|
|
171
|
+
// tsx IS installed but registering its loader threw. Report the real
|
|
172
|
+
// cause — telling the user to install tsx here would be a misdiagnosis.
|
|
173
|
+
console.log(` ${(0, ui_js_1.dim)('tsx is installed, but registering its TypeScript loader failed:')}`);
|
|
174
|
+
(0, ui_js_1.newline)();
|
|
175
|
+
console.log(` ${(0, loader_js_1.getTsLoaderError)() ?? '(unknown error)'}`);
|
|
176
|
+
(0, ui_js_1.newline)();
|
|
177
|
+
console.log(` ${(0, ui_js_1.dim)('Try upgrading tsx:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx@latest')}${(0, ui_js_1.dim)(', or rename your file to')} ${(0, ui_js_1.cyan)('.mjs')}.`);
|
|
178
|
+
}
|
|
170
179
|
else {
|
|
171
180
|
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.')}`);
|
|
172
181
|
(0, ui_js_1.newline)();
|
|
@@ -210,7 +219,7 @@ async function loadSchemaFile(schemaFile) {
|
|
|
210
219
|
// ERR_UNKNOWN_FILE_EXTENSION for `.ts`.
|
|
211
220
|
if ((0, loader_js_1.needsTsLoader)(absPath)) {
|
|
212
221
|
const status = await (0, loader_js_1.registerTsLoader)();
|
|
213
|
-
if (status === 'missing' || status === 'unsupported') {
|
|
222
|
+
if (status === 'missing' || status === 'unsupported' || status === 'failed') {
|
|
214
223
|
failMissingTsLoader(schemaFile, status);
|
|
215
224
|
}
|
|
216
225
|
}
|
|
@@ -346,7 +355,7 @@ export default defineSchema({
|
|
|
346
355
|
// id: { type: 'serial', primaryKey: true },
|
|
347
356
|
// email: { type: 'text', notNull: true, unique: true },
|
|
348
357
|
// name: { type: 'text', notNull: true },
|
|
349
|
-
// created_at: { type: '
|
|
358
|
+
// created_at: { type: 'timestamp', default: 'NOW()' },
|
|
350
359
|
// },
|
|
351
360
|
});
|
|
352
361
|
`, 'utf-8');
|
|
@@ -409,6 +418,9 @@ export default defineSchema({
|
|
|
409
418
|
console.log(` ${(0, ui_js_1.dim)('or create a')} ${(0, ui_js_1.cyan)('.env')} ${(0, ui_js_1.dim)('file with')} ${(0, ui_js_1.cyan)('DATABASE_URL=postgres://...')}`);
|
|
410
419
|
}
|
|
411
420
|
console.log(` ${(0, ui_js_1.dim)('2.')} Run ${(0, ui_js_1.cyan)('npx turbine generate')} to introspect your DB`);
|
|
421
|
+
if (!(0, loader_js_1.canResolveTsx)()) {
|
|
422
|
+
console.log(` ${(0, ui_js_1.dim)('Note: the TypeScript config requires')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('—')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
|
|
423
|
+
}
|
|
412
424
|
}
|
|
413
425
|
else {
|
|
414
426
|
console.log(` ${(0, ui_js_1.dim)('1.')} Import the generated client:`);
|
|
@@ -1152,13 +1164,15 @@ function showMigrateHelp() {
|
|
|
1152
1164
|
(0, ui_js_1.newline)();
|
|
1153
1165
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
1154
1166
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
1167
|
+
console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff ${(0, ui_js_1.dim)('(create only)')}`);
|
|
1155
1168
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
1156
|
-
console.log(` ${(0, ui_js_1.cyan)('--dry-run')}
|
|
1157
|
-
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')}
|
|
1158
|
-
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')}
|
|
1169
|
+
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
1170
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation ${(0, ui_js_1.dim)('(migrate up only — advanced)')}`);
|
|
1171
|
+
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
|
|
1159
1172
|
(0, ui_js_1.newline)();
|
|
1160
1173
|
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
1161
1174
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate create add_users_table`);
|
|
1175
|
+
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate create add_email_index --auto`);
|
|
1162
1176
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate up`);
|
|
1163
1177
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate down --step 2`);
|
|
1164
1178
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate status`);
|
|
@@ -1203,16 +1217,17 @@ function showHelp() {
|
|
|
1203
1217
|
(0, ui_js_1.newline)();
|
|
1204
1218
|
console.log(` ${(0, ui_js_1.bold)('Commands:')}`);
|
|
1205
1219
|
console.log(` ${(0, ui_js_1.cyan)('init')} Initialize a Turbine project`);
|
|
1206
|
-
console.log(` ${(0, ui_js_1.cyan)('generate')} ${(0, ui_js_1.dim)('| pull')}
|
|
1220
|
+
console.log(` ${(0, ui_js_1.cyan)('generate')} ${(0, ui_js_1.dim)('| pull')} Introspect database ${ui_js_1.symbols.arrow} generate types`);
|
|
1207
1221
|
console.log(` ${(0, ui_js_1.cyan)('push')} Apply schema definitions to database`);
|
|
1208
|
-
console.log(` ${(0, ui_js_1.cyan)('migrate')} ${(0, ui_js_1.dim)('<sub>')}
|
|
1209
|
-
console.log(` ${(0, ui_js_1.dim)('create <name>')}
|
|
1222
|
+
console.log(` ${(0, ui_js_1.cyan)('migrate')} ${(0, ui_js_1.dim)('<sub>')} SQL migration management`);
|
|
1223
|
+
console.log(` ${(0, ui_js_1.dim)('create <name>')} Create a new migration file`);
|
|
1210
1224
|
console.log(` ${(0, ui_js_1.dim)('up')} Apply pending migrations`);
|
|
1211
1225
|
console.log(` ${(0, ui_js_1.dim)('down')} Rollback last migration`);
|
|
1212
1226
|
console.log(` ${(0, ui_js_1.dim)('status')} Show applied/pending migrations`);
|
|
1213
1227
|
console.log(` ${(0, ui_js_1.cyan)('seed')} Run seed file`);
|
|
1214
|
-
console.log(` ${(0, ui_js_1.cyan)('status')} ${(0, ui_js_1.dim)('| info')}
|
|
1228
|
+
console.log(` ${(0, ui_js_1.cyan)('status')} ${(0, ui_js_1.dim)('| info')} Show schema summary`);
|
|
1215
1229
|
console.log(` ${(0, ui_js_1.cyan)('studio')} Launch local read-only web UI`);
|
|
1230
|
+
console.log(` ${(0, ui_js_1.cyan)('observe')} Launch metrics dashboard ${(0, ui_js_1.dim)('(requires TURBINE_OBSERVE_URL)')}`);
|
|
1216
1231
|
(0, ui_js_1.newline)();
|
|
1217
1232
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
1218
1233
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
@@ -1224,8 +1239,13 @@ function showHelp() {
|
|
|
1224
1239
|
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
|
|
1225
1240
|
console.log(` ${(0, ui_js_1.cyan)('--force, -f')} Overwrite existing files`);
|
|
1226
1241
|
(0, ui_js_1.newline)();
|
|
1227
|
-
console.log(` ${(0, ui_js_1.bold)('
|
|
1228
|
-
console.log(` ${(0, ui_js_1.cyan)('--
|
|
1242
|
+
console.log(` ${(0, ui_js_1.bold)('Migrate options:')}`);
|
|
1243
|
+
console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff ${(0, ui_js_1.dim)('(create)')}`);
|
|
1244
|
+
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
1245
|
+
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)')}`);
|
|
1246
|
+
(0, ui_js_1.newline)();
|
|
1247
|
+
console.log(` ${(0, ui_js_1.bold)('Studio / observe options:')}`);
|
|
1248
|
+
console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4983 studio, 4984 observe)')}`);
|
|
1229
1249
|
console.log(` ${(0, ui_js_1.cyan)('--host')} ${(0, ui_js_1.dim)('<addr>')} Bind address ${(0, ui_js_1.dim)('(default: 127.0.0.1)')}`);
|
|
1230
1250
|
console.log(` ${(0, ui_js_1.cyan)('--no-open')} Don't auto-open the browser`);
|
|
1231
1251
|
(0, ui_js_1.newline)();
|
|
@@ -1249,7 +1269,17 @@ function showVersion() {
|
|
|
1249
1269
|
// Using process.argv[1] instead of import.meta.url so the same code compiles
|
|
1250
1270
|
// cleanly for both the ESM and CJS builds.
|
|
1251
1271
|
try {
|
|
1252
|
-
|
|
1272
|
+
// Resolve symlinks first: `npx turbine` runs via node_modules/.bin/turbine,
|
|
1273
|
+
// a symlink whose dirname would walk the CONSUMER's tree and never find
|
|
1274
|
+
// turbine-orm's package.json (printing no version number at all).
|
|
1275
|
+
let entry = process.argv[1] ?? '';
|
|
1276
|
+
try {
|
|
1277
|
+
entry = (0, node_fs_1.realpathSync)(entry);
|
|
1278
|
+
}
|
|
1279
|
+
catch {
|
|
1280
|
+
// keep the raw path if realpath fails (e.g. deleted cwd)
|
|
1281
|
+
}
|
|
1282
|
+
let dir = (0, node_path_1.dirname)(entry);
|
|
1253
1283
|
for (let i = 0; i < 6; i++) {
|
|
1254
1284
|
const candidate = (0, node_path_1.resolve)(dir, 'package.json');
|
|
1255
1285
|
if ((0, node_fs_1.existsSync)(candidate)) {
|
|
@@ -1297,7 +1327,7 @@ async function main() {
|
|
|
1297
1327
|
const configPath = (0, config_js_1.findConfigFile)();
|
|
1298
1328
|
if ((0, loader_js_1.needsTsLoader)(configPath)) {
|
|
1299
1329
|
const status = await (0, loader_js_1.registerTsLoader)();
|
|
1300
|
-
if (status === 'missing' || status === 'unsupported') {
|
|
1330
|
+
if (status === 'missing' || status === 'unsupported' || status === 'failed') {
|
|
1301
1331
|
failMissingTsLoader(configPath ?? 'turbine.config.ts', status);
|
|
1302
1332
|
}
|
|
1303
1333
|
}
|
package/dist/cjs/cli/loader.js
CHANGED
|
@@ -9,9 +9,18 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Strategy:
|
|
11
11
|
* 1. If the file we're about to import ends in `.ts` / `.mts` / `.cts`,
|
|
12
|
-
* probe whether `tsx
|
|
13
|
-
* 2.
|
|
14
|
-
*
|
|
12
|
+
* probe whether `tsx` is resolvable from the user's CWD.
|
|
13
|
+
* 2. Prefer tsx's supported programmatic API, `tsx/esm/api`'s `register()`.
|
|
14
|
+
* Calling Node's `module.register('tsx/esm', ...)` directly throws
|
|
15
|
+
* "tsx must be loaded with --import instead of --loader" on every Node
|
|
16
|
+
* version that has `module.register()` (>= 20.6) — tsx's hook file
|
|
17
|
+
* guards against being loaded that way. The `tsx/esm/api` entry point
|
|
18
|
+
* is the documented path and works everywhere `module.register()` does.
|
|
19
|
+
* 3. Fall back to `module.register('tsx/esm', ...)` only for very old tsx
|
|
20
|
+
* versions (< 4.0) that predate `tsx/esm/api`.
|
|
21
|
+
* 4. If tsx isn't installed, or registration genuinely fails, surface an
|
|
22
|
+
* actionable error — including the REAL underlying error message, never
|
|
23
|
+
* a misdiagnosed "tsx is not installed".
|
|
15
24
|
*
|
|
16
25
|
* `tsx` is intentionally NOT a runtime dependency — many projects already
|
|
17
26
|
* have it, and adding a heavy dev tool to a 1-dependency ORM would be silly.
|
|
@@ -52,6 +61,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
52
61
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
62
|
exports.needsTsLoader = needsTsLoader;
|
|
54
63
|
exports.canResolveTsx = canResolveTsx;
|
|
64
|
+
exports.getTsLoaderError = getTsLoaderError;
|
|
55
65
|
exports.registerTsLoader = registerTsLoader;
|
|
56
66
|
exports._resetTsLoaderStateForTests = _resetTsLoaderStateForTests;
|
|
57
67
|
const node_module_1 = require("node:module");
|
|
@@ -89,6 +99,14 @@ function canResolveTsx(resolver) {
|
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
let tsLoaderState = null;
|
|
102
|
+
let tsLoaderError = null;
|
|
103
|
+
/**
|
|
104
|
+
* The underlying error message from the last failed registration attempt,
|
|
105
|
+
* or null. Lets the CLI report the REAL cause instead of guessing.
|
|
106
|
+
*/
|
|
107
|
+
function getTsLoaderError() {
|
|
108
|
+
return tsLoaderError;
|
|
109
|
+
}
|
|
92
110
|
/**
|
|
93
111
|
* Register the tsx ESM loader so subsequent dynamic imports of `.ts` files
|
|
94
112
|
* work. Safe to call multiple times — internal flag prevents double registration.
|
|
@@ -96,17 +114,51 @@ let tsLoaderState = null;
|
|
|
96
114
|
* Returns:
|
|
97
115
|
* - 'registered' loader was successfully registered this call
|
|
98
116
|
* - 'already' a loader was previously registered (idempotent)
|
|
99
|
-
* - 'unsupported' Node lacks `module.register()` (Node < 20.6)
|
|
117
|
+
* - 'unsupported' Node lacks `module.register()` (Node < 20.6) and tsx has
|
|
118
|
+
* no programmatic API to fall back to
|
|
100
119
|
* - 'missing' `tsx` is not installed in the user's project
|
|
120
|
+
* - 'failed' tsx IS installed but registration threw — see
|
|
121
|
+
* {@link getTsLoaderError} for the underlying message
|
|
101
122
|
*/
|
|
102
123
|
async function registerTsLoader() {
|
|
103
124
|
if (tsLoaderState === 'registered' || tsLoaderState === 'already') {
|
|
104
125
|
return 'already';
|
|
105
126
|
}
|
|
127
|
+
const userRequire = (0, node_module_1.createRequire)(`${process.cwd()}/`);
|
|
128
|
+
// Preferred: tsx's supported programmatic API (tsx >= 4.0).
|
|
129
|
+
let apiPath = null;
|
|
130
|
+
try {
|
|
131
|
+
apiPath = userRequire.resolve('tsx/esm/api');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
apiPath = null;
|
|
135
|
+
}
|
|
136
|
+
if (apiPath) {
|
|
137
|
+
try {
|
|
138
|
+
const api = (await Promise.resolve(`${(0, node_url_1.pathToFileURL)(apiPath).href}`).then(s => __importStar(require(s))));
|
|
139
|
+
if (typeof api.register !== 'function') {
|
|
140
|
+
throw new Error(`tsx/esm/api resolved at ${apiPath} but exports no register() function`);
|
|
141
|
+
}
|
|
142
|
+
api.register();
|
|
143
|
+
tsLoaderState = 'registered';
|
|
144
|
+
tsLoaderError = null;
|
|
145
|
+
return 'registered';
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
tsLoaderState = 'failed';
|
|
149
|
+
tsLoaderError = err instanceof Error ? err.message : String(err);
|
|
150
|
+
return 'failed';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// tsx/esm/api not resolvable — is tsx installed at all?
|
|
106
154
|
if (!canResolveTsx()) {
|
|
107
155
|
tsLoaderState = 'missing';
|
|
108
156
|
return 'missing';
|
|
109
157
|
}
|
|
158
|
+
// Legacy fallback for tsx < 4.0 (no tsx/esm/api): Node's module.register.
|
|
159
|
+
// On tsx >= 4.19 this path throws ("tsx must be loaded with --import
|
|
160
|
+
// instead of --loader") — but those versions all ship tsx/esm/api, so we
|
|
161
|
+
// only land here for genuinely old installs.
|
|
110
162
|
try {
|
|
111
163
|
const mod = await Promise.resolve().then(() => __importStar(require('node:module')));
|
|
112
164
|
const register = mod.register;
|
|
@@ -116,14 +168,17 @@ async function registerTsLoader() {
|
|
|
116
168
|
}
|
|
117
169
|
register('tsx/esm', (0, node_url_1.pathToFileURL)(`${process.cwd()}/`));
|
|
118
170
|
tsLoaderState = 'registered';
|
|
171
|
+
tsLoaderError = null;
|
|
119
172
|
return 'registered';
|
|
120
173
|
}
|
|
121
|
-
catch {
|
|
122
|
-
tsLoaderState = '
|
|
123
|
-
|
|
174
|
+
catch (err) {
|
|
175
|
+
tsLoaderState = 'failed';
|
|
176
|
+
tsLoaderError = err instanceof Error ? err.message : String(err);
|
|
177
|
+
return 'failed';
|
|
124
178
|
}
|
|
125
179
|
}
|
|
126
180
|
/** Reset the loader state — used by unit tests only. */
|
|
127
181
|
function _resetTsLoaderStateForTests() {
|
|
128
182
|
tsLoaderState = null;
|
|
183
|
+
tsLoaderError = null;
|
|
129
184
|
}
|