turbine-orm 0.9.0 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # turbine-orm
2
2
 
3
- Postgres-native TypeScript ORM runs on **Neon, Vercel Postgres, Cloudflare, Supabase**, and any pg-compatible driver. Streaming cursors, typed errors, single-query nested relations. 1 dependency, ~110KB.
3
+ Postgres ORM built for the edge. One runtime dependency. Built-in read-only Studio. Code-first and DB-first schema workflows in the same CLI. Runs on **Neon, Vercel Postgres, Cloudflare Hyperdrive, Supabase**, and any pg-compatible driver.
4
4
 
5
5
  ```
6
6
  npm install turbine-orm
@@ -8,9 +8,14 @@ npm install turbine-orm
8
8
 
9
9
  ## Why Turbine?
10
10
 
11
- Turbine is a PostgreSQL-native TypeScript ORM built around features no other ORM bundles together: a **read-only, DBA-approvable Studio web UI** (the only one in the TS ORM ecosystem `BEGIN READ ONLY` + statement-stacking guard + loopback-only + 24-byte auth token), **cursor-based streaming** through nested relations, **typed error classes** with PostgreSQL constraint mapping, **pipeline batching** (N queries, 1 round-trip), **middleware**, and a driver-agnostic core that plugs into any pg-compatible pool so it runs on Vercel Edge, Cloudflare Workers, Deno Deploy, and similar environments. 1 runtime dependency (`pg`), ~110KB on npm.
11
+ The elevator pitch: **a Prisma-like DX without Prisma's engine, a Drizzle-class runtime footprint, and the only built-in read-only Studio in the TS ORM ecosystem.** Four things bundled together that no other ORM bundles:
12
12
 
13
- **One round-trip for nested relations.** `db.users.findMany({ with: { posts: { with: { comments: true } } } })` resolves the entire object graph in a single database round-trip, regardless of nesting depth. Prisma 7+ and Drizzle v2 also do single-query nested loads — Turbine's advantage is architectural simplicity: 1 dependency, no code generation DSL, no query plan compiler.
13
+ 1. **One runtime dependency (`pg`).** No engine binary, no WASM adapter, no code-generation DSL. ~110 KB on npm. 5 KB on the edge entry.
14
+ 2. **First-class edge support.** `turbineHttp(pool, schema)` — one import swap — and the full API runs on Neon, Vercel Postgres, Cloudflare Hyperdrive, Supabase. No extra adapter package, no WASM bundle.
15
+ 3. **Built-in read-only Studio.** `npx turbine studio` spins up a loopback-bound, token-authed web UI with `BEGIN READ ONLY` + statement-stacking guard. DBA-approvable. No separate install.
16
+ 4. **Code-first and DB-first in the same CLI.** `defineSchema()` in TypeScript *or* `npx turbine pull` against a live database — same generated client, same migrations runner. Prisma forces the DSL; Drizzle is code-only.
17
+
18
+ Plus the architectural bits you'd expect: pipeline batching (N queries in one round-trip via the pg extended-query protocol), `json_agg`-based nested relation loading (same technique Drizzle and Prisma 7 use — no N+1), deep `with` type inference, streaming cursors, typed error hierarchy with `isRetryable` discriminants, middleware.
14
19
 
15
20
  ## Benchmarks
16
21
 
@@ -34,7 +39,7 @@ Tested against **Prisma 7.6** (adapter-pg, relationJoins preview on) and **Drizz
34
39
  - **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.
35
40
  - **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.
36
41
 
37
- Beyond the numbers, Turbine's real strengths are: **one runtime dependency** (`pg`, ~110 KB), a **single import swap** for edge runtimes (`turbine-orm/serverless`), **typed Postgres errors** with a `readonly isRetryable` const for retry loops, and the **read-only Studio** web UI that ships in the CLI — the only one in the TS ORM ecosystem that physically cannot mutate your database. Deep type inference through `with` clauses is runtime-correct today (the relations are nested for you in a single round-trip) and lands at the type level in v1.0 — see the tracking issue for progress.
42
+ Beyond the numbers, Turbine's real strengths are: **one runtime dependency** (`pg`, ~110 KB), a **single import swap** for edge runtimes (`turbine-orm/serverless`), **typed Postgres errors** with a `readonly isRetryable` const for retry loops, and the **read-only Studio** web UI that ships in the CLI — the only one in the TS ORM ecosystem that physically cannot mutate your database. And 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 `*With*` helper interfaces.
38
43
 
39
44
  > Full analysis with p50/p95/p99 and methodology notes: [`benchmarks/RESULTS.md`](./benchmarks/RESULTS.md).
40
45
  > Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench.ts`
