turbine-orm 0.27.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +17 -13
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/index.js +273 -71
  4. package/dist/cjs/cli/mcp.js +788 -0
  5. package/dist/cjs/cli/migrate.js +95 -20
  6. package/dist/cjs/cli/studio.js +3 -2
  7. package/dist/cjs/client.js +267 -34
  8. package/dist/cjs/dialect.js +2 -0
  9. package/dist/cjs/generate.js +171 -7
  10. package/dist/cjs/index.js +4 -1
  11. package/dist/cjs/introspect.js +177 -4
  12. package/dist/cjs/query/batched-loader.js +148 -0
  13. package/dist/cjs/query/builder.js +714 -133
  14. package/dist/cjs/schema-builder.js +59 -4
  15. package/dist/cjs/schema-sql.js +315 -6
  16. package/dist/cjs/seed.js +66 -0
  17. package/dist/cli/config.d.ts +9 -2
  18. package/dist/cli/config.js +19 -3
  19. package/dist/cli/index.d.ts +52 -1
  20. package/dist/cli/index.js +272 -74
  21. package/dist/cli/mcp.d.ts +17 -0
  22. package/dist/cli/mcp.js +781 -0
  23. package/dist/cli/migrate.d.ts +37 -0
  24. package/dist/cli/migrate.js +92 -20
  25. package/dist/cli/studio.d.ts +3 -2
  26. package/dist/cli/studio.js +3 -2
  27. package/dist/client.d.ts +136 -1
  28. package/dist/client.js +267 -34
  29. package/dist/dialect.d.ts +17 -0
  30. package/dist/dialect.js +2 -0
  31. package/dist/generate.d.ts +17 -0
  32. package/dist/generate.js +171 -10
  33. package/dist/index.d.ts +4 -3
  34. package/dist/index.js +2 -0
  35. package/dist/introspect.d.ts +20 -1
  36. package/dist/introspect.js +175 -4
  37. package/dist/query/batched-loader.d.ts +29 -2
  38. package/dist/query/batched-loader.js +148 -1
  39. package/dist/query/builder.d.ts +156 -8
  40. package/dist/query/builder.js +715 -134
  41. package/dist/query/index.d.ts +1 -1
  42. package/dist/query/types.d.ts +113 -8
  43. package/dist/schema-builder.d.ts +73 -8
  44. package/dist/schema-builder.js +59 -4
  45. package/dist/schema-sql.d.ts +67 -0
  46. package/dist/schema-sql.js +310 -6
  47. package/dist/schema.d.ts +53 -0
  48. package/dist/seed.d.ts +4 -0
  49. package/dist/seed.js +63 -0
  50. package/package.json +2 -3
package/README.md CHANGED
@@ -10,22 +10,24 @@ npm install turbine-orm
10
10
 
11
11
  ## Why Turbine?
12
12
 
13
- Every TS ORM now resolves nested relations in a single `json_agg` query — Prisma 7 and Drizzle v2 both ship it, and so does Turbine. That part is table stakes. The reason to reach for Turbine is the **safety bundle**: the boxes a DBA ticks before a query layer goes anywhere near production. It's the only TypeScript ORM that ships all six of these together:
13
+ Every TS ORM now resolves nested relations in a single `json_agg` query — Prisma 7 and Drizzle both ship it, and so does Turbine. That part is table stakes. The reason to reach for Turbine is the **safety bundle**: the boxes a DBA ticks before a query layer goes anywhere near production. It's the only TypeScript ORM that ships all six of these together:
14
14
 
