turbine-orm 0.4.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 +243 -26
- package/dist/cjs/cli/config.js +151 -0
- package/dist/cjs/cli/index.js +1176 -0
- package/dist/cjs/cli/migrate.js +446 -0
- package/dist/cjs/cli/ui.js +233 -0
- package/dist/cjs/client.js +512 -0
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +321 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/introspect.js +287 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +78 -0
- package/dist/cjs/query.js +1891 -0
- package/dist/cjs/schema-builder.js +238 -0
- package/dist/cjs/schema-sql.js +509 -0
- package/dist/cjs/schema.js +140 -0
- package/dist/cjs/serverless.js +110 -0
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +256 -49
- package/dist/cli/migrate.d.ts +35 -6
- package/dist/cli/migrate.js +124 -76
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +87 -3
- package/dist/client.js +122 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.js +37 -11
- package/dist/index.d.ts +10 -8
- package/dist/index.js +15 -11
- package/dist/introspect.js +3 -5
- package/dist/pipeline.js +8 -1
- package/dist/query.d.ts +310 -45
- package/dist/query.js +565 -237
- package/dist/schema-builder.js +91 -23
- package/dist/schema-sql.d.ts +6 -2
- package/dist/schema-sql.js +180 -26
- package/dist/schema.js +4 -1
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +44 -21
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -126
package/dist/query.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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.
|
|
@@ -35,6 +35,8 @@ export interface WhereOperator<V = unknown> {
|
|
|
35
35
|
contains?: string;
|
|
36
36
|
startsWith?: string;
|
|
37
37
|
endsWith?: string;
|
|
38
|
+
/** Set to 'insensitive' to use ILIKE instead of LIKE for string comparisons */
|
|
39
|
+
mode?: 'default' | 'insensitive';
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* A where value can be:
|
|
@@ -59,10 +61,22 @@ export type WhereClause<T> = {
|
|
|
59
61
|
/** Relation filters — keyed by relation name, value is { some, every, none } */
|
|
60
62
|
[relationName: string]: unknown;
|
|
61
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Unparameterized with clause — accepts any relation name.
|
|
66
|
+
* Used internally by the query builder at runtime.
|
|
67
|
+
*/
|
|
62
68
|
export interface WithClause {
|
|
63
69
|
[relation: string]: true | WithOptions;
|
|
64
70
|
}
|
|
65
|
-
|
|
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> {
|
|
66
80
|
with?: WithClause;
|
|
67
81
|
where?: Record<string, unknown>;
|
|
68
82
|
orderBy?: Record<string, OrderDirection>;
|
|
@@ -72,56 +86,136 @@ export interface WithOptions {
|
|
|
72
86
|
/** Exclude these fields from the relation */
|
|
73
87
|
omit?: Record<string, boolean>;
|
|
74
88
|
}
|
|
75
|
-
|
|
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>> {
|
|
76
102
|
where: WhereClause<T>;
|
|
77
103
|
select?: Record<string, boolean>;
|
|
78
104
|
omit?: Record<string, boolean>;
|
|
79
|
-
with?:
|
|
105
|
+
with?: W;
|
|
106
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
107
|
+
timeout?: number;
|
|
80
108
|
}
|
|
81
|
-
export interface FindManyArgs<T> {
|
|
109
|
+
export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
|
|
82
110
|
where?: WhereClause<T>;
|
|
83
111
|
select?: Record<string, boolean>;
|
|
84
112
|
omit?: Record<string, boolean>;
|
|
85
113
|
orderBy?: Record<string, OrderDirection>;
|
|
86
114
|
limit?: number;
|
|
87
115
|
offset?: number;
|
|
88
|
-
with?:
|
|
116
|
+
with?: W;
|
|
89
117
|
/** Cursor-based pagination: start after this row */
|
|
90
118
|
cursor?: Partial<T>;
|
|
91
119
|
/** Number of records to take (used with cursor) */
|
|
92
120
|
take?: number;
|
|
93
121
|
/** De-duplicate results by specified fields */
|
|
94
122
|
distinct?: (keyof T & string)[];
|
|
123
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
124
|
+
timeout?: number;
|
|
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;
|
|
95
129
|
}
|
|
96
130
|
export interface CreateArgs<T> {
|
|
97
131
|
data: Partial<T>;
|
|
132
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
133
|
+
timeout?: number;
|
|
98
134
|
}
|
|
99
135
|
export interface CreateManyArgs<T> {
|
|
100
136
|
data: Partial<T>[];
|
|
101
137
|
/** When true, adds ON CONFLICT DO NOTHING to skip duplicate rows */
|
|
102
138
|
skipDuplicates?: boolean;
|
|
139
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
140
|
+
timeout?: number;
|
|
103
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
|
+
};
|
|
104
172
|
export interface UpdateArgs<T> {
|
|
105
173
|
where: WhereClause<T>;
|
|
106
|
-
data:
|
|
174
|
+
data: UpdateInput<T>;
|
|
175
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
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;
|
|
107
185
|
}
|
|
108
186
|
export interface UpdateManyArgs<T> {
|
|
109
187
|
where: WhereClause<T>;
|
|
110
|
-
data:
|
|
188
|
+
data: UpdateInput<T>;
|
|
189
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
190
|
+
timeout?: number;
|
|
191
|
+
/** See {@link UpdateArgs.allowFullTableScan}. */
|
|
192
|
+
allowFullTableScan?: boolean;
|
|
111
193
|
}
|
|
112
194
|
export interface DeleteArgs<T> {
|
|
113
195
|
where: WhereClause<T>;
|
|
196
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
197
|
+
timeout?: number;
|
|
198
|
+
/** See {@link UpdateArgs.allowFullTableScan}. */
|
|
199
|
+
allowFullTableScan?: boolean;
|
|
114
200
|
}
|
|
115
201
|
export interface DeleteManyArgs<T> {
|
|
116
202
|
where: WhereClause<T>;
|
|
203
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
204
|
+
timeout?: number;
|
|
205
|
+
/** See {@link UpdateArgs.allowFullTableScan}. */
|
|
206
|
+
allowFullTableScan?: boolean;
|
|
117
207
|
}
|
|
118
208
|
export interface UpsertArgs<T> {
|
|
119
209
|
where: WhereClause<T>;
|
|
120
210
|
create: Partial<T>;
|
|
121
211
|
update: Partial<T>;
|
|
212
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
213
|
+
timeout?: number;
|
|
122
214
|
}
|
|
123
215
|
export interface CountArgs<T> {
|
|
124
216
|
where?: WhereClause<T>;
|
|
217
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
218
|
+
timeout?: number;
|
|
125
219
|
}
|
|
126
220
|
export interface GroupByArgs<T> {
|
|
127
221
|
by: (keyof T & string)[];
|
|
@@ -136,10 +230,10 @@ export interface GroupByArgs<T> {
|
|
|
136
230
|
_min?: Partial<Record<keyof T & string, boolean>>;
|
|
137
231
|
/** Maximum value of fields in each group */
|
|
138
232
|
_max?: Partial<Record<keyof T & string, boolean>>;
|
|
139
|
-
/** Having clause for filtering groups */
|
|
140
|
-
having?: Record<string, unknown>;
|
|
141
233
|
/** Order groups */
|
|
142
234
|
orderBy?: Record<string, OrderDirection>;
|
|
235
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
236
|
+
timeout?: number;
|
|
143
237
|
}
|
|
144
238
|
/** Arguments for the standalone aggregate method */
|
|
145
239
|
export interface AggregateArgs<T> {
|
|
@@ -154,6 +248,8 @@ export interface AggregateArgs<T> {
|
|
|
154
248
|
_min?: Partial<Record<keyof T & string, boolean>>;
|
|
155
249
|
/** Maximum value of fields */
|
|
156
250
|
_max?: Partial<Record<keyof T & string, boolean>>;
|
|
251
|
+
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
252
|
+
timeout?: number;
|
|
157
253
|
}
|
|
158
254
|
/** Result type for aggregate queries */
|
|
159
255
|
export interface AggregateResult<T> {
|
|
@@ -211,7 +307,14 @@ type MiddlewareFn = (params: {
|
|
|
211
307
|
action: string;
|
|
212
308
|
args: Record<string, unknown>;
|
|
213
309
|
}) => Promise<unknown>) => Promise<unknown>;
|
|
214
|
-
|
|
310
|
+
/** Options passed from TurbineClient to QueryInterface */
|
|
311
|
+
export interface QueryInterfaceOptions {
|
|
312
|
+
/** Default LIMIT applied to findMany() when no limit is specified */
|
|
313
|
+
defaultLimit?: number;
|
|
314
|
+
/** Log a warning when findMany() is called without a limit */
|
|
315
|
+
warnOnUnlimited?: boolean;
|
|
316
|
+
}
|
|
317
|
+
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
215
318
|
private readonly pool;
|
|
216
319
|
private readonly table;
|
|
217
320
|
private readonly schema;
|
|
@@ -219,28 +322,54 @@ export declare class QueryInterface<T extends object> {
|
|
|
219
322
|
/** SQL template cache: cacheKey → sql string (params are always positional $1,$2,...) */
|
|
220
323
|
private readonly sqlCache;
|
|
221
324
|
private readonly middlewares;
|
|
222
|
-
private readonly
|
|
223
|
-
|
|
325
|
+
private readonly defaultLimit?;
|
|
326
|
+
private readonly warnOnUnlimited;
|
|
327
|
+
/** Pre-computed column type lookups (avoids linear scans per query) */
|
|
328
|
+
private readonly columnPgTypeMap;
|
|
329
|
+
private readonly columnArrayTypeMap;
|
|
330
|
+
constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
|
|
331
|
+
/**
|
|
332
|
+
* Execute a pool.query with an optional timeout.
|
|
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.
|
|
335
|
+
*/
|
|
336
|
+
private queryWithTimeout;
|
|
224
337
|
/**
|
|
225
338
|
* Execute a query through the middleware chain.
|
|
226
339
|
* If no middlewares are registered, executes directly.
|
|
340
|
+
*
|
|
341
|
+
* Middleware can inspect and log query parameters, modify results after execution,
|
|
342
|
+
* and measure timing. Note: query SQL is generated before middleware runs, so
|
|
343
|
+
* modifying params.args in middleware will NOT affect the executed SQL.
|
|
344
|
+
* To intercept queries before SQL generation, use the raw() method instead.
|
|
227
345
|
*/
|
|
228
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[]>;
|
|
229
351
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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
|
+
* ```
|
|
232
365
|
*/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
findFirstOrThrow(args?: FindManyArgs<T>): Promise<T>;
|
|
241
|
-
buildFindFirstOrThrow(args?: FindManyArgs<T>): DeferredQuery<T>;
|
|
242
|
-
findUniqueOrThrow(args: FindUniqueArgs<T>): Promise<T>;
|
|
243
|
-
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>;
|
|
244
373
|
create(args: CreateArgs<T>): Promise<T>;
|
|
245
374
|
buildCreate(args: CreateArgs<T>): DeferredQuery<T>;
|
|
246
375
|
createMany(args: CreateManyArgs<T>): Promise<T[]>;
|
|
@@ -278,8 +407,33 @@ export declare class QueryInterface<T extends object> {
|
|
|
278
407
|
private toColumn;
|
|
279
408
|
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
280
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;
|
|
281
426
|
/** Build WHERE clause from a where object (supports operators, NULL, OR) */
|
|
282
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;
|
|
283
437
|
/**
|
|
284
438
|
* Build the inner WHERE expression (without the WHERE keyword).
|
|
285
439
|
* Returns null if no conditions exist.
|
|
@@ -305,39 +459,147 @@ export declare class QueryInterface<T extends object> {
|
|
|
305
459
|
private buildOrderBy;
|
|
306
460
|
/** Parse a flat row: convert snake_case to camelCase + Date coercion */
|
|
307
461
|
private parseRow;
|
|
308
|
-
/**
|
|
309
|
-
* Fast path: parse a flat row when the table has NO date columns.
|
|
310
|
-
* Only renames snake_case -> camelCase via the pre-computed reverseMap.
|
|
311
|
-
* Skips all date coercion checks — avoids Set.has() per field per row.
|
|
312
|
-
* Used by findUnique/findMany fast paths when hasNoDateColumns is true.
|
|
313
|
-
*/
|
|
314
|
-
private parseRowFast;
|
|
315
462
|
/** Parse a row that may contain JSON nested relation columns */
|
|
316
463
|
private parseNestedRow;
|
|
317
464
|
/**
|
|
318
|
-
* 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.
|
|
319
478
|
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
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.
|
|
322
483
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
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.
|
|
325
504
|
*/
|
|
326
505
|
private buildSelectWithRelations;
|
|
327
506
|
/**
|
|
328
|
-
*
|
|
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`.
|
|
329
533
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
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)
|
|
332
539
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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.
|
|
565
|
+
*
|
|
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
|
+
* ```
|
|
577
|
+
*
|
|
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).
|
|
336
597
|
*/
|
|
337
598
|
private buildRelationSubquery;
|
|
338
599
|
/**
|
|
339
600
|
* Get the Postgres type for a column (e.g. 'jsonb', 'text', '_int4').
|
|
340
601
|
* Used to detect JSONB/array columns for specialized operators.
|
|
602
|
+
* Uses pre-computed Map for O(1) lookup instead of linear scan.
|
|
341
603
|
*/
|
|
342
604
|
private getColumnPgType;
|
|
343
605
|
/**
|
|
@@ -355,7 +617,10 @@ export declare class QueryInterface<T extends object> {
|
|
|
355
617
|
* Supports: has, hasEvery, hasSome, isEmpty.
|
|
356
618
|
*/
|
|
357
619
|
private buildArrayFilterClauses;
|
|
358
|
-
/**
|
|
620
|
+
/**
|
|
621
|
+
* Get the Postgres array type for a column (used by UNNEST in createMany).
|
|
622
|
+
* Uses pre-computed Map for O(1) lookup instead of linear scan.
|
|
623
|
+
*/
|
|
359
624
|
private getColumnArrayType;
|
|
360
625
|
}
|
|
361
626
|
export {};
|