@@ -560,7 +565,7 @@ Priority order: CLI flags > environment variables (`DATABASE_URL`) > config file
560
565
 
561
566
  ## How It Works
562
567
 
563
- Turbine resolves the entire object graph in a single database round-trip, regardless of nesting depth. The runtime nests relations for you via `json_agg` — one round-trip, no N+1, no client-side stitching. Deep `with` type inference at the TypeScript level (so `users[0].posts[0].comments[0].author.name` autocompletes without a manual assertion) is the last piece landing in v1.0; today, the generated `*With*` helper interfaces let you annotate the return type when you need the nested shape to narrow.
568
+ Turbine resolves the entire object graph in a single database round-trip, regardless of nesting depth. The runtime nests relations for you via `json_agg` — one round-trip, no N+1, no client-side stitching. And the `with` clause is fully type-inferred: the generator emits branded `*Relations` interfaces with `RelationDescriptor` phantom fields, and a recursive `WithResult<T, R, W>` conditional type walks an arbitrarily-deep `with` literal to produce the exact nested return shape. Write `db.users.findMany({ with: { posts: { with: { comments: { with: { author: true } } } } } })` and `users[0].posts[0].comments[0].author.name` autocompletes no manual assertion, no `*With*` helper annotation.
564
569
 
565
570
  Prisma 7+ and Drizzle v2 also do single-query nested loads. Turbine's advantage isn't query latency (see [Benchmarks](#benchmarks) — all three are within noise over a real pooled database); it's architectural simplicity plus the read-only Studio. One runtime dependency (`pg`), no DSL compiler, no driver adapter shim for edge, and the only TS ORM Studio your DBA will approve.
566
571
 
@@ -33,12 +33,12 @@ const node_fs_1 = require("node:fs");
33
33
  const node_path_1 = require("node:path");
34
34
  const pg_1 = __importDefault(require("pg"));
35
35
  const errors_js_1 = require("../errors.js");
36
- const query_js_1 = require("../query.js");
36
+ const index_js_1 = require("../query/index.js");
37
37
  // ---------------------------------------------------------------------------
38
38
  // Tracking table management
39
39
  // ---------------------------------------------------------------------------
40
40
  const TRACKING_TABLE = '_turbine_migrations';
41
- const QUOTED_TRACKING_TABLE = (0, query_js_1.quoteIdent)(TRACKING_TABLE);
41
+ const QUOTED_TRACKING_TABLE = (0, index_js_1.quoteIdent)(TRACKING_TABLE);
42
42
  const CREATE_TRACKING_TABLE = `
43
43
  CREATE TABLE IF NOT EXISTS ${QUOTED_TRACKING_TABLE} (
44
44
  id SERIAL PRIMARY KEY,
@@ -38,7 +38,7 @@ const node_os_1 = require("node:os");
38
38
  const node_path_1 = require("node:path");
39
39
  const pg_1 = __importDefault(require("pg"));
40
40
  const introspect_js_1 = require("../introspect.js");
41
- const query_js_1 = require("../query.js");
41
+ const index_js_1 = require("../query/index.js");
42
42
  const studio_ui_generated_js_1 = require("./studio-ui.generated.js");
43
43
  // ---------------------------------------------------------------------------
44
44
  // Main entry point
@@ -259,10 +259,10 @@ async function apiTableRows(res, ctx, rawTableName, params) {
259
259
  if (orderByRaw) {
260
260
  const col = resolveColumnName(table, orderByRaw);
261
261
  if (col)
262
- orderByClause = `ORDER BY ${(0, query_js_1.quoteIdent)(col)} ${dir}`;
262
+ orderByClause = `ORDER BY ${(0, index_js_1.quoteIdent)(col)} ${dir}`;
263
263
  }
264
264
  if (!orderByClause && table.primaryKey.length > 0 && table.primaryKey[0]) {
265
- orderByClause = `ORDER BY ${(0, query_js_1.quoteIdent)(table.primaryKey[0])} ${dir}`;
265
+ orderByClause = `ORDER BY ${(0, index_js_1.quoteIdent)(table.primaryKey[0])} ${dir}`;
266
266
  }
267
267
  // Full-text-ish search: ILIKE across text/varchar columns. The value is
268
268
  // parameterized so injection is impossible. Each query gets its own
@@ -276,7 +276,7 @@ async function apiTableRows(res, ctx, rawTableName, params) {
276
276
  let mainWhere = '';
277
277
  if (hasSearch && pattern !== null) {
278
278
  mainValues.push(pattern);
279
- const conds = textColumns.map((c) => `${(0, query_js_1.quoteIdent)(c)} ILIKE $3`);
279
+ const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $3`);
280
280
  mainWhere = `WHERE (${conds.join(' OR ')})`;
281
281
  }
