turbine-orm 0.5.0 → 0.7.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.
- package/README.md +194 -26
- package/dist/cjs/cli/config.js +5 -15
- package/dist/cjs/cli/index.js +240 -41
- package/dist/cjs/cli/migrate.js +71 -46
- package/dist/cjs/cli/ui.js +5 -9
- package/dist/cjs/client.js +109 -46
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +33 -13
- package/dist/cjs/index.js +39 -20
- package/dist/cjs/introspect.js +3 -5
- package/dist/cjs/pipeline.js +9 -2
- package/dist/cjs/query.js +442 -109
- package/dist/cjs/schema-builder.js +93 -24
- package/dist/cjs/schema-sql.js +157 -19
- package/dist/cjs/schema.js +5 -2
- package/dist/cjs/serverless.js +87 -176
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +245 -46
- package/dist/cli/migrate.d.ts +6 -1
- package/dist/cli/migrate.js +72 -47
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +77 -4
- package/dist/client.js +109 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +36 -16
- package/dist/index.d.ts +11 -9
- package/dist/index.js +16 -12
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +4 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +9 -2
- package/dist/query.d.ts +257 -36
- package/dist/query.js +443 -110
- package/dist/schema-builder.d.ts +2 -2
- package/dist/schema-builder.js +93 -25
- package/dist/schema-sql.d.ts +7 -3
- package/dist/schema-sql.js +157 -19
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +5 -2
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +33 -16
- package/dist/types.d.ts +0 -93
- package/dist/types.js +0 -126
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# turbine-orm
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npm install turbine-orm
|
|
@@ -8,30 +8,35 @@ npm install turbine-orm
|
|
|
8
8
|
|
|
9
9
|
## Why Turbine?
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Turbine is a PostgreSQL-native TypeScript ORM with features no other ORM offers together: **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. It resolves nested relations in a single SQL query using `json_agg` — an approach now shared by Prisma 7+ and Drizzle v2, but Turbine does it with 1 runtime dependency (`pg`) and ~110KB on npm.
|
|
12
12
|
|
|
13
|
-
**One query
|
|
13
|
+
**One query for nested relations.** When you write `db.users.findMany({ with: { posts: { with: { comments: true } } } })`, Turbine generates a single SQL statement using correlated subqueries with `json_agg`. Modern ORMs like Prisma 7+ and Drizzle v2 use similar single-query approaches (LATERAL JOINs). Turbine's advantage is architectural simplicity: 1 dependency, no code generation DSL, and PostgreSQL-native depth.
|
|
14
14
|
|
|
15
15
|
## Benchmarks
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Tested against **Prisma 7.6** (adapter-pg, relationJoins) and **Drizzle 0.45** (relational queries) on the same PostgreSQL database. 200 iterations, 20 warmup, Node v22.
|
|
18
18
|
|
|
19
|
-
| Scenario | Turbine |
|
|
19
|
+
| Scenario | Turbine | Prisma 7 | Drizzle v2 |
|
|
20
20
|
|---|---|---|---|
|
|
21
|
-
| **
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
| Scenario | Turbine |
|
|
21
|
+
| **findMany — 100 rows (flat)** | **0.39 ms** | 0.58 ms | 0.44 ms |
|
|
22
|
+
| **findMany — L2 nested (users + posts)** | **1.29 ms** | 1.84 ms | 1.30 ms |
|
|
23
|
+
| **findMany — L3 nested (users → posts → comments)** | **0.50 ms** | 0.91 ms | 0.69 ms |
|
|
24
|
+
| **findUnique by PK** | **0.08 ms** | 0.13 ms | 0.14 ms |
|
|
25
|
+
| **findUnique — L3 nested** | **0.18 ms** | 0.32 ms | 0.34 ms |
|
|
26
|
+
| **count** | **0.06 ms** | 0.10 ms | 0.08 ms |
|
|
27
|
+
|
|
28
|
+
| Scenario | Turbine | Prisma 7 | Drizzle v2 |
|
|
29
29
|
|---|---|---|---|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
30
|
+
| findMany — flat | **1.00x** | 1.51x | 1.15x |
|
|
31
|
+
| findMany — L2 nested | **1.00x** | 1.43x | 1.01x |
|
|
32
|
+
| findMany — L3 nested | **1.00x** | 1.81x | 1.38x |
|
|
33
|
+
| findUnique by PK | **1.00x** | 1.67x | 1.69x |
|
|
34
|
+
| findUnique — L3 nested | **1.00x** | 1.81x | 1.93x |
|
|
35
|
+
| count | **1.00x** | 1.70x | 1.38x |
|
|
33
36
|
|
|
34
|
-
Turbine is
|
|
37
|
+
Turbine is fastest in every scenario. The advantage is largest on deep nesting (1.8x vs Prisma, up to 1.9x vs Drizzle) and single-record lookups (1.7x). All three ORMs now use single-query approaches for nested relations — Turbine's advantage comes from lower per-query overhead (minimal JS object allocation, no query plan compilation layer, direct pg driver access).
|
|
38
|
+
|
|
39
|
+
> Reproduce: `cd benchmarks && npm install && npx prisma generate && DATABASE_URL=... npx tsx bench.ts`
|
|
35
40
|
|
|
36
41
|
## Quick Start
|
|
37
42
|
|
|
@@ -188,6 +193,22 @@ const users = await db.users.findMany({
|
|
|
188
193
|
// Generates: WHERE email ILIKE '%alice%'
|
|
189
194
|
```
|
|
190
195
|
|
|
196
|
+
### Streaming large result sets
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Stream rows using PostgreSQL cursors — constant memory, no matter how many rows
|
|
200
|
+
for await (const user of db.users.findManyStream({
|
|
201
|
+
where: { orgId: 1 },
|
|
202
|
+
batchSize: 500, // internal FETCH batch size (default: 100)
|
|
203
|
+
orderBy: { id: 'asc' },
|
|
204
|
+
with: { posts: true }, // nested relations work too
|
|
205
|
+
})) {
|
|
206
|
+
process.stdout.write(`${user.email}\n`);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Uses `DECLARE CURSOR` under the hood — rows are fetched in batches on a dedicated connection, parsed individually, and yielded via `AsyncGenerator`. Safe to `break` early; the cursor and connection are cleaned up automatically.
|
|
211
|
+
|
|
191
212
|
### Query timeout
|
|
192
213
|
|
|
193
214
|
```typescript
|
|
@@ -227,6 +248,31 @@ db.$use(async (params, next) => {
|
|
|
227
248
|
});
|
|
228
249
|
```
|
|
229
250
|
|
|
251
|
+
### Error handling
|
|
252
|
+
|
|
253
|
+
Turbine throws typed errors you can catch programmatically:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { NotFoundError, ValidationError, TimeoutError } from 'turbine-orm';
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const user = await db.users.findUniqueOrThrow({ where: { id: 999 } });
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof NotFoundError) {
|
|
262
|
+
// err.code === 'TURBINE_E001'
|
|
263
|
+
console.log('User not found');
|
|
264
|
+
} else if (err instanceof TimeoutError) {
|
|
265
|
+
// err.code === 'TURBINE_E002'
|
|
266
|
+
console.log('Query timed out');
|
|
267
|
+
} else if (err instanceof ValidationError) {
|
|
268
|
+
// err.code === 'TURBINE_E003'
|
|
269
|
+
console.log('Invalid query:', err.message);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Error codes: `TURBINE_E001` (NotFound), `TURBINE_E002` (Timeout), `TURBINE_E003` (Validation), `TURBINE_E004` (Connection), `TURBINE_E005` (Relation), `TURBINE_E006` (Migration), `TURBINE_E007` (CircularRelation).
|
|
275
|
+
|
|
230
276
|
## CLI
|
|
231
277
|
|
|
232
278
|
```
|
|
@@ -236,17 +282,19 @@ Commands:
|
|
|
236
282
|
init Initialize a Turbine project (creates config, dirs, templates)
|
|
237
283
|
generate | pull Introspect database and generate TypeScript types + client
|
|
238
284
|
push Apply schema-builder definitions to database
|
|
239
|
-
migrate create <name>
|
|
240
|
-
migrate
|
|
241
|
-
migrate
|
|
242
|
-
migrate
|
|
243
|
-
|
|
244
|
-
|
|
285
|
+
migrate create <name> Create a new SQL migration file
|
|
286
|
+
migrate create <name> --auto Auto-generate from schema diff
|
|
287
|
+
migrate up Apply pending migrations
|
|
288
|
+
migrate down Rollback last migration
|
|
289
|
+
migrate status Show applied/pending migrations
|
|
290
|
+
seed Run seed file
|
|
291
|
+
status Show database schema summary
|
|
245
292
|
|
|
246
293
|
Options:
|
|
247
294
|
--url, -u <url> Postgres connection string
|
|
248
295
|
--out, -o <dir> Output directory (default: ./generated/turbine)
|
|
249
296
|
--schema, -s <name> Postgres schema (default: public)
|
|
297
|
+
--auto Auto-generate migration from schema diff
|
|
250
298
|
--dry-run Show SQL without executing
|
|
251
299
|
--verbose, -v Detailed output
|
|
252
300
|
```
|
|
@@ -279,9 +327,12 @@ npx turbine generate # Regenerate typed client
|
|
|
279
327
|
### Migration workflow
|
|
280
328
|
|
|
281
329
|
```bash
|
|
282
|
-
# Create a
|
|
330
|
+
# Create a blank migration (write SQL manually)
|
|
283
331
|
npx turbine migrate create add_users_table
|
|
284
|
-
|
|
332
|
+
|
|
333
|
+
# Auto-generate migration from schema diff (compares defineSchema() vs live DB)
|
|
334
|
+
npx turbine migrate create add_email_index --auto
|
|
335
|
+
# -> Generates UP (ALTER/CREATE) and DOWN (reverse) SQL automatically
|
|
285
336
|
|
|
286
337
|
# Apply all pending migrations
|
|
287
338
|
npx turbine migrate up
|
|
@@ -293,6 +344,80 @@ npx turbine migrate down
|
|
|
293
344
|
npx turbine migrate status
|
|
294
345
|
```
|
|
295
346
|
|
|
347
|
+
## Serverless / Edge
|
|
348
|
+
|
|
349
|
+
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.
|
|
350
|
+
|
|
351
|
+
### Neon Serverless (HTTP / WebSocket)
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
// app/api/users/route.ts
|
|
355
|
+
import { Pool } from '@neondatabase/serverless';
|
|
356
|
+
import { turbineHttp } from 'turbine-orm/serverless';
|
|
357
|
+
import { schema } from '@/generated/turbine/metadata';
|
|
358
|
+
|
|
359
|
+
export const runtime = 'edge';
|
|
360
|
+
|
|
361
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
362
|
+
const db = turbineHttp(pool, schema);
|
|
363
|
+
|
|
364
|
+
export async function GET() {
|
|
365
|
+
const users = await db.table('users').findMany({
|
|
366
|
+
with: { posts: { with: { comments: true } } },
|
|
367
|
+
limit: 10,
|
|
368
|
+
});
|
|
369
|
+
return Response.json(users);
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Vercel Postgres
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import { createPool } from '@vercel/postgres';
|
|
377
|
+
import { turbineHttp } from 'turbine-orm/serverless';
|
|
378
|
+
import { schema } from './generated/turbine/metadata.js';
|
|
379
|
+
|
|
380
|
+
const pool = createPool({ connectionString: process.env.POSTGRES_URL });
|
|
381
|
+
const db = turbineHttp(pool, schema);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Supabase (direct Postgres — no HTTP proxy needed)
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { TurbineClient } from 'turbine-orm';
|
|
388
|
+
import { schema } from './generated/turbine/metadata.js';
|
|
389
|
+
|
|
390
|
+
const db = new TurbineClient({
|
|
391
|
+
connectionString: process.env.SUPABASE_DB_URL,
|
|
392
|
+
ssl: { rejectUnauthorized: false },
|
|
393
|
+
}, schema);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Cloudflare Workers
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { Pool } from '@neondatabase/serverless';
|
|
400
|
+
import { turbineHttp } from 'turbine-orm/serverless';
|
|
401
|
+
import { schema } from './generated/turbine/metadata';
|
|
402
|
+
|
|
403
|
+
export default {
|
|
404
|
+
async fetch(req: Request, env: Env) {
|
|
405
|
+
const pool = new Pool({ connectionString: env.DATABASE_URL });
|
|
406
|
+
const db = turbineHttp(pool, schema);
|
|
407
|
+
const users = await db.table('users').findMany({ limit: 10 });
|
|
408
|
+
return Response.json(users);
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Limitations on HTTP drivers
|
|
414
|
+
|
|
415
|
+
- **Streaming cursors** (`findManyStream`) require `DECLARE CURSOR`, which most HTTP drivers don't support. Use `findMany` with `limit` + pagination instead.
|
|
416
|
+
- **LISTEN/NOTIFY** is not available over HTTP.
|
|
417
|
+
- Transactions work but hold an HTTP connection for their duration — keep them short.
|
|
418
|
+
|
|
419
|
+
When Turbine receives an external pool, `db.disconnect()` is a no-op: the caller owns the pool's lifecycle.
|
|
420
|
+
|
|
296
421
|
## Configuration
|
|
297
422
|
|
|
298
423
|
Create `turbine.config.ts` in your project root (or run `npx turbine init`):
|
|
@@ -331,7 +456,50 @@ SELECT u.*,
|
|
|
331
456
|
FROM users u WHERE u.org_id = 1
|
|
332
457
|
```
|
|
333
458
|
|
|
334
|
-
This resolves the entire 3-level object graph in one database round-trip. Prisma
|
|
459
|
+
This resolves the entire 3-level object graph in one database round-trip. Prisma 7+ and Drizzle v2 also use single-query approaches (LATERAL JOINs), but Turbine's correlated subquery strategy has lower per-query overhead — see [Benchmarks](#benchmarks).
|
|
460
|
+
|
|
461
|
+
## Type Mapping
|
|
462
|
+
|
|
463
|
+
Turbine maps Postgres types to TypeScript:
|
|
464
|
+
|
|
465
|
+
| Postgres | TypeScript | Notes |
|
|
466
|
+
|---|---|---|
|
|
467
|
+
| `int2`, `int4`, `float4`, `float8` | `number` | Standard numeric types |
|
|
468
|
+
| `int8` / `bigint` | `number` | Values > `Number.MAX_SAFE_INTEGER` (2^53 - 1) are returned as `string` at runtime to avoid precision loss. This affects < 0.01% of use cases (auto-increment IDs, counts, etc. are all safe). |
|
|
469
|
+
| `numeric`, `money` | `string` | Arbitrary precision — kept as string to avoid JS float issues |
|
|
470
|
+
| `text`, `varchar`, `uuid`, `citext` | `string` | |
|
|
471
|
+
| `timestamptz`, `timestamp`, `date` | `Date` | |
|
|
472
|
+
| `boolean` | `boolean` | |
|
|
473
|
+
| `json`, `jsonb` | `unknown` | |
|
|
474
|
+
| `bytea` | `Buffer` | |
|
|
475
|
+
| Array types | `T[]` | e.g. `_text` → `string[]` |
|
|
476
|
+
|
|
477
|
+
## Comparison
|
|
478
|
+
|
|
479
|
+
| | **Turbine** | **Prisma** | **Drizzle** | **Kysely** |
|
|
480
|
+
|---|---|---|---|---|
|
|
481
|
+
| **Nested relations** | 1 query (`json_agg`) | 1 query (LATERAL JOIN + json_agg, since v5.8) | 1 query (LATERAL JOINs) | Manual (`jsonArrayFrom`) |
|
|
482
|
+
| **API style** | `findMany`, `with` | `findMany`, `include` | SQL-like + relational | SQL builder |
|
|
483
|
+
| **Schema** | TypeScript | Custom DSL (`.prisma`) | TypeScript | Manual interfaces |
|
|
484
|
+
| **Runtime deps** | 1 (`pg`) | `@prisma/client` + adapter | 0 | 0 |
|
|
485
|
+
| **Multi-DB** | PostgreSQL only | PG, MySQL, SQLite, MSSQL | PG, MySQL, SQLite | PG, MySQL, SQLite |
|
|
486
|
+
| **Code generation** | `turbine generate` | `prisma generate` | Not needed | Not needed |
|
|
487
|
+
|
|
488
|
+
All three ORMs now use single-query approaches for nested relations. Turbine uses correlated subqueries with `json_agg`, Prisma 7 uses LATERAL JOIN + `json_agg`, and Drizzle uses LATERAL JOINs. Turbine is 1.4–1.9x faster due to lower per-query overhead — minimal JS object allocation, no query plan compilation layer, and direct pg driver access. See [Benchmarks](#benchmarks) for full results.
|
|
489
|
+
|
|
490
|
+
## Limitations
|
|
491
|
+
|
|
492
|
+
Turbine is focused and opinionated. Here's what it doesn't do:
|
|
493
|
+
|
|
494
|
+
- **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
|
+
- **No full-text search operators.** TSVECTOR/TSQUERY are not exposed in the query builder. Use `db.raw` for full-text queries.
|
|
497
|
+
- **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
|
+
- **No admin UI.** Turbine Studio is planned but not yet available.
|
|
499
|
+
|
|
500
|
+
## Examples
|
|
501
|
+
|
|
502
|
+
- **[Next.js](./examples/nextjs/)** — Server-rendered app with nested relations, streaming, and live code demos
|
|
335
503
|
|
|
336
504
|
## Requirements
|
|
337
505
|
|
package/dist/cjs/cli/config.js
CHANGED
|
@@ -49,12 +49,7 @@ const node_url_1 = require("node:url");
|
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
// Config file names, in priority order
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
|
-
const CONFIG_FILES = [
|
|
53
|
-
'turbine.config.ts',
|
|
54
|
-
'turbine.config.mts',
|
|
55
|
-
'turbine.config.js',
|
|
56
|
-
'turbine.config.mjs',
|
|
57
|
-
];
|
|
52
|
+
const CONFIG_FILES = ['turbine.config.ts', 'turbine.config.mts', 'turbine.config.js', 'turbine.config.mjs'];
|
|
58
53
|
// ---------------------------------------------------------------------------
|
|
59
54
|
// Load config
|
|
60
55
|
// ---------------------------------------------------------------------------
|
|
@@ -106,10 +101,7 @@ function findConfigFile(cwd) {
|
|
|
106
101
|
*/
|
|
107
102
|
function resolveConfig(fileConfig, overrides) {
|
|
108
103
|
return {
|
|
109
|
-
url: overrides.url ??
|
|
110
|
-
process.env['DATABASE_URL'] ??
|
|
111
|
-
fileConfig.url ??
|
|
112
|
-
'',
|
|
104
|
+
url: overrides.url ?? process.env.DATABASE_URL ?? fileConfig.url ?? '',
|
|
113
105
|
out: overrides.out ?? fileConfig.out ?? './generated/turbine',
|
|
114
106
|
schema: overrides.schema ?? fileConfig.schema ?? 'public',
|
|
115
107
|
include: overrides.include ?? fileConfig.include ?? [],
|
|
@@ -123,15 +115,13 @@ function resolveConfig(fileConfig, overrides) {
|
|
|
123
115
|
// Config file template (for `turbine init`)
|
|
124
116
|
// ---------------------------------------------------------------------------
|
|
125
117
|
function configTemplate(connectionString) {
|
|
126
|
-
const
|
|
127
|
-
const urlLine = connectionString
|
|
128
|
-
? ` url: '${connectionString}',`
|
|
129
|
-
: ` url: process.env.DATABASE_URL,`;
|
|
118
|
+
const _url = connectionString ?? 'process.env.DATABASE_URL';
|
|
119
|
+
const urlLine = connectionString ? ` url: '${connectionString}',` : ` url: process.env.DATABASE_URL,`;
|
|
130
120
|
return `import type { TurbineCliConfig } from 'turbine-orm/cli';
|
|
131
121
|
|
|
132
122
|
/**
|
|
133
123
|
* Turbine configuration
|
|
134
|
-
* @see https://
|
|
124
|
+
* @see https://turbineorm.dev
|
|
135
125
|
*/
|
|
136
126
|
const config: TurbineCliConfig = {
|
|
137
127
|
/** Postgres connection string */
|