turbine-orm 0.16.0 → 0.19.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-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +35 -73
- 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 +581 -17
- 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-ui.generated.js +1 -1
- package/dist/cli/studio.js +35 -73
- 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 +582 -18
- 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
package/dist/generate.js
CHANGED
|
@@ -152,7 +152,8 @@ export function generateTypes(schema) {
|
|
|
152
152
|
lines.push(`export interface ${typeName}Relations {`);
|
|
153
153
|
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
154
154
|
const targetType = entityName(rel.to);
|
|
155
|
-
|
|
155
|
+
// manyToMany is a collection too → 'many' cardinality (same as hasMany).
|
|
156
|
+
const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
|
|
156
157
|
const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
|
|
157
158
|
lines.push(` ${relName}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
|
|
158
159
|
}
|
|
@@ -161,7 +162,7 @@ export function generateTypes(schema) {
|
|
|
161
162
|
// --- Legacy per-relation interfaces (kept for backward compatibility) ---
|
|
162
163
|
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
163
164
|
const targetType = entityName(rel.to);
|
|
164
|
-
if (rel.type === 'hasMany') {
|
|
165
|
+
if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
|
|
165
166
|
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
166
167
|
lines.push(`export interface ${typeName}With${snakeToPascal(relName)} extends ${typeName} {`);
|
|
167
168
|
lines.push(` ${relName}: ${targetType}[];`);
|
|
@@ -328,7 +329,17 @@ function generateMetadata(schema) {
|
|
|
328
329
|
const refLiteral = Array.isArray(rel.referenceKey)
|
|
329
330
|
? `[${rel.referenceKey.map((c) => `'${escSQ(c)}'`).join(', ')}]`
|
|
330
331
|
: `'${escSQ(rel.referenceKey)}'`;
|
|
331
|
-
|
|
332
|
+
// manyToMany relations carry a `through` junction descriptor — emit it so
|
|
333
|
+
// the runtime query builder can JOIN through the junction table.
|
|
334
|
+
let throughLiteral = '';
|
|
335
|
+
if (rel.through) {
|
|
336
|
+
const keyLiteral = (k) => Array.isArray(k) ? `[${k.map((c) => `'${escSQ(c)}'`).join(', ')}]` : `'${escSQ(k)}'`;
|
|
337
|
+
throughLiteral =
|
|
338
|
+
`, through: { table: '${escSQ(rel.through.table)}', ` +
|
|
339
|
+
`sourceKey: ${keyLiteral(rel.through.sourceKey)}, ` +
|
|
340
|
+
`targetKey: ${keyLiteral(rel.through.targetKey)} }`;
|
|
341
|
+
}
|
|
342
|
+
lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: ${fkLiteral}, referenceKey: ${refLiteral}${throughLiteral} },`);
|
|
332
343
|
}
|
|
333
344
|
lines.push(' },');
|
|
334
345
|
// indexes
|
package/dist/index.d.ts
CHANGED
|
@@ -43,10 +43,12 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export type { ObserveConfig, ObserveHandle } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type NestedCreateOp, type NestedUpdateOp, type OmitResult, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
46
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type NestedCreateOp, type NestedUpdateOp, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
47
|
+
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
47
48
|
export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
48
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
49
|
-
export { ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, defineSchema, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
50
|
+
export { applyManyToManyRelations, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, defineSchema, type ManyToManyDef, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
50
51
|
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
51
52
|
export { type TurbineHttpOptions, turbineHttp } from './serverless.js';
|
|
53
|
+
export { buildTypedSql, TypedSqlQuery } from './typed-sql.js';
|
|
52
54
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -48,14 +48,18 @@ export { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './
|
|
|
48
48
|
export { executePipeline, pipelineSupported } from './pipeline.js';
|
|
49
49
|
// Query builder
|
|
50
50
|
export { QueryInterface, } from './query/index.js';
|
|
51
|
+
// Realtime — LISTEN/NOTIFY pub/sub
|
|
52
|
+
export { validateChannel } from './realtime.js';
|
|
51
53
|
// Schema utilities
|
|
52
54
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
53
55
|
// Schema builder — define schemas in TypeScript
|
|
54
|
-
export { ColumnBuilder, column, defineSchema,
|
|
56
|
+
export { applyManyToManyRelations, ColumnBuilder, column, defineSchema,
|
|
55
57
|
// Legacy compat (deprecated — use object format with defineSchema)
|
|
56
58
|
table, } from './schema-builder.js';
|
|
57
59
|
// Schema SQL — generate DDL, diff, and push
|
|
58
60
|
export { schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
59
61
|
// Serverless / edge factory
|
|
60
62
|
export { turbineHttp } from './serverless.js';
|
|
63
|
+
// Typed raw SQL — Turbine's TypedSQL escape hatch
|
|
64
|
+
export { buildTypedSql, TypedSqlQuery } from './typed-sql.js';
|
|
61
65
|
//# sourceMappingURL=index.js.map
|
package/dist/introspect.js
CHANGED
|
@@ -275,6 +275,87 @@ export async function introspect(options) {
|
|
|
275
275
|
referenceKey,
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
|
+
// ----- Conservative many-to-many auto-detection (PURELY ADDITIVE) -----
|
|
279
|
+
//
|
|
280
|
+
// Auto-detecting m2m is a footgun: any table with two FKs *looks* like a
|
|
281
|
+
// junction, but a `enrollments(student_id, course_id, grade, enrolled_at)`
|
|
282
|
+
// table is a first-class entity, not a join table. Prisma and Drizzle both
|
|
283
|
+
// require explicit m2m declaration for exactly this reason.
|
|
284
|
+
//
|
|
285
|
+
// We only treat a table J as a PURE junction when ALL of these hold:
|
|
286
|
+
// 1. J's primary key is exactly two columns.
|
|
287
|
+
// 2. J has exactly two FKs, each single-column.
|
|
288
|
+
// 3. Each FK's source column is one of J's two PK columns (the PK *is* the
|
|
289
|
+
// two FK columns — no surrogate PK, no extra identity).
|
|
290
|
+
// 4. The two FKs target two DISTINCT tables (A and B).
|
|
291
|
+
// 5. J has no columns beyond those two FK/PK columns (no payload columns
|
|
292
|
+
// like `grade` or `created_at`).
|
|
293
|
+
//
|
|
294
|
+
// For such a J linking A and B we ADD a `manyToMany` relation on A → B and
|
|
295
|
+
// symmetrically on B → A, both routed `through` J. The existing belongsTo /
|
|
296
|
+
// hasMany relations derived from J's FKs are left untouched — this block
|
|
297
|
+
// never removes or renames anything. If the chosen relation name already
|
|
298
|
+
// exists on the source table (e.g. another relation grabbed it), we SKIP to
|
|
299
|
+
// stay additive.
|
|
300
|
+
for (const tableName of tableNames) {
|
|
301
|
+
const pk = pkByTable.get(tableName) ?? [];
|
|
302
|
+
if (pk.length !== 2)
|
|
303
|
+
continue;
|
|
304
|
+
// FKs whose source is this table.
|
|
305
|
+
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
306
|
+
if (tableFks.length !== 2)
|
|
307
|
+
continue;
|
|
308
|
+
// Both FKs must be single-column.
|
|
309
|
+
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
310
|
+
continue;
|
|
311
|
+
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
312
|
+
const pkSet = new Set(pk);
|
|
313
|
+
// Both FK columns must be the PK columns (and vice-versa).
|
|
314
|
+
if (!fkCols.every((c) => pkSet.has(c)))
|
|
315
|
+
continue;
|
|
316
|
+
if (new Set(fkCols).size !== 2)
|
|
317
|
+
continue;
|
|
318
|
+
// Two DISTINCT target tables.
|
|
319
|
+
const [fkA, fkB] = tableFks;
|
|
320
|
+
if (fkA.targetTable === fkB.targetTable)
|
|
321
|
+
continue;
|
|
322
|
+
// No payload columns: J's columns are exactly the two FK/PK columns.
|
|
323
|
+
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
324
|
+
if (jCols.length !== 2)
|
|
325
|
+
continue;
|
|
326
|
+
// For each direction, the m2m `referenceKey` is the *targeted* table's
|
|
327
|
+
// referenced column(s); the junction's sourceKey is the FK column pointing
|
|
328
|
+
// to that table; the targetKey is the FK column pointing to the OTHER table.
|
|
329
|
+
const addM2M = (self, other) => {
|
|
330
|
+
const sourceTbl = self.targetTable; // A
|
|
331
|
+
const targetTbl = other.targetTable; // B
|
|
332
|
+
const relName = snakeToCamel(targetTbl); // plural table name → e.g. "tags"
|
|
333
|
+
if (!relationsByTable.has(sourceTbl))
|
|
334
|
+
relationsByTable.set(sourceTbl, {});
|
|
335
|
+
const existing = relationsByTable.get(sourceTbl);
|
|
336
|
+
// Additive-only: never clobber an existing relation name.
|
|
337
|
+
if (existing[relName])
|
|
338
|
+
return;
|
|
339
|
+
existing[relName] = {
|
|
340
|
+
type: 'manyToMany',
|
|
341
|
+
name: relName,
|
|
342
|
+
from: sourceTbl,
|
|
343
|
+
to: targetTbl,
|
|
344
|
+
// referenceKey = A's referenced column(s) that J's sourceKey points at.
|
|
345
|
+
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
346
|
+
// foreignKey is unused for m2m correlation but kept for shape parity
|
|
347
|
+
// (mirrors the source-side reference for back-compat consumers).
|
|
348
|
+
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
349
|
+
through: {
|
|
350
|
+
table: tableName,
|
|
351
|
+
sourceKey: self.sourceColumns[0], // J col → A
|
|
352
|
+
targetKey: other.sourceColumns[0], // J col → B
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
addM2M(fkA, fkB); // A → B
|
|
357
|
+
addM2M(fkB, fkA); // B → A
|
|
358
|
+
}
|
|
278
359
|
// ----- Assemble TableMetadata for each table -----
|
|
279
360
|
const tables = {};
|
|
280
361
|
for (const tableName of tableNames) {
|
package/dist/nested-write.js
CHANGED
|
@@ -137,17 +137,29 @@ export async function executeNestedCreate(ctx, tableName, data, depth = 0, path
|
|
|
137
137
|
}
|
|
138
138
|
validateOps(relName, ops, false);
|
|
139
139
|
}
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
//
|
|
140
|
+
// belongsTo relations put the foreign key on the PARENT row, so they must be
|
|
141
|
+
// resolved BEFORE the parent is inserted — otherwise a NOT NULL FK column
|
|
142
|
+
// fails on the initial INSERT. We resolve each belongsTo op (create/connect/
|
|
143
|
+
// connectOrCreate) to its referenced row and fold the FK values into the
|
|
144
|
+
// parent's own INSERT.
|
|
145
|
+
const belongsToFks = {};
|
|
146
|
+
for (const [relName, ops] of Object.entries(relations)) {
|
|
147
|
+
const rel = tableMeta.relations[relName];
|
|
148
|
+
if (rel.type === 'belongsTo') {
|
|
149
|
+
Object.assign(belongsToFks, await resolveBelongsToForCreate(ctx, rel, ops, tableName, depth, path, relName));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Insert the parent row (scalars + resolved belongsTo foreign keys)
|
|
153
|
+
const parentRow = (await ctx.tx.table(tableName).create({
|
|
154
|
+
data: { ...scalars, ...belongsToFks },
|
|
155
|
+
}));
|
|
156
|
+
// Process hasMany / hasOne relations — their FK lives on the CHILD, so they
|
|
157
|
+
// need the parent row to exist first.
|
|
143
158
|
for (const [relName, ops] of Object.entries(relations)) {
|
|
144
159
|
const rel = tableMeta.relations[relName];
|
|
145
160
|
if (rel.type === 'hasMany' || rel.type === 'hasOne') {
|
|
146
161
|
await processHasManyCreate(ctx, rel, ops, parentRow, depth, path, relName);
|
|
147
162
|
}
|
|
148
|
-
else if (rel.type === 'belongsTo') {
|
|
149
|
-
await processBelongsToCreate(ctx, rel, ops, parentRow, tableName, depth, path, relName);
|
|
150
|
-
}
|
|
151
163
|
}
|
|
152
164
|
// Build the `with` clause for the final read to return the full tree
|
|
153
165
|
const withClause = {};
|
|
@@ -312,6 +324,58 @@ async function processHasManyCreate(ctx, rel, ops, parentRow, depth, path, relNa
|
|
|
312
324
|
// ---------------------------------------------------------------------------
|
|
313
325
|
// belongsTo create operations
|
|
314
326
|
// ---------------------------------------------------------------------------
|
|
327
|
+
/**
|
|
328
|
+
* Resolve a belongsTo relation's create/connect/connectOrCreate op to the
|
|
329
|
+
* foreign-key value(s) that belong on the PARENT row, returning them keyed by
|
|
330
|
+
* the parent's own field names so they can be merged into the parent INSERT.
|
|
331
|
+
*
|
|
332
|
+
* Used by the create path only. (The update path uses processBelongsToCreate,
|
|
333
|
+
* which UPDATEs the FK after the parent already exists.)
|
|
334
|
+
*/
|
|
335
|
+
async function resolveBelongsToForCreate(ctx, rel, ops, parentTable, depth, path, relName) {
|
|
336
|
+
const fks = normalizeKeyColumns(rel.foreignKey);
|
|
337
|
+
const refs = normalizeKeyColumns(rel.referenceKey);
|
|
338
|
+
const parentMeta = ctx.schema.tables[parentTable];
|
|
339
|
+
const relatedTable = ctx.schema.tables[rel.to];
|
|
340
|
+
let relatedRow = null;
|
|
341
|
+
if (ops.create !== undefined) {
|
|
342
|
+
const items = toArray(ops.create);
|
|
343
|
+
if (items.length > 0) {
|
|
344
|
+
relatedRow = (await executeNestedCreate(ctx, rel.to, items[0], depth + 1, [...path, relName]));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else if (ops.connect !== undefined) {
|
|
348
|
+
const items = toArray(ops.connect);
|
|
349
|
+
if (items.length > 0) {
|
|
350
|
+
const target = items[0];
|
|
351
|
+
relatedRow = (await ctx.tx.table(rel.to).findUnique({ where: target }));
|
|
352
|
+
if (!relatedRow) {
|
|
353
|
+
throw new ValidationError(`[turbine] connect on "${relName}": no ${rel.to} row found matching ${JSON.stringify(target)}.`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else if (ops.connectOrCreate !== undefined) {
|
|
358
|
+
const items = toArray(ops.connectOrCreate);
|
|
359
|
+
if (items.length > 0) {
|
|
360
|
+
const op = items[0];
|
|
361
|
+
relatedRow = (await ctx.tx.table(rel.to).findUnique({ where: op.where }));
|
|
362
|
+
if (!relatedRow) {
|
|
363
|
+
// For belongsTo the FK lives on the parent, so the related row is
|
|
364
|
+
// created plainly (no FK injection) and we read its reference key.
|
|
365
|
+
relatedRow = (await ctx.tx.table(rel.to).create({ data: op.create }));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const fkScalars = {};
|
|
370
|
+
if (relatedRow) {
|
|
371
|
+
for (let i = 0; i < fks.length; i++) {
|
|
372
|
+
const fkField = parentMeta.reverseColumnMap[fks[i]] ?? fks[i];
|
|
373
|
+
const refField = relatedTable?.reverseColumnMap[refs[i]] ?? refs[i];
|
|
374
|
+
fkScalars[fkField] = relatedRow[refField];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return fkScalars;
|
|
378
|
+
}
|
|
315
379
|
async function processBelongsToCreate(ctx, rel, ops, parentRow, parentTable, depth, path, relName) {
|
|
316
380
|
const fks = normalizeKeyColumns(rel.foreignKey);
|
|
317
381
|
const refs = normalizeKeyColumns(rel.referenceKey);
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -114,6 +114,14 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
114
114
|
private readonly columnArrayTypeMap;
|
|
115
115
|
/** Tracks tables that have already triggered a deep-with warning (one-time) */
|
|
116
116
|
private readonly deepWithWarned;
|
|
117
|
+
/**
|
|
118
|
+
* Per-table memo of date columns keyed by their camelCase FIELD name.
|
|
119
|
+
* `meta.dateColumns` is keyed by raw snake_case column name, which matches
|
|
120
|
+
* top-level rows from pg. Nested relation rows arrive from json_build_object
|
|
121
|
+
* with camelCase keys, so they need this camelCase-keyed set to be coerced
|
|
122
|
+
* to Date as well (otherwise nested dates leak through as strings).
|
|
123
|
+
*/
|
|
124
|
+
private readonly camelDateFieldCache;
|
|
117
125
|
/** True when this QI runs inside an active transaction (set via _txScoped option). */
|
|
118
126
|
private readonly txScoped;
|
|
119
127
|
/** Original options reference — forwarded to child QIs in nested writes. */
|
|
@@ -251,6 +259,24 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
251
259
|
buildCount(args?: CountArgs<T>): DeferredQuery<number>;
|
|
252
260
|
groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;
|
|
253
261
|
buildGroupBy(args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
|
|
262
|
+
/**
|
|
263
|
+
* Build the SQL fragments for a {@link HavingClause}.
|
|
264
|
+
*
|
|
265
|
+
* Each aggregate expression (`COUNT(*)`, `SUM("col")`, etc.) is constructed
|
|
266
|
+
* from a **schema-validated, quoted** column identifier — `this.toColumn()`
|
|
267
|
+
* throws {@link ValidationError} for unknown fields and `this.q()` quotes via
|
|
268
|
+
* the dialect, so no unvalidated identifier ever reaches the SQL string. Every
|
|
269
|
+
* comparison value is pushed onto the shared `params` array and referenced by
|
|
270
|
+
* a `$N` placeholder via {@link buildHavingNumericClauses} — there is no string
|
|
271
|
+
* interpolation of user values.
|
|
272
|
+
*/
|
|
273
|
+
private buildHavingClauses;
|
|
274
|
+
/**
|
|
275
|
+
* Convert a single having filter into one or more parameterized SQL
|
|
276
|
+
* comparisons against the given aggregate expression. A bare number is
|
|
277
|
+
* shorthand for equality. Unknown operator keys throw {@link ValidationError}.
|
|
278
|
+
*/
|
|
279
|
+
private buildHavingNumericClauses;
|
|
254
280
|
aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
|
|
255
281
|
buildAggregate(args: AggregateArgs<T>): DeferredQuery<AggregateResult<T>>;
|
|
256
282
|
/**
|
|
@@ -311,6 +337,19 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
311
337
|
private collectJsonFilterParams;
|
|
312
338
|
/** Collect params from array filter. Mirrors buildArrayFilterClauses. */
|
|
313
339
|
private collectArrayFilterParams;
|
|
340
|
+
/**
|
|
341
|
+
* Collect params for an orderBy clause. Only vector KNN ordering pushes a
|
|
342
|
+
* param (the `$n::vector` query vector); plain direction ordering is
|
|
343
|
+
* parameterless. Mirrors buildOrderBy's push order exactly so the cached-SQL
|
|
344
|
+
* param re-collection stays in lockstep.
|
|
345
|
+
*/
|
|
346
|
+
private collectOrderByParams;
|
|
347
|
+
/**
|
|
348
|
+
* Collect params for a vector distance WHERE filter. Mirrors
|
|
349
|
+
* {@link buildVectorFilterClauses}: the `$n::vector` query vector first, then
|
|
350
|
+
* the comparison threshold(s).
|
|
351
|
+
*/
|
|
352
|
+
private collectVectorFilterParams;
|
|
314
353
|
/**
|
|
315
354
|
* Produce a fingerprint for a `with` clause tree. Recursion mirrors
|
|
316
355
|
* buildSelectWithRelations / buildRelationSubquery.
|
|
@@ -368,9 +407,40 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
368
407
|
* Each operator key becomes its own clause, all ANDed together.
|
|
369
408
|
*/
|
|
370
409
|
private buildOperatorClauses;
|
|
371
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* Build ORDER BY clause from an object.
|
|
412
|
+
*
|
|
413
|
+
* Each value is either a plain direction (`'asc'`/`'desc'`) or — for pgvector
|
|
414
|
+
* columns — a `{ distance: { to, metric, direction? } }` KNN ordering object.
|
|
415
|
+
* Vector ordering binds the query vector as a `$n::vector` param, so a `params`
|
|
416
|
+
* array MUST be supplied when a vector ordering may be present (top-level
|
|
417
|
+
* findMany path). When `params` is omitted (groupBy / relation path) a vector
|
|
418
|
+
* ordering throws — KNN ordering is only supported at the top level.
|
|
419
|
+
*/
|
|
372
420
|
private buildOrderBy;
|
|
421
|
+
/**
|
|
422
|
+
* Resolve a {@link VectorMetric} to its pgvector distance operator from a
|
|
423
|
+
* fixed allow-list, validating the target column is actually a `vector`
|
|
424
|
+
* column. Throws {@link ValidationError} for an unknown metric or a
|
|
425
|
+
* non-vector column — a user-supplied string can never become a SQL operator.
|
|
426
|
+
*/
|
|
427
|
+
private vectorOperator;
|
|
428
|
+
/**
|
|
429
|
+
* Validate and bind a query vector as a single `$n::vector` parameter.
|
|
430
|
+
* Every element must be a finite number (no NaN / Infinity / strings) so a
|
|
431
|
+
* malformed array can never produce a broken `::vector` literal, and the array
|
|
432
|
+
* is NEVER string-interpolated into the SQL text. Returns the `$n::vector`
|
|
433
|
+
* placeholder string.
|
|
434
|
+
*/
|
|
435
|
+
private pushVectorParam;
|
|
373
436
|
/** Parse a flat row: convert snake_case to camelCase + Date coercion */
|
|
437
|
+
/**
|
|
438
|
+
* Returns the set of camelCase field names for a table's date columns,
|
|
439
|
+
* derived once from `meta.dateColumns` (snake_case) via reverseColumnMap and
|
|
440
|
+
* memoized per table. Used so nested relation rows (camelCase keys) coerce
|
|
441
|
+
* dates the same way top-level rows do.
|
|
442
|
+
*/
|
|
443
|
+
private getCamelDateFields;
|
|
374
444
|
private parseRow;
|
|
375
445
|
/** Parse a row that may contain JSON nested relation columns */
|
|
376
446
|
private parseNestedRow;
|
|
@@ -509,6 +579,27 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
509
579
|
* evaluates to a JSON array (hasMany) or a JSON object (belongsTo/hasOne).
|
|
510
580
|
*/
|
|
511
581
|
private buildRelationSubquery;
|
|
582
|
+
/**
|
|
583
|
+
* Build the json_agg subquery for a `manyToMany` relation, JOINing the target
|
|
584
|
+
* table through a junction (join) table.
|
|
585
|
+
*
|
|
586
|
+
* Shape (no LIMIT/ORDER):
|
|
587
|
+
* ```sql
|
|
588
|
+
* SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
|
|
589
|
+
* FROM <target> <talias>
|
|
590
|
+
* JOIN <junction> <jalias> ON <jalias>.<targetKey> = <talias>.<targetPK>
|
|
591
|
+
* WHERE <jalias>.<sourceKey> = <parentRef>.<referenceKey>
|
|
592
|
+
* ```
|
|
593
|
+
*
|
|
594
|
+
* With LIMIT/ORDER, the rows are wrapped in an inner subquery so the LIMIT
|
|
595
|
+
* applies BEFORE aggregation (identical strategy to hasMany).
|
|
596
|
+
*
|
|
597
|
+
* Cardinality is always 'many' → empty-array fallback, never NULL.
|
|
598
|
+
*
|
|
599
|
+
* IMPORTANT: every `params.push` here MUST be mirrored, in the same order, in
|
|
600
|
+
* {@link collectRelationSubqueryParams} or pipeline batching will desync.
|
|
601
|
+
*/
|
|
602
|
+
private buildManyToManySubquery;
|
|
512
603
|
/**
|
|
513
604
|
* Get the Postgres type for a column (e.g. 'jsonb', 'text', '_int4').
|
|
514
605
|
* Used to detect JSONB/array columns for specialized operators.
|
|
@@ -530,6 +621,18 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
530
621
|
* Supports: has, hasEvery, hasSome, isEmpty.
|
|
531
622
|
*/
|
|
532
623
|
private buildArrayFilterClauses;
|
|
624
|
+
/**
|
|
625
|
+
* Build SQL clauses for a pgvector distance WHERE filter:
|
|
626
|
+
*
|
|
627
|
+
* `"embedding" <-> $1::vector < $2`
|
|
628
|
+
*
|
|
629
|
+
* The query vector is bound as a `$n::vector` param (never interpolated), the
|
|
630
|
+
* metric maps to an operator via a fixed allow-list, and each comparison
|
|
631
|
+
* threshold (`lt`/`lte`/`gt`/`gte`) is its own bound param. Emits one clause
|
|
632
|
+
* per supplied comparator (all ANDed). Param push order matches
|
|
633
|
+
* {@link collectVectorFilterParams}.
|
|
634
|
+
*/
|
|
635
|
+
private buildVectorFilterClauses;
|
|
533
636
|
/**
|
|
534
637
|
* Build SQL clause for full-text search using to_tsvector @@ to_tsquery.
|
|
535
638
|
* The config name is validated to prevent injection (only alphanumeric + underscore).
|