282
282
  // Count query: $1 = pattern (if search)
@@ -284,10 +284,10 @@ async function apiTableRows(res, ctx, rawTableName, params) {
284
284
  let countWhere = '';
285
285
  if (hasSearch && pattern !== null) {
286
286
  countValues.push(pattern);
287
- const conds = textColumns.map((c) => `${(0, query_js_1.quoteIdent)(c)} ILIKE $1`);
287
+ const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $1`);
288
288
  countWhere = `WHERE (${conds.join(' OR ')})`;
289
289
  }
290
- const qualifiedTable = `${(0, query_js_1.quoteIdent)(ctx.options.schema)}.${(0, query_js_1.quoteIdent)(table.name)}`;
290
+ const qualifiedTable = `${(0, index_js_1.quoteIdent)(ctx.options.schema)}.${(0, index_js_1.quoteIdent)(table.name)}`;
291
291
  const sql = `SELECT * FROM ${qualifiedTable} ${mainWhere} ${orderByClause} LIMIT $1 OFFSET $2`;
292
292
  const countSql = `SELECT COUNT(*)::text AS count FROM ${qualifiedTable} ${countWhere}`;
293
293
  const client = await ctx.pool.connect();
@@ -397,7 +397,7 @@ async function apiBuilder(req, res, ctx) {
397
397
  }
398
398
  let deferred;
399
399
  try {
400
- const qi = new query_js_1.QueryInterface(ctx.pool, tableName, ctx.metadata, [], {
400
+ const qi = new index_js_1.QueryInterface(ctx.pool, tableName, ctx.metadata, [], {
401
401
  warnOnUnlimited: false,
402
402
  sqlCache: false,
403
403
  preparedStatements: false,
@@ -30,7 +30,7 @@ exports.TurbineClient = exports.TransactionClient = void 0;
30
30
  const pg_1 = __importDefault(require("pg"));
31
31
  const errors_js_1 = require("./errors.js");
32
32
  const pipeline_js_1 = require("./pipeline.js");
33
- const query_js_1 = require("./query.js");
33
+ const index_js_1 = require("./query/index.js");
34
34
  /** Maps isolation level names to SQL */
35
35
  const ISOLATION_LEVELS = {
36
36
  ReadUncommitted: 'READ UNCOMMITTED',
@@ -79,7 +79,7 @@ class TransactionClient {
79
79
  // Create a QueryInterface that uses the transaction client as its "pool"
80
80
  // We use a proxy pool that routes queries through the transaction client
81
81
  const txPool = this.createTxPool();
82
- qi = new query_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
82
+ qi = new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
83
83
  this.tableCache.set(name, qi);
84
84
  }
85
85
  return qi;
@@ -306,7 +306,7 @@ class TurbineClient {
306
306
  table(name) {
307
307
  let qi = this.tableCache.get(name);
308
308
  if (!qi) {
309
- qi = new query_js_1.QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
309
+ qi = new index_js_1.QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
310
310
  this.tableCache.set(name, qi);
311
311
  }
312
312
  return qi;
package/dist/cjs/index.js CHANGED
@@ -71,8 +71,8 @@ var pipeline_js_1 = require("./pipeline.js");
71
71
  Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return pipeline_js_1.executePipeline; } });
72
72
  Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: function () { return pipeline_js_1.pipelineSupported; } });
73
73
  // Query builder
74
- var query_js_1 = require("./query.js");
75
- Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return query_js_1.QueryInterface; } });
74
+ var index_js_1 = require("./query/index.js");
75
+ Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return index_js_1.QueryInterface; } });
76
76
  // Schema utilities
77
77
  var schema_js_1 = require("./schema.js");
78
78
  Object.defineProperty(exports, "camelToSnake", { enumerable: true, get: function () { return schema_js_1.camelToSnake; } });