turbine-orm 0.19.1 → 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 +75 -7
- package/dist/cjs/cli/index.js +17 -9
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +0 -30
- package/dist/cjs/client.js +12 -13
- package/dist/cjs/query/builder.js +115 -49
- package/dist/cjs/query/utils.js +1 -0
- package/dist/cli/index.js +17 -9
- package/dist/cli/migrate.d.ts +2 -2
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +0 -10
- package/dist/cli/studio.js +0 -29
- package/dist/client.d.ts +12 -13
- package/dist/client.js +12 -13
- package/dist/index.d.ts +1 -1
- package/dist/query/builder.d.ts +8 -6
- package/dist/query/builder.js +115 -49
- 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 +3 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ 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 ~
|
|
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
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.
|
|
@@ -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
|
|
@@ -638,6 +659,42 @@ npx turbine studio --port 5173 --host 127.0.0.1 --no-open
|
|
|
638
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.
|
|
639
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.
|
|
640
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.
|
|
697
|
+
|
|
641
698
|
## Serverless / Edge
|
|
642
699
|
|
|
643
700
|
Turbine's core is driver-agnostic: pass any pg-compatible pool to `TurbineConfig.pool` (or use the `turbineHttp()` factory) and Turbine runs on **Vercel Edge**, **Cloudflare Workers**, **Deno Deploy**, **Netlify Edge**, or any other environment where a direct TCP connection is unavailable. No new dependencies — install whichever driver you already use.
|
|
@@ -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/cjs/cli/index.js
CHANGED
|
@@ -1164,13 +1164,15 @@ function showMigrateHelp() {
|
|
|
1164
1164
|
(0, ui_js_1.newline)();
|
|
1165
1165
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
1166
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)')}`);
|
|
1167
1168
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
1168
|
-
console.log(` ${(0, ui_js_1.cyan)('--dry-run')}
|
|
1169
|
-
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')}
|
|
1170
|
-
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`);
|
|
1171
1172
|
(0, ui_js_1.newline)();
|
|
1172
1173
|
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
|
1173
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`);
|
|
1174
1176
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate up`);
|
|
1175
1177
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate down --step 2`);
|
|
1176
1178
|
console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate status`);
|
|
@@ -1215,16 +1217,17 @@ function showHelp() {
|
|
|
1215
1217
|
(0, ui_js_1.newline)();
|
|
1216
1218
|
console.log(` ${(0, ui_js_1.bold)('Commands:')}`);
|
|
1217
1219
|
console.log(` ${(0, ui_js_1.cyan)('init')} Initialize a Turbine project`);
|
|
1218
|
-
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`);
|
|
1219
1221
|
console.log(` ${(0, ui_js_1.cyan)('push')} Apply schema definitions to database`);
|
|
1220
|
-
console.log(` ${(0, ui_js_1.cyan)('migrate')} ${(0, ui_js_1.dim)('<sub>')}
|
|
1221
|
-
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`);
|
|
1222
1224
|
console.log(` ${(0, ui_js_1.dim)('up')} Apply pending migrations`);
|
|
1223
1225
|
console.log(` ${(0, ui_js_1.dim)('down')} Rollback last migration`);
|
|
1224
1226
|
console.log(` ${(0, ui_js_1.dim)('status')} Show applied/pending migrations`);
|
|
1225
1227
|
console.log(` ${(0, ui_js_1.cyan)('seed')} Run seed file`);
|
|
1226
|
-
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`);
|
|
1227
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)')}`);
|
|
1228
1231
|
(0, ui_js_1.newline)();
|
|
1229
1232
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
1230
1233
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
@@ -1236,8 +1239,13 @@ function showHelp() {
|
|
|
1236
1239
|
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
|
|
1237
1240
|
console.log(` ${(0, ui_js_1.cyan)('--force, -f')} Overwrite existing files`);
|
|
1238
1241
|
(0, ui_js_1.newline)();
|
|
1239
|
-
console.log(` ${(0, ui_js_1.bold)('
|
|
1240
|
-
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)')}`);
|
|
1241
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)')}`);
|
|
1242
1250
|
console.log(` ${(0, ui_js_1.cyan)('--no-open')} Don't auto-open the browser`);
|
|
1243
1251
|
(0, ui_js_1.newline)();
|