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/dist/pipeline.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* turbine-orm — Pipeline execution
|
|
3
3
|
*
|
|
4
4
|
* Pipelines batch multiple independent queries into a single database round-trip.
|
|
5
5
|
* Instead of N sequential awaits (N round-trips), you get 1 round-trip for all N queries.
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* we simulate it by running queries concurrently on a single connection or via
|
|
16
16
|
* a multi-statement batch.
|
|
17
17
|
*/
|
|
18
|
+
import { wrapPgError } from './errors.js';
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Pipeline executor
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -52,7 +53,13 @@ export async function executePipeline(pool, queries) {
|
|
|
52
53
|
// Future: use actual Postgres pipeline protocol for true pipelining.
|
|
53
54
|
const results = [];
|
|
54
55
|
for (const q of queries) {
|
|
55
|
-
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await client.query(q.sql, q.params);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
throw wrapPgError(err);
|
|
62
|
+
}
|
|
56
63
|
results.push(q.transform(raw));
|
|
57
64
|
}
|
|
58
65
|
await client.query('COMMIT');
|
package/dist/query.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* turbine-orm — Query builder
|
|
3
3
|
*
|
|
4
4
|
* Each table accessor (db.users, db.posts, etc.) returns a QueryInterface<T>
|
|
5
5
|
* that builds parameterized SQL and executes it through the connection pool.
|
|
6
6
|
*
|
|
7
7
|
* Nested relations use json_build_object + json_agg subqueries for single-query
|
|
8
|
-
* resolution —
|
|
8
|
+
* resolution — a PostgreSQL-native approach that eliminates N+1 query patterns.
|
|
9
9
|
*
|
|
10
10
|
* Schema-driven: all column names, types, and relations come from introspected
|
|
11
11
|
* metadata — nothing is hardcoded.
|
|
@@ -61,10 +61,22 @@ export type WhereClause<T> = {
|
|
|
61
61
|
/** Relation filters — keyed by relation name, value is { some, every, none } */
|
|
62
62
|
[relationName: string]: unknown;
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Unparameterized with clause — accepts any relation name.
|
|
66
|
+
* Used internally by the query builder at runtime.
|
|
67
|
+
*/
|
|
64
68
|
export interface WithClause {
|
|
65
69
|
[relation: string]: true | WithOptions;
|
|
66
70
|
}
|
|
67
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Relation-aware with clause. When R (the relations map) is provided,
|
|
73
|
+
* only keys from R are autocompleted. Used in public method signatures
|
|
74
|
+
* so the compiler can narrow the return type.
|
|
75
|
+
*/
|
|
76
|
+
export type TypedWithClause<R extends object = {}> = [keyof R] extends [never] ? WithClause : {
|
|
77
|
+
[K in keyof R]?: true | WithOptions;
|
|
78
|
+
};
|
|
79
|
+
export interface WithOptions<_T = unknown> {
|
|
68
80
|
with?: WithClause;
|
|
69
81
|
where?: Record<string, unknown>;
|
|
70
82
|
orderBy?: Record<string, OrderDirection>;
|
|
@@ -74,22 +86,34 @@ export interface WithOptions {
|
|
|
74
86
|
/** Exclude these fields from the relation */
|
|
75
87
|
omit?: Record<string, boolean>;
|
|
76
88
|
}
|
|
77
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Compute the result type when relations are included via `with`.
|
|
91
|
+
*
|
|
92
|
+
* - When R is the default ({}) or W is empty, resolves to plain T.
|
|
93
|
+
* - When R is a real relations map and W specifies relation keys, merges matching
|
|
94
|
+
* relation types from R onto T.
|
|
95
|
+
*
|
|
96
|
+
* @typeParam T - Base entity type (e.g. User)
|
|
97
|
+
* @typeParam R - Relations map (e.g. { posts: Post[]; profile: Profile | null })
|
|
98
|
+
* @typeParam W - The with clause object (e.g. { posts: true })
|
|
99
|
+
*/
|
|
100
|
+
export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T : [keyof W & keyof R] extends [never] ? T : T & Pick<R, keyof W & keyof R>;
|
|
101
|
+
export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
|
|
78
102
|
where: WhereClause<T>;
|
|
79
103
|
select?: Record<string, boolean>;
|
|
80
104
|
omit?: Record<string, boolean>;
|
|
81
|
-
with?:
|
|
105
|
+
with?: W;
|
|
82
106
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
83
107
|
timeout?: number;
|
|
84
108
|
}
|
|
85
|
-
export interface FindManyArgs<T> {
|
|
109
|
+
export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
|
|
86
110
|
where?: WhereClause<T>;
|
|
87
111
|
select?: Record<string, boolean>;
|
|
88
112
|
omit?: Record<string, boolean>;
|
|
89
113
|
orderBy?: Record<string, OrderDirection>;
|
|
90
114
|
limit?: number;
|
|
91
115
|
offset?: number;
|
|
92
|
-
with?:
|
|
116
|
+
with?: W;
|
|
93
117
|
/** Cursor-based pagination: start after this row */
|
|
94
118
|
cursor?: Partial<T>;
|
|
95
119
|
/** Number of records to take (used with cursor) */
|
|
@@ -99,6 +123,10 @@ export interface FindManyArgs<T> {
|
|
|
99
123
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
100
124
|
timeout?: number;
|
|
101
125
|
}
|
|
126
|
+
export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> extends FindManyArgs<T, R, W> {
|
|
127
|
+
/** Number of rows to fetch per internal FETCH batch (default: 100) */
|
|
128
|
+
batchSize?: number;
|
|
129
|
+
}
|
|
102
130
|
export interface CreateArgs<T> {
|
|
103
131
|
data: Partial<T>;
|
|
104
132
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
@@ -111,27 +139,71 @@ export interface CreateManyArgs<T> {
|
|
|
111
139
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
112
140
|
timeout?: number;
|
|
113
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Atomic update operators for a field.
|
|
144
|
+
*
|
|
145
|
+
* `set` works on any type; `increment`, `decrement`, `multiply`, and `divide`
|
|
146
|
+
* are only valid on numeric fields. They generate SQL like
|
|
147
|
+
* `col = col + $n` (and the corresponding `-`, `*`, `/` variants) instead of
|
|
148
|
+
* plain absolute assignments, so they are safe against concurrent writers —
|
|
149
|
+
* the database performs the math atomically.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* db.posts.update({ where: { id: 5 }, data: { viewCount: { increment: 1 } } });
|
|
153
|
+
*/
|
|
154
|
+
export type UpdateOperatorInput<V> = {
|
|
155
|
+
set: V;
|
|
156
|
+
} | (V extends number ? {
|
|
157
|
+
increment: number;
|
|
158
|
+
} : never) | (V extends number ? {
|
|
159
|
+
decrement: number;
|
|
160
|
+
} : never) | (V extends number ? {
|
|
161
|
+
multiply: number;
|
|
162
|
+
} : never) | (V extends number ? {
|
|
163
|
+
divide: number;
|
|
164
|
+
} : never);
|
|
165
|
+
/**
|
|
166
|
+
* Update data — each field can be a plain value or an atomic operator object.
|
|
167
|
+
* Back-compatible with `Partial<T>`: plain values still typecheck unchanged.
|
|
168
|
+
*/
|
|
169
|
+
export type UpdateInput<T> = {
|
|
170
|
+
[K in keyof T]?: T[K] | UpdateOperatorInput<T[K]>;
|
|
171
|
+
};
|
|
114
172
|
export interface UpdateArgs<T> {
|
|
115
173
|
where: WhereClause<T>;
|
|
116
|
-
data:
|
|
174
|
+
data: UpdateInput<T>;
|
|
117
175
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
118
176
|
timeout?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Opt in to running this mutation when `where` resolves to an empty
|
|
179
|
+
* predicate (e.g. `{}` or `{ id: undefined }`). Default `false` — an
|
|
180
|
+
* empty predicate throws `ValidationError` to catch the common case of
|
|
181
|
+
* a filter value accidentally being `undefined`. Set this to `true` only
|
|
182
|
+
* when an unconditional mutation is the intended behaviour.
|
|
183
|
+
*/
|
|
184
|
+
allowFullTableScan?: boolean;
|
|
119
185
|
}
|
|
120
186
|
export interface UpdateManyArgs<T> {
|
|
121
187
|
where: WhereClause<T>;
|
|
122
|
-
data:
|
|
188
|
+
data: UpdateInput<T>;
|
|
123
189
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
124
190
|
timeout?: number;
|
|
191
|
+
/** See {@link UpdateArgs.allowFullTableScan}. */
|
|
192
|
+
allowFullTableScan?: boolean;
|
|
125
193
|
}
|
|
126
194
|
export interface DeleteArgs<T> {
|
|
127
195
|
where: WhereClause<T>;
|
|
128
196
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
129
197
|
timeout?: number;
|
|
198
|
+
/** See {@link UpdateArgs.allowFullTableScan}. */
|
|
199
|
+
allowFullTableScan?: boolean;
|
|
130
200
|
}
|
|
131
201
|
export interface DeleteManyArgs<T> {
|
|
132
202
|
where: WhereClause<T>;
|
|
133
203
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
134
204
|
timeout?: number;
|
|
205
|
+
/** See {@link UpdateArgs.allowFullTableScan}. */
|
|
206
|
+
allowFullTableScan?: boolean;
|
|
135
207
|
}
|
|
136
208
|
export interface UpsertArgs<T> {
|
|
137
209
|
where: WhereClause<T>;
|
|
@@ -158,8 +230,6 @@ export interface GroupByArgs<T> {
|
|
|
158
230
|
_min?: Partial<Record<keyof T & string, boolean>>;
|
|
159
231
|
/** Maximum value of fields in each group */
|
|
160
232
|
_max?: Partial<Record<keyof T & string, boolean>>;
|
|
161
|
-
/** Having clause for filtering groups */
|
|
162
|
-
having?: Record<string, unknown>;
|
|
163
233
|
/** Order groups */
|
|
164
234
|
orderBy?: Record<string, OrderDirection>;
|
|
165
235
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
@@ -244,7 +314,7 @@ export interface QueryInterfaceOptions {
|
|
|
244
314
|
/** Log a warning when findMany() is called without a limit */
|
|
245
315
|
warnOnUnlimited?: boolean;
|
|
246
316
|
}
|
|
247
|
-
export declare class QueryInterface<T extends object> {
|
|
317
|
+
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
248
318
|
private readonly pool;
|
|
249
319
|
private readonly table;
|
|
250
320
|
private readonly schema;
|
|
@@ -261,6 +331,7 @@ export declare class QueryInterface<T extends object> {
|
|
|
261
331
|
/**
|
|
262
332
|
* Execute a pool.query with an optional timeout.
|
|
263
333
|
* If timeout is set, races the query against a timer and rejects on expiry.
|
|
334
|
+
* pg driver errors are translated to typed Turbine errors via wrapPgError.
|
|
264
335
|
*/
|
|
265
336
|
private queryWithTimeout;
|
|
266
337
|
/**
|
|
@@ -273,21 +344,32 @@ export declare class QueryInterface<T extends object> {
|
|
|
273
344
|
* To intercept queries before SQL generation, use the raw() method instead.
|
|
274
345
|
*/
|
|
275
346
|
private executeWithMiddleware;
|
|
347
|
+
findUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
|
|
348
|
+
buildFindUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T | null>;
|
|
349
|
+
findMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>[]>;
|
|
350
|
+
buildFindMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T[]>;
|
|
276
351
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
352
|
+
* Stream rows from a findMany query using PostgreSQL cursors.
|
|
353
|
+
* Returns an AsyncIterable that yields individual rows, fetching in batches internally.
|
|
354
|
+
*
|
|
355
|
+
* Uses DECLARE CURSOR within a dedicated transaction on a single pooled connection.
|
|
356
|
+
* The cursor is automatically closed and the connection released when iteration
|
|
357
|
+
* completes or is terminated early (e.g. `break` from `for await`).
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* for await (const user of db.users.findManyStream({ where: { orgId: 1 }, batchSize: 500 })) {
|
|
362
|
+
* process.stdout.write(`${user.email}\n`);
|
|
363
|
+
* }
|
|
364
|
+
* ```
|
|
279
365
|
*/
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
findFirstOrThrow(args?: FindManyArgs<T>): Promise<T>;
|
|
288
|
-
buildFindFirstOrThrow(args?: FindManyArgs<T>): DeferredQuery<T>;
|
|
289
|
-
findUniqueOrThrow(args: FindUniqueArgs<T>): Promise<T>;
|
|
290
|
-
buildFindUniqueOrThrow(args: FindUniqueArgs<T>): DeferredQuery<T>;
|
|
366
|
+
findManyStream<W extends TypedWithClause<R> = {}>(args?: FindManyStreamArgs<T, R, W>): AsyncGenerator<WithResult<T, R, W>, void, undefined>;
|
|
367
|
+
findFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
|
|
368
|
+
buildFindFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T | null>;
|
|
369
|
+
findFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>>;
|
|
370
|
+
buildFindFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T>;
|
|
371
|
+
findUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W>>;
|
|
372
|
+
buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T>;
|
|
291
373
|
create(args: CreateArgs<T>): Promise<T>;
|
|
292
374
|
buildCreate(args: CreateArgs<T>): DeferredQuery<T>;
|
|
293
375
|
createMany(args: CreateManyArgs<T>): Promise<T[]>;
|
|
@@ -325,8 +407,33 @@ export declare class QueryInterface<T extends object> {
|
|
|
325
407
|
private toColumn;
|
|
326
408
|
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
327
409
|
private toSqlColumn;
|
|
410
|
+
/**
|
|
411
|
+
* Build a single SET clause entry for update/updateMany.
|
|
412
|
+
*
|
|
413
|
+
* Supports plain values and atomic operator objects ({ set, increment,
|
|
414
|
+
* decrement, multiply, divide }). An operator object is detected ONLY when
|
|
415
|
+
* it has EXACTLY one key that is one of the 5 operator keys — this avoids
|
|
416
|
+
* misinterpreting JSON column values like `{ set: 'x' }` as operators
|
|
417
|
+
* (real operator objects always have exactly one key, and a plain JSON
|
|
418
|
+
* payload that happens to have a single `set` key is extremely unusual).
|
|
419
|
+
* Multi-key objects are always treated as plain (JSON) values.
|
|
420
|
+
*
|
|
421
|
+
* Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
|
|
422
|
+
* pushes any required params onto the shared params array so that WHERE
|
|
423
|
+
* clause numbering continues correctly afterward.
|
|
424
|
+
*/
|
|
425
|
+
private buildSetClause;
|
|
328
426
|
/** Build WHERE clause from a where object (supports operators, NULL, OR) */
|
|
329
427
|
private buildWhere;
|
|
428
|
+
/**
|
|
429
|
+
* Refuse mutations with an empty predicate unless explicitly opted in.
|
|
430
|
+
*
|
|
431
|
+
* An empty `where` (e.g. `{}` or `{ id: undefined }`) resolves to a
|
|
432
|
+
* mutation with no filter — a common footgun when a caller's filter
|
|
433
|
+
* value accidentally resolves to `undefined`. This guard throws
|
|
434
|
+
* `ValidationError` in that case unless `allowFullTableScan: true`.
|
|
435
|
+
*/
|
|
436
|
+
private assertMutationHasPredicate;
|
|
330
437
|
/**
|
|
331
438
|
* Build the inner WHERE expression (without the WHERE keyword).
|
|
332
439
|
* Returns null if no conditions exist.
|
|
@@ -355,24 +462,138 @@ export declare class QueryInterface<T extends object> {
|
|
|
355
462
|
/** Parse a row that may contain JSON nested relation columns */
|
|
356
463
|
private parseNestedRow;
|
|
357
464
|
/**
|
|
358
|
-
* Build a SELECT clause
|
|
465
|
+
* Build a SELECT clause that includes both base columns and nested relation subqueries.
|
|
466
|
+
*
|
|
467
|
+
* For each relation specified in the `with` clause, this method generates a correlated
|
|
468
|
+
* subquery using PostgreSQL's `json_agg(json_build_object(...))` pattern. The result
|
|
469
|
+
* is a single SQL SELECT clause that resolves the full object tree in one query --
|
|
470
|
+
* no N+1 problem.
|
|
471
|
+
*
|
|
472
|
+
* **How it works:**
|
|
473
|
+
* 1. Resolves the base columns for the root table (all columns, or a subset via `columnsList`).
|
|
474
|
+
* 2. Iterates over each key in the `with` clause, looking up the relation definition.
|
|
475
|
+
* 3. For each relation, delegates to {@link buildRelationSubquery} to generate a
|
|
476
|
+
* correlated subquery that returns JSON (array for hasMany, object for belongsTo/hasOne).
|
|
477
|
+
* 4. Each subquery is aliased as the relation name in the final SELECT.
|
|
359
478
|
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
479
|
+
* **aliasCounter:** A shared `{ n: number }` object is passed through all nesting levels.
|
|
480
|
+
* Each call to `buildRelationSubquery` increments it to produce unique table aliases
|
|
481
|
+
* (`t0`, `t1`, `t2`, ...) across arbitrarily deep relation trees, preventing alias
|
|
482
|
+
* collisions in the generated SQL.
|
|
362
483
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
484
|
+
* **Example output:**
|
|
485
|
+
* ```sql
|
|
486
|
+
* "users"."id", "users"."name", "users"."email",
|
|
487
|
+
* (SELECT COALESCE(json_agg(json_build_object('id', t0."id", 'title', t0."title")), '[]'::json)
|
|
488
|
+
* FROM "posts" t0 WHERE t0."user_id" = "users"."id") AS "posts"
|
|
489
|
+
* ```
|
|
490
|
+
*
|
|
491
|
+
* @param table - The root table name (e.g. `"users"`).
|
|
492
|
+
* @param withClause - An object mapping relation names to their include specs
|
|
493
|
+
* (`true` for default inclusion, or `WithOptions` for select/omit/where/orderBy/limit).
|
|
494
|
+
* @param params - Shared parameter array for parameterized values (`$1`, `$2`, ...).
|
|
495
|
+
* Nested where/limit values are pushed here to prevent SQL injection.
|
|
496
|
+
* @param columnsList - Optional subset of columns to include in the SELECT. When `null`
|
|
497
|
+
* or omitted, all columns from the table's schema metadata are used.
|
|
498
|
+
* @param depth - Current nesting depth, passed through to {@link buildRelationSubquery}
|
|
499
|
+
* for circular-relation detection. Defaults to `0` at the top level.
|
|
500
|
+
* @param path - Breadcrumb trail of relation names traversed so far, used in error
|
|
501
|
+
* messages when circular or too-deep nesting is detected.
|
|
502
|
+
* @returns A complete SELECT clause string (without the `SELECT` keyword) containing
|
|
503
|
+
* base columns and relation subqueries.
|
|
365
504
|
*/
|
|
366
505
|
private buildSelectWithRelations;
|
|
367
506
|
/**
|
|
368
|
-
*
|
|
507
|
+
* Generate a correlated subquery that returns JSON for a single relation.
|
|
508
|
+
*
|
|
509
|
+
* This is the core of Turbine's single-query nested relation strategy. For a given
|
|
510
|
+
* relation (e.g. `posts` on a `users` query), it produces a self-contained SQL subquery
|
|
511
|
+
* that PostgreSQL evaluates per parent row, returning either a JSON array (hasMany) or
|
|
512
|
+
* a single JSON object (belongsTo / hasOne).
|
|
513
|
+
*
|
|
514
|
+
* ### Algorithm overview
|
|
515
|
+
*
|
|
516
|
+
* 1. **Alias generation:** Allocates a unique alias (`t0`, `t1`, ...) from the shared
|
|
517
|
+
* `aliasCounter` so that deeply nested subqueries never collide.
|
|
518
|
+
*
|
|
519
|
+
* 2. **Column resolution:** Honors `select` / `omit` options to control which columns
|
|
520
|
+
* appear in the output JSON.
|
|
521
|
+
*
|
|
522
|
+
* 3. **`json_build_object`:** Builds a JSON object for each row by mapping camelCase
|
|
523
|
+
* field names to their column values:
|
|
524
|
+
* ```sql
|
|
525
|
+
* json_build_object('id', t0."id", 'title', t0."title", 'createdAt', t0."created_at")
|
|
526
|
+
* ```
|
|
527
|
+
*
|
|
528
|
+
* 4. **`json_agg` wrapping (hasMany):** For one-to-many relations, wraps the
|
|
529
|
+
* `json_build_object` call in `json_agg(...)` to aggregate all matching child rows
|
|
530
|
+
* into a JSON array. Uses `COALESCE(..., '[]'::json)` so the result is never NULL.
|
|
531
|
+
* For belongsTo / hasOne, no aggregation is used -- just the single JSON object
|
|
532
|
+
* with `LIMIT 1`.
|
|
533
|
+
*
|
|
534
|
+
* 5. **Correlation (WHERE clause):** Links the subquery to the parent row:
|
|
535
|
+
* - **hasMany:** `alias.foreignKey = parentRef.referenceKey`
|
|
536
|
+
* (e.g. `t0."user_id" = "users"."id"` -- child FK points to parent PK)
|
|
537
|
+
* - **belongsTo / hasOne:** `alias.referenceKey = parentRef.foreignKey`
|
|
538
|
+
* (e.g. `t0."id" = "posts"."author_id"` -- parent FK points to child PK)
|
|
539
|
+
*
|
|
540
|
+
* 6. **Recursion:** If the spec includes a nested `with` clause, this method calls
|
|
541
|
+
* itself recursively for each nested relation, passing the current alias as
|
|
542
|
+
* `parentRef`. The nested subquery appears as an additional key in the
|
|
543
|
+
* `json_build_object` call, wrapped in `COALESCE(..., '[]'::json)`.
|
|
544
|
+
* Depth is incremented and capped at 10 to guard against circular relations.
|
|
545
|
+
*
|
|
546
|
+
* 7. **LIMIT / ORDER BY wrapping:** For hasMany relations with `limit` or `orderBy`,
|
|
547
|
+
* the query is restructured into a two-level form:
|
|
548
|
+
* ```sql
|
|
549
|
+
* SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
|
|
550
|
+
* FROM (
|
|
551
|
+
* SELECT t0.* FROM "posts" t0
|
|
552
|
+
* WHERE t0."user_id" = "users"."id"
|
|
553
|
+
* ORDER BY t0."created_at" DESC
|
|
554
|
+
* LIMIT $1
|
|
555
|
+
* ) t0i
|
|
556
|
+
* ```
|
|
557
|
+
* This ensures LIMIT and ORDER BY apply to the raw rows *before* `json_agg`
|
|
558
|
+
* aggregation. Without the inner subquery, LIMIT would be meaningless because
|
|
559
|
+
* `json_agg` produces a single aggregated row.
|
|
560
|
+
*
|
|
561
|
+
* 8. **Parameter threading:** All user-supplied values (where filters, limit) are
|
|
562
|
+
* pushed to the shared `params` array with `$N` placeholders. No string
|
|
563
|
+
* interpolation of user data ever occurs -- all identifiers go through
|
|
564
|
+
* `quoteIdent()` and all values are parameterized.
|
|
369
565
|
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
566
|
+
* ### Example output (hasMany with nested relation)
|
|
567
|
+
* ```sql
|
|
568
|
+
* SELECT COALESCE(json_agg(json_build_object(
|
|
569
|
+
* 'id', t0."id",
|
|
570
|
+
* 'title', t0."title",
|
|
571
|
+
* 'comments', COALESCE((
|
|
572
|
+
* SELECT COALESCE(json_agg(json_build_object('id', t1."id", 'body', t1."body")), '[]'::json)
|
|
573
|
+
* FROM "comments" t1 WHERE t1."post_id" = t0."id"
|
|
574
|
+
* ), '[]'::json)
|
|
575
|
+
* )), '[]'::json) FROM "posts" t0 WHERE t0."user_id" = "users"."id"
|
|
576
|
+
* ```
|
|
372
577
|
*
|
|
373
|
-
* @param
|
|
374
|
-
*
|
|
375
|
-
* @param
|
|
578
|
+
* @param relDef - The relation definition from schema metadata (contains `to`, `type`,
|
|
579
|
+
* `foreignKey`, `referenceKey`).
|
|
580
|
+
* @param spec - Either `true` (include with defaults) or a `WithOptions` object that
|
|
581
|
+
* can specify `select`, `omit`, `where`, `orderBy`, `limit`, and nested `with`.
|
|
582
|
+
* @param params - Shared parameter array. User-supplied values are pushed here and
|
|
583
|
+
* referenced as `$1`, `$2`, etc. in the generated SQL.
|
|
584
|
+
* @param parentRef - The alias (e.g. `"t0"`) or table name (e.g. `"users"`) of the
|
|
585
|
+
* parent query. Used to build the correlated WHERE clause that ties
|
|
586
|
+
* child rows to their parent row.
|
|
587
|
+
* @param aliasCounter - Shared mutable counter (`{ n: number }`) for generating unique
|
|
588
|
+
* table aliases (`t0`, `t1`, `t2`, ...) across all nesting levels.
|
|
589
|
+
* Each call increments `n` by 1.
|
|
590
|
+
* @param depth - Current nesting depth (starts at `0`). Incremented on each recursive
|
|
591
|
+
* call. If it reaches 10, a {@link CircularRelationError} is thrown.
|
|
592
|
+
* @param path - Breadcrumb trail of relation/table names traversed so far
|
|
593
|
+
* (e.g. `["users", "posts", "comments"]`). Used in the error message
|
|
594
|
+
* when circular or too-deep nesting is detected.
|
|
595
|
+
* @returns A complete SQL subquery string (without surrounding parentheses) that
|
|
596
|
+
* evaluates to a JSON array (hasMany) or a JSON object (belongsTo/hasOne).
|
|
376
597
|
*/
|
|
377
598
|
private buildRelationSubquery;
|
|
378
599
|
/**
|