15
15
  1. **Read-only Studio your DBA will approve.** `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.
16
16
  2. **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.
17
- 3. **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.
17
+ 3. **One runtime dependency (`pg`).** No engine binary, no WASM, no adapter packages to keep in lockstep. The main entry bundles to ~42 kB brotli; the edge entry to ~33 kB brotli. Prisma 7 dropped its Rust query engine, but its client still ships a TypeScript/WASM query compiler — a ~1.6 MB bundle, down from the ~14 MB Rust-era client.
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.
20
20
  6. **Pipeline batching via wire protocol.** Real Parse/Bind/Execute pipeline — not queries wrapped in a transaction. N independent queries in one round-trip.
21
21
 
22
22
  See [How It Works](#how-it-works) for the `json_agg` query strategy itself — but the query strategy isn't why you'd pick Turbine. The safety bundle above is: a Studio that can't mutate prod, errors that never leak PII, one dependency, and checksummed migrations.
23
23
 
24
+ **New in 0.28.0:** [global filters](https://turbineorm.dev/global-filters) for soft-delete and multi-tenancy · [read replicas](https://turbineorm.dev/read-replicas) with a `$primary()` escape hatch · a read-only [MCP server](https://turbineorm.dev/mcp) for AI agents · [seed-as-code](https://turbineorm.dev/seeding) and a non-interactive `migrate deploy` for CI · [Zod generation](https://turbineorm.dev/zod) · read-only [views & generated columns](https://turbineorm.dev/views) · `NULLS FIRST/LAST` ordering, relation `_count`, and ordering by a relation · schema referential actions, enums, array, `vector`, and check constraints.
25
+
24
26
  ## Benchmarks
25
27
 
26
28
  Tested against **Prisma 7.6** (adapter-pg, relationJoins preview on) and **Drizzle 0.45** (relational queries) on a **Neon** PostgreSQL database (pooled endpoint, US-East, PostgreSQL 17.8). 100 iterations, 20 warmup, Node v22. Same schema, same data (1K users, 10K posts, 50K comments), same connection pool config. _Measured April 2026 on turbine-orm 0.7.1; the core read path these scenarios exercise is unchanged through 0.17.0 — see [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md) to reproduce._
27
29
 
28
- | Scenario | Turbine | Prisma 7 | Drizzle v2 |
30
+ | Scenario | Turbine | Prisma 7 | Drizzle 0.45 |
29
31
  |---|---|---|---|
30
32
  | findMany — 100 users (flat) | **51.97 ms** | 52.90 ms | 53.51 ms |
31
33
  | findMany — 50 users + posts (L2) | **55.84 ms** | 56.10 ms | 88.80 ms |
@@ -43,7 +45,7 @@ Tested against **Prisma 7.6** (adapter-pg, relationJoins preview on) and **Drizz
43
45
  - **Streaming 50K rows.** Turbine's optimized streaming (speculative first fetch + batch size 1000) matches Prisma at ~3.1–3.2 s. Drizzle's keyset pagination is 1.49× slower at 4.6 s. Turbine's cursor still gives you correctness on any `orderBy` and clean early-`break` semantics.
44
46
  - **Pipeline batching** puts 5 independent queries through a single round-trip using the Postgres extended-query pipeline protocol — all three ORMs are tied here since each runs 5 queries sequentially in a transaction.
45
47
 
46
- Performance is at parity with Prisma and Drizzle — the real reasons to choose Turbine are elsewhere: **one dependency and no WASM engine** (vs Prisma's 1.6 MB WASM query engine), the **only read-only Studio** in the TS ORM ecosystem, **PII-safe error messages** that never leak user data, and **SQL-first migrations** with SHA-256 drift detection. Deep type inference through `with` clauses works end-to-end: write `db.users.findMany({ with: { posts: { with: { comments: true } } } })` and `users[0].posts[0].comments[0].body` autocompletes — no manual assertion, no helper annotation.
48
+ Performance is at parity with Prisma and Drizzle — the real reasons to choose Turbine are elsewhere: **one dependency and no WASM** (vs Prisma 7's ~1.6 MB TypeScript/WASM query compiler), the **only read-only Studio** in the TS ORM ecosystem, **PII-safe error messages** that never leak user data, and **SQL-first migrations** with SHA-256 drift detection. Deep type inference through `with` clauses works end-to-end: write `db.users.findMany({ with: { posts: { with: { comments: true } } } })` and `users[0].posts[0].comments[0].body` autocompletes — no manual assertion, no helper annotation.
47
49
 
48
50
  > Full analysis with p50/p95/p99 and methodology notes: [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md).
49
51
  > Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench.ts`
@@ -688,7 +690,7 @@ npx turbine studio --port 5173 --host 127.0.0.1 --no-open
688
690
  **Security posture (read-only by design)**
689
691
 
690
692
  - **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.
691
- - **Loopback by default** (`127.0.0.1`) with a loud warning if you bind to a non-loopback address.
693
+ - **Loopback by default** (`127.0.0.1`). Non-loopback `--host` is **refused** unless you pass `--allow-remote` (loud warning when you opt in).
692
694
  - **Per-process auth token** — 24 random bytes of hex, stored in a `SameSite=Strict` `HttpOnly` cookie.
693
695
  - **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.
