turbine-orm 0.16.0 → 0.18.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 +180 -12
- package/dist/adapters/cockroachdb.js +4 -2
- package/dist/adapters/index.js +4 -1
- package/dist/adapters/yugabytedb.js +4 -2
- package/dist/cjs/adapters/cockroachdb.js +4 -2
- package/dist/cjs/adapters/index.js +4 -1
- package/dist/cjs/adapters/yugabytedb.js +4 -2
- package/dist/cjs/cli/studio.js +5 -1
- package/dist/cjs/client.js +164 -0
- package/dist/cjs/errors.js +35 -5
- package/dist/cjs/generate.js +14 -3
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.js +81 -0
- package/dist/cjs/nested-write.js +70 -6
- package/dist/cjs/query/builder.js +538 -12
- package/dist/cjs/realtime.js +147 -0
- package/dist/cjs/schema-builder.js +86 -0
- package/dist/cjs/schema.js +10 -0
- package/dist/cjs/typed-sql.js +149 -0
- package/dist/cli/studio.js +5 -1
- package/dist/client.d.ts +120 -0
- package/dist/client.js +165 -1
- package/dist/errors.js +35 -5
- package/dist/generate.js +14 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -1
- package/dist/introspect.js +81 -0
- package/dist/nested-write.js +70 -6
- package/dist/query/builder.d.ts +104 -1
- package/dist/query/builder.js +539 -13
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +126 -2
- package/dist/realtime.d.ts +71 -0
- package/dist/realtime.js +144 -0
- package/dist/schema-builder.d.ts +68 -1
- package/dist/schema-builder.js +85 -0
- package/dist/schema.d.ts +18 -1
- package/dist/schema.js +10 -0
- package/dist/typed-sql.d.ts +101 -0
- package/dist/typed-sql.js +145 -0
- package/package.json +17 -15
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — Typed raw SQL (Turbine's answer to Prisma's TypedSQL)
|
|
3
|
+
*
|
|
4
|
+
* `client.raw()` returns untyped rows. This module adds a *typed* escape hatch:
|
|
5
|
+
* a generic tagged template where the caller supplies the row shape, and the
|
|
6
|
+
* builder yields a typed result that can be awaited as an array of rows, or
|
|
7
|
+
* narrowed to a single row (`.one()`) or a single scalar value (`.scalar()`).
|
|
8
|
+
*
|
|
9
|
+
* Design goals & guarantees:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Compile-time only types.** `T` is supplied by the caller and never
|
|
12
|
+
* validated at runtime — exactly like Prisma's TypedSQL and the existing
|
|
13
|
+
* `raw<T>()`. Postgres still returns whatever the SQL selects; the generic
|
|
14
|
+
* is a convenience for autocomplete and downstream type-checking.
|
|
15
|
+
*
|
|
16
|
+
* 2. **Mandatory parameterization.** Only the *static* string segments of the
|
|
17
|
+
* template literal ever reach the SQL text. Every interpolated `${value}`
|
|
18
|
+
* becomes a `$N` placeholder and is passed in the params array — it is
|
|
19
|
+
* impossible to string-concatenate a value into the query through this API.
|
|
20
|
+
* This is the whole point of the tagged-template shape: the literal segments
|
|
21
|
+
* are frozen by the compiler (`TemplateStringsArray`), and the only way to
|
|
22
|
+
* get a runtime value into the query is via `${...}`, which we bind.
|
|
23
|
+
*
|
|
24
|
+
* 3. **Rows are returned as-is (no snake→camel mapping).** This matches the
|
|
25
|
+
* existing `client.raw()` behavior: a typed raw query is a literal escape
|
|
26
|
+
* hatch, so the result columns are whatever your `SELECT` names them. Alias
|
|
27
|
+
* columns in SQL (`SELECT created_at AS "createdAt"`) if you want camelCase.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // Awaited directly -> rows
|
|
32
|
+
* const rows = await db.sql<{ id: number; name: string }>`
|
|
33
|
+
* SELECT id, name FROM users WHERE org_id = ${orgId}
|
|
34
|
+
* `;
|
|
35
|
+
* // ^? { id: number; name: string }[]
|
|
36
|
+
*
|
|
37
|
+
* // .one() -> single row or null
|
|
38
|
+
* const user = await db.sql<{ id: number; name: string }>`
|
|
39
|
+
* SELECT id, name FROM users WHERE id = ${userId}
|
|
40
|
+
* `.one();
|
|
41
|
+
* // ^? { id: number; name: string } | null
|
|
42
|
+
*
|
|
43
|
+
* // .scalar() -> first column of first row, or null
|
|
44
|
+
* const total = await db.sql<{ count: number }>`
|
|
45
|
+
* SELECT COUNT(*)::int AS count FROM users WHERE org_id = ${orgId}
|
|
46
|
+
* `.scalar();
|
|
47
|
+
* // ^? number | null
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
import type { PgCompatPool } from './client.js';
|
|
51
|
+
/**
|
|
52
|
+
* Build a `(sql, params)` pair from a tagged-template invocation.
|
|
53
|
+
*
|
|
54
|
+
* Each interpolated value is replaced by a positional `$N` placeholder and
|
|
55
|
+
* pushed to the params array in order. The static string segments are the only
|
|
56
|
+
* thing concatenated into the SQL text. This is the single point that
|
|
57
|
+
* guarantees parameterization for the entire typed-SQL surface.
|
|
58
|
+
*
|
|
59
|
+
* Exported for unit testing the parameterization invariant without a database.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildTypedSql(strings: TemplateStringsArray, values: readonly unknown[]): {
|
|
62
|
+
sql: string;
|
|
63
|
+
params: unknown[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A pending typed raw SQL query. Implements the thenable contract, so it can be
|
|
67
|
+
* `await`ed directly to get `T[]`, or refined via `.one()` / `.scalar()` first.
|
|
68
|
+
*
|
|
69
|
+
* The query is executed lazily and exactly once per terminal call (`then`,
|
|
70
|
+
* `one`, `scalar`). Each terminal method runs the query independently — this is
|
|
71
|
+
* an escape hatch, not a cached query object, so don't call two terminals on
|
|
72
|
+
* the same builder expecting a single round-trip; build a fresh template each
|
|
73
|
+
* time (the common pattern is `await db.sql\`...\`` inline).
|
|
74
|
+
*/
|
|
75
|
+
export declare class TypedSqlQuery<T extends Record<string, unknown>> implements PromiseLike<T[]> {
|
|
76
|
+
private readonly pool;
|
|
77
|
+
private readonly sql;
|
|
78
|
+
private readonly params;
|
|
79
|
+
private readonly logging;
|
|
80
|
+
constructor(pool: PgCompatPool, sql: string, params: unknown[], logging: boolean);
|
|
81
|
+
/** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
|
|
82
|
+
private run;
|
|
83
|
+
/**
|
|
84
|
+
* PromiseLike implementation: `await db.sql<T>\`...\`` resolves to `T[]`.
|
|
85
|
+
*/
|
|
86
|
+
then<TResult1 = T[], TResult2 = never>(onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
87
|
+
/**
|
|
88
|
+
* Execute and return the first row, or `null` if the query returns no rows.
|
|
89
|
+
* Use for queries you expect to match at most one row.
|
|
90
|
+
*/
|
|
91
|
+
one(): Promise<T | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Execute and return the first column of the first row, or `null` if there
|
|
94
|
+
* are no rows. Useful for `SELECT COUNT(*)`, `SELECT EXISTS(...)`, etc.
|
|
95
|
+
*
|
|
96
|
+
* The generic `V` defaults to the value type of `T`'s first property, but you
|
|
97
|
+
* can override it: `db.sql<{ count: number }>\`...\`.scalar<number>()`.
|
|
98
|
+
*/
|
|
99
|
+
scalar<V = T[keyof T]>(): Promise<V | null>;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=typed-sql.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — Typed raw SQL (Turbine's answer to Prisma's TypedSQL)
|
|
3
|
+
*
|
|
4
|
+
* `client.raw()` returns untyped rows. This module adds a *typed* escape hatch:
|
|
5
|
+
* a generic tagged template where the caller supplies the row shape, and the
|
|
6
|
+
* builder yields a typed result that can be awaited as an array of rows, or
|
|
7
|
+
* narrowed to a single row (`.one()`) or a single scalar value (`.scalar()`).
|
|
8
|
+
*
|
|
9
|
+
* Design goals & guarantees:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Compile-time only types.** `T` is supplied by the caller and never
|
|
12
|
+
* validated at runtime — exactly like Prisma's TypedSQL and the existing
|
|
13
|
+
* `raw<T>()`. Postgres still returns whatever the SQL selects; the generic
|
|
14
|
+
* is a convenience for autocomplete and downstream type-checking.
|
|
15
|
+
*
|
|
16
|
+
* 2. **Mandatory parameterization.** Only the *static* string segments of the
|
|
17
|
+
* template literal ever reach the SQL text. Every interpolated `${value}`
|
|
18
|
+
* becomes a `$N` placeholder and is passed in the params array — it is
|
|
19
|
+
* impossible to string-concatenate a value into the query through this API.
|
|
20
|
+
* This is the whole point of the tagged-template shape: the literal segments
|
|
21
|
+
* are frozen by the compiler (`TemplateStringsArray`), and the only way to
|
|
22
|
+
* get a runtime value into the query is via `${...}`, which we bind.
|
|
23
|
+
*
|
|
24
|
+
* 3. **Rows are returned as-is (no snake→camel mapping).** This matches the
|
|
25
|
+
* existing `client.raw()` behavior: a typed raw query is a literal escape
|
|
26
|
+
* hatch, so the result columns are whatever your `SELECT` names them. Alias
|
|
27
|
+
* columns in SQL (`SELECT created_at AS "createdAt"`) if you want camelCase.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // Awaited directly -> rows
|
|
32
|
+
* const rows = await db.sql<{ id: number; name: string }>`
|
|
33
|
+
* SELECT id, name FROM users WHERE org_id = ${orgId}
|
|
34
|
+
* `;
|
|
35
|
+
* // ^? { id: number; name: string }[]
|
|
36
|
+
*
|
|
37
|
+
* // .one() -> single row or null
|
|
38
|
+
* const user = await db.sql<{ id: number; name: string }>`
|
|
39
|
+
* SELECT id, name FROM users WHERE id = ${userId}
|
|
40
|
+
* `.one();
|
|
41
|
+
* // ^? { id: number; name: string } | null
|
|
42
|
+
*
|
|
43
|
+
* // .scalar() -> first column of first row, or null
|
|
44
|
+
* const total = await db.sql<{ count: number }>`
|
|
45
|
+
* SELECT COUNT(*)::int AS count FROM users WHERE org_id = ${orgId}
|
|
46
|
+
* `.scalar();
|
|
47
|
+
* // ^? number | null
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
import { ValidationError, wrapPgError } from './errors.js';
|
|
51
|
+
/**
|
|
52
|
+
* Build a `(sql, params)` pair from a tagged-template invocation.
|
|
53
|
+
*
|
|
54
|
+
* Each interpolated value is replaced by a positional `$N` placeholder and
|
|
55
|
+
* pushed to the params array in order. The static string segments are the only
|
|
56
|
+
* thing concatenated into the SQL text. This is the single point that
|
|
57
|
+
* guarantees parameterization for the entire typed-SQL surface.
|
|
58
|
+
*
|
|
59
|
+
* Exported for unit testing the parameterization invariant without a database.
|
|
60
|
+
*/
|
|
61
|
+
export function buildTypedSql(strings, values) {
|
|
62
|
+
// The tagged-template API guarantees `strings.length === values.length + 1`.
|
|
63
|
+
// Guard the directly-callable (exported) surface so a mismatched call can't
|
|
64
|
+
// silently desync `$N` placeholders from params (pg would reject it, but fail
|
|
65
|
+
// loudly here instead).
|
|
66
|
+
if (strings.length !== values.length + 1) {
|
|
67
|
+
throw new ValidationError(`[turbine] sql template segment/value count mismatch: ${strings.length} segments, ${values.length} values.`);
|
|
68
|
+
}
|
|
69
|
+
let sql = '';
|
|
70
|
+
for (let i = 0; i < strings.length; i++) {
|
|
71
|
+
sql += strings[i];
|
|
72
|
+
if (i < values.length) {
|
|
73
|
+
sql += `$${i + 1}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { sql, params: values.slice() };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A pending typed raw SQL query. Implements the thenable contract, so it can be
|
|
80
|
+
* `await`ed directly to get `T[]`, or refined via `.one()` / `.scalar()` first.
|
|
81
|
+
*
|
|
82
|
+
* The query is executed lazily and exactly once per terminal call (`then`,
|
|
83
|
+
* `one`, `scalar`). Each terminal method runs the query independently — this is
|
|
84
|
+
* an escape hatch, not a cached query object, so don't call two terminals on
|
|
85
|
+
* the same builder expecting a single round-trip; build a fresh template each
|
|
86
|
+
* time (the common pattern is `await db.sql\`...\`` inline).
|
|
87
|
+
*/
|
|
88
|
+
export class TypedSqlQuery {
|
|
89
|
+
pool;
|
|
90
|
+
sql;
|
|
91
|
+
params;
|
|
92
|
+
logging;
|
|
93
|
+
constructor(pool, sql, params, logging) {
|
|
94
|
+
this.pool = pool;
|
|
95
|
+
this.sql = sql;
|
|
96
|
+
this.params = params;
|
|
97
|
+
this.logging = logging;
|
|
98
|
+
}
|
|
99
|
+
/** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
|
|
100
|
+
async run() {
|
|
101
|
+
if (this.logging) {
|
|
102
|
+
console.log(`[turbine] Typed SQL: ${this.sql.trim().substring(0, 120)}...`);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const result = await this.pool.query(this.sql, this.params);
|
|
106
|
+
return result.rows;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
throw wrapPgError(err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* PromiseLike implementation: `await db.sql<T>\`...\`` resolves to `T[]`.
|
|
114
|
+
*/
|
|
115
|
+
// biome-ignore lint/suspicious/noThenProperty: intentional thenable — this IS the PromiseLike contract that makes `await db.sql\`...\`` resolve to rows
|
|
116
|
+
then(onfulfilled, onrejected) {
|
|
117
|
+
return this.run().then(onfulfilled, onrejected);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Execute and return the first row, or `null` if the query returns no rows.
|
|
121
|
+
* Use for queries you expect to match at most one row.
|
|
122
|
+
*/
|
|
123
|
+
async one() {
|
|
124
|
+
const rows = await this.run();
|
|
125
|
+
return rows.length > 0 ? rows[0] : null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Execute and return the first column of the first row, or `null` if there
|
|
129
|
+
* are no rows. Useful for `SELECT COUNT(*)`, `SELECT EXISTS(...)`, etc.
|
|
130
|
+
*
|
|
131
|
+
* The generic `V` defaults to the value type of `T`'s first property, but you
|
|
132
|
+
* can override it: `db.sql<{ count: number }>\`...\`.scalar<number>()`.
|
|
133
|
+
*/
|
|
134
|
+
async scalar() {
|
|
135
|
+
const rows = await this.run();
|
|
136
|
+
if (rows.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const first = rows[0];
|
|
139
|
+
const keys = Object.keys(first);
|
|
140
|
+
if (keys.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
return first[keys[0]];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=typed-sql.js.map
|
package/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations.
|
|
3
|
+
"version": "0.18.0",
|
|
4
|
+
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
8
9
|
"import": "./dist/index.js",
|
|
9
|
-
"require": "./dist/cjs/index.js"
|
|
10
|
-
"types": "./dist/index.d.ts"
|
|
10
|
+
"require": "./dist/cjs/index.js"
|
|
11
11
|
},
|
|
12
12
|
"./serverless": {
|
|
13
|
+
"types": "./dist/serverless.d.ts",
|
|
13
14
|
"import": "./dist/serverless.js",
|
|
14
|
-
"require": "./dist/cjs/serverless.js"
|
|
15
|
-
"types": "./dist/serverless.d.ts"
|
|
15
|
+
"require": "./dist/cjs/serverless.js"
|
|
16
16
|
},
|
|
17
17
|
"./cli": {
|
|
18
|
+
"types": "./dist/cli/config.d.ts",
|
|
18
19
|
"import": "./dist/cli/config.js",
|
|
19
|
-
"require": "./dist/cjs/cli/config.js"
|
|
20
|
-
"types": "./dist/cli/config.d.ts"
|
|
20
|
+
"require": "./dist/cjs/cli/config.js"
|
|
21
21
|
},
|
|
22
22
|
"./adapters": {
|
|
23
|
+
"types": "./dist/adapters/index.d.ts",
|
|
23
24
|
"import": "./dist/adapters/index.js",
|
|
24
|
-
"require": "./dist/cjs/adapters/index.js"
|
|
25
|
-
"types": "./dist/adapters/index.d.ts"
|
|
25
|
+
"require": "./dist/cjs/adapters/index.js"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"main": "./dist/cjs/index.js",
|
|
@@ -48,9 +48,10 @@
|
|
|
48
48
|
"generate": "tsx src/cli/index.ts generate",
|
|
49
49
|
"status": "tsx src/cli/index.ts status",
|
|
50
50
|
"examples": "tsx examples/examples.ts",
|
|
51
|
-
"
|
|
51
|
+
"dogfood": "tsx examples/dogfood.ts",
|
|
52
|
+
"test": "tsx --test --test-concurrency=1 src/test/*.test.ts",
|
|
52
53
|
"test:unit": "tsx --test src/test/schema-builder.test.ts src/test/errors.test.ts src/test/stress.test.ts src/test/migrate.test.ts src/test/update-operators.test.ts src/test/empty-where-guard.test.ts src/test/cli.test.ts src/test/serverless.test.ts src/test/pipeline.test.ts src/test/pipeline-submittable.test.ts src/test/with-inference.test.ts src/test/operator-validation.test.ts src/test/unlimited-warning.test.ts src/test/generate-relations.test.ts src/test/stream-and-parse.test.ts src/test/sql-cache.test.ts src/test/dialect.test.ts src/test/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts src/test/cockroachdb-adapter.test.ts src/test/yugabytedb-adapter.test.ts src/test/pg-compat.test.ts src/test/relation-filter-validation.test.ts src/test/client-coverage.test.ts src/test/schema-diff.test.ts src/test/composite-fk.test.ts src/test/retry.test.ts src/test/text-search.test.ts src/test/optimistic-lock.test.ts src/test/sql-safety-property.test.ts src/test/nested-write.test.ts src/test/nested-write-update-upsert.test.ts src/test/cursor-pagination.test.ts src/test/client-branches.test.ts src/test/is-isNot-filter.test.ts src/test/event-emitter.test.ts src/test/observe.test.ts",
|
|
53
|
-
"test:coverage": "c8 tsx --test
|
|
54
|
+
"test:coverage": "c8 tsx --test --test-concurrency=1 src/test/*.test.ts",
|
|
54
55
|
"lint": "biome check src/",
|
|
55
56
|
"lint:fix": "biome check --write src/",
|
|
56
57
|
"format": "biome format --write src/",
|
|
@@ -74,17 +75,18 @@
|
|
|
74
75
|
"node": ">=18.0.0"
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
78
|
+
"@types/pg": "^8.11.11",
|
|
77
79
|
"pg": "^8.13.1"
|
|
78
80
|
},
|
|
79
81
|
"devDependencies": {
|
|
80
82
|
"@biomejs/biome": "^2.4.10",
|
|
81
|
-
"@size-limit/
|
|
83
|
+
"@size-limit/esbuild": "^12.1.0",
|
|
84
|
+
"@size-limit/file": "^12.1.0",
|
|
82
85
|
"@types/node": "^22.10.0",
|
|
83
|
-
"@types/pg": "^8.11.11",
|
|
84
86
|
"c8": "^11.0.0",
|
|
85
87
|
"husky": "^9.1.7",
|
|
86
88
|
"lint-staged": "^16.4.0",
|
|
87
|
-
"size-limit": "^12.0
|
|
89
|
+
"size-limit": "^12.1.0",
|
|
88
90
|
"tsx": "^4.19.0",
|
|
89
91
|
"typescript": "^5.7.0"
|
|
90
92
|
},
|