694
696
  - **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.
@@ -727,7 +729,7 @@ TURBINE_OBSERVE_URL=postgres://... npx turbine observe
727
729
  # Flags: --port (default 4984), --host (default 127.0.0.1), --no-open
728
730
  ```
729
731
 
730
- 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.
732
+ Same security model as Studio: loopback by default, non-loopback refused without `--allow-remote`, per-process random auth token in an `HttpOnly` cookie, CSP headers, and read-only access to the metrics table.
731
733
 
732
734
  ## Serverless / Edge
733
735
 
@@ -907,7 +909,7 @@ Priority order: CLI flags > environment variables (`DATABASE_URL`) > config file
907
909
 
908
910
  ## How It Works
909
911
 
910
- Turbine resolves nested relations the same way Prisma 7 and Drizzle v2 do: correlated subqueries with `json_agg` + `json_build_object`, evaluated by PostgreSQL in a single round-trip. No N+1, no client-side stitching, no separate queries per relation. The `with` clause is fully type-inferred end-to-end — write `db.users.findMany({ with: { posts: { with: { comments: { with: { author: true } } } } } })` and `users[0].posts[0].comments[0].author.name` autocompletes with zero manual annotation.
912
+ Turbine resolves nested relations the same way Prisma 7 and Drizzle do: correlated subqueries with `json_agg` + `json_build_object`, evaluated by PostgreSQL in a single round-trip. No N+1, no client-side stitching, no separate queries per relation. The `with` clause is fully type-inferred end-to-end — write `db.users.findMany({ with: { posts: { with: { comments: { with: { author: true } } } } } })` and `users[0].posts[0].comments[0].author.name` autocompletes with zero manual annotation.
911
913
 
912
914
  The query strategy is table stakes now. What isn't table stakes: the one-dependency, no-WASM footprint, the read-only Studio your DBA will approve, the error messages that never leak PII, and the SQL-first migrations with SHA-256 drift detection. See [Why Turbine?](#why-turbine) for the full breakdown.
913
915
 
@@ -931,13 +933,13 @@ Turbine maps Postgres types to TypeScript:
931
933
 
932
934
  | | **Turbine** | **Prisma** | **Drizzle** | **Kysely** |
933
935
  |---|---|---|---|---|
934
- | **Engine / runtime** | No engine binary (`pg` only) | Client + 1.6 MB WASM engine | No engine | No engine |
935
- | **Runtime deps** | 1 (`pg`) | `@prisma/client` + adapter | 0 | 0 |
936
- | **Main bundle (brotli)** | ~31 kB | dominated by 1.6 MB WASM | ~7 KB core | small |
937
- | **Studio** | Read-only, 192-bit auth | Full CRUD, cloud-hosted | Paid tier | None |
936
+ | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine |
937
+ | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 |
938
+ | **Main bundle (brotli)** | ~42 kB | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
939
+ | **Studio** | Read-only, 192-bit auth | Full CRUD, cloud-hosted | Free; hosted Gateway paid | None |
938
940
  | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
939
941
  | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None |
940
- | **Edge runtime** | One import swap, ~22 kB brotli | 1.6 MB WASM adapter | Native | Native |
942
+ | **Edge runtime** | One import swap, ~33 kB brotli | Driver adapter + WASM compiler | Native | Native |
941
943
  | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
942
944
  | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
943
945
  | **Nested relations** | 1 query, deep type inference | 1 query, shallow inference | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
@@ -946,7 +948,9 @@ Turbine maps Postgres types to TypeScript:
946
948
  | **LISTEN/NOTIFY** | `$listen` / `$notify` | None | None | None |
947
949
  | **Multi-DB** | Postgres-first (+ SQLite/MySQL/MSSQL engines) | PG, MySQL, SQLite, MSSQL | PG, MySQL, SQLite | PG, MySQL, SQLite |
948
950
 
949
- All three ORMs now do single-query nested loads — that's table stakes. Turbine's real differentiators: no engine binary or WASM — just one dependency (`pg`), vs Prisma's 1.6 MB WASM query engine; the only read-only Studio in the ecosystem; error messages that never leak PII; and SQL-first migrations with SHA-256 drift detection. See [Benchmarks](#benchmarks) for performance numbers — most scenarios are within noise over a real pooled database.
951
+ All three ORMs now do single-query nested loads — that's table stakes. Turbine's real differentiators: no engine binary or WASM — just one dependency (`pg`), vs Prisma 7's ~1.6 MB TypeScript/WASM query compiler and required driver adapter; the only read-only Studio in the ecosystem; error messages that never leak PII; and SQL-first migrations with SHA-256 drift detection. See [Benchmarks](#benchmarks) for performance numbers — most scenarios are within noise over a real pooled database.
952
+
953
+ **A note on Kysely.** Kysely's [`jsonArrayFrom` / `jsonObjectFrom`](https://kysely.dev/docs/recipes/relations) relations recipe builds nested results with the same correlated-subquery-plus-JSON approach Turbine uses — good evidence the pattern is the right one. The gap is in what the driver can no longer see once rows are aggregated into JSON: nested fields lose their column types, so a `Date` inside a `jsonArrayFrom` result is typed `Date` but arrives as a **string** at runtime ([kysely-org/kysely#482](https://github.com/kysely-org/kysely/issues/482)), and the nesting isn't type-checked at depth. Turbine's `WithResult` inference types the whole tree, and `parseNestedRow` re-applies date coercion (and snake→camel mapping) to every nested row — so `users[0].posts[0].createdAt` is an actual `Date`, at any depth, with no plugin to wire up.
950
954
 
951
955
  ## Limitations
952
956
 
@@ -43,6 +43,7 @@ exports.looksLikeSchemaFilePath = looksLikeSchemaFilePath;
43
43
  exports.loadConfig = loadConfig;
44
44
  exports.findConfigFile = findConfigFile;
45
45
  exports.resolveConfig = resolveConfig;
46
+ exports.resolveSeedFile = resolveSeedFile;
46
47
  exports.configTemplate = configTemplate;
47
48
  const node_fs_1 = require("node:fs");
48
49
  const node_path_1 = require("node:path");
@@ -64,6 +65,7 @@ function looksLikeSchemaFilePath(schema) {
64
65
  // Config file names, in priority order
65
66
  // ---------------------------------------------------------------------------
66
67
  const CONFIG_FILES = ['turbine.config.ts', 'turbine.config.mts', 'turbine.config.js', 'turbine.config.mjs'];
68
+ const DEFAULT_SEED_CANDIDATES = ['seed.ts', 'seed.js', 'seed.sql'];
67
69
  // ---------------------------------------------------------------------------
68
70
  // Load config
69
71
  // ---------------------------------------------------------------------------
@@ -121,10 +123,25 @@ function resolveConfig(fileConfig, overrides) {
121
123
  include: overrides.include ?? fileConfig.include ?? [],
122
124
  exclude: overrides.exclude ?? fileConfig.exclude ?? [],
123
125
  migrationsDir: fileConfig.migrationsDir ?? './turbine/migrations',
124
- seedFile: fileConfig.seedFile ?? './turbine/seed.ts',
126
+ seedFile: fileConfig.seed ?? fileConfig.seedFile,
125
127
  schemaFile: fileConfig.schemaFile ?? './turbine/schema.ts',
126
128
  };
127
129
  }
130
+ /**
131
+ * Resolve the seed file path. An explicit config value wins even if the file
132
+ * does not exist yet; otherwise the root-level defaults are tried in order.
133
+ */
134
+ function resolveSeedFile(config, cwd = process.cwd()) {
135
+ const explicit = config.seed ?? config.seedFile;
136
+ if (explicit)
137
+ return (0, node_path_1.resolve)(cwd, explicit);
138
+ for (const candidate of DEFAULT_SEED_CANDIDATES) {
139
+ const filePath = (0, node_path_1.resolve)(cwd, candidate);
140
+ if ((0, node_fs_1.existsSync)(filePath))
141
+ return filePath;
142
+ }
143
+ return null;
144
+ }
128
145
  // ---------------------------------------------------------------------------
129
146
  // Config file template (for `turbine init`)
130
147
  // ---------------------------------------------------------------------------
@@ -153,8 +170,8 @@ ${urlLine}
153
170
  /** Directory for SQL migration files */
154
171
  migrationsDir: './turbine/migrations',
155
172
 
156
- /** Path to seed file */
157
- seedFile: './turbine/seed.ts',
173
+ /** Path to seed file (defaults: ./seed.ts, ./seed.js, ./seed.sql) */
174
+ seed: './seed.ts',
158
175
 
159
176
  /** Path to schema builder file (for turbine push) */
160
177
  schemaFile: './turbine/schema.ts',