turbine-orm 0.9.2 → 0.10.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 +34 -16
- package/dist/adapters/cockroachdb.d.ts +40 -0
- package/dist/adapters/cockroachdb.js +172 -0
- package/dist/adapters/index.d.ts +107 -0
- package/dist/adapters/index.js +83 -0
- package/dist/adapters/yugabytedb.d.ts +52 -0
- package/dist/adapters/yugabytedb.js +156 -0
- package/dist/cjs/adapters/cockroachdb.js +174 -0
- package/dist/cjs/adapters/index.js +87 -0
- package/dist/cjs/adapters/yugabytedb.js +158 -0
- package/dist/cjs/cli/index.js +2 -1
- package/dist/cjs/cli/migrate.js +18 -12
- package/dist/cjs/cli/studio.js +5 -4
- package/dist/cjs/generate.js +8 -1
- package/dist/cjs/index.js +10 -3
- package/dist/cjs/introspect.js +46 -18
- package/dist/cjs/query/builder.js +22 -5
- package/dist/cjs/query/index.js +2 -1
- package/dist/cjs/query/utils.js +18 -0
- package/dist/cjs/schema.js +8 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/migrate.d.ts +3 -0
- package/dist/cli/migrate.js +16 -10
- package/dist/cli/studio.d.ts +4 -0
- package/dist/cli/studio.js +5 -4
- package/dist/generate.js +8 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/introspect.js +46 -18
- package/dist/query/builder.js +23 -6
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/utils.d.ts +8 -0
- package/dist/query/utils.js +17 -0
- package/dist/schema.d.ts +6 -4
- package/dist/schema.js +7 -0
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* await db.disconnect();
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
+
export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapters/index.js';
|
|
35
36
|
// Client
|
|
36
37
|
export { TransactionClient, TurbineClient, } from './client.js';
|
|
37
38
|
// Error types
|
|
@@ -45,7 +46,7 @@ export { executePipeline, pipelineSupported } from './pipeline.js';
|
|
|
45
46
|
// Query builder
|
|
46
47
|
export { QueryInterface, } from './query/index.js';
|
|
47
48
|
// Schema utilities
|
|
48
|
-
export { camelToSnake, isDateType, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
49
|
+
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
49
50
|
// Schema builder — define schemas in TypeScript
|
|
50
51
|
export { ColumnBuilder, column, defineSchema,
|
|
51
52
|
// Legacy compat (deprecated — use object format with defineSchema)
|
package/dist/introspect.js
CHANGED
|
@@ -66,13 +66,16 @@ const SQL_FOREIGN_KEYS = `
|
|
|
66
66
|
const SQL_UNIQUE_CONSTRAINTS = `
|
|
67
67
|
SELECT
|
|
68
68
|
tc.table_name,
|
|
69
|
-
|
|
69
|
+
tc.constraint_name,
|
|
70
|
+
kcu.column_name,
|
|
71
|
+
kcu.ordinal_position
|
|
70
72
|
FROM information_schema.table_constraints tc
|
|
71
73
|
JOIN information_schema.key_column_usage kcu
|
|
72
74
|
ON tc.constraint_name = kcu.constraint_name
|
|
73
75
|
AND tc.table_schema = kcu.table_schema
|
|
74
76
|
WHERE tc.constraint_type = 'UNIQUE'
|
|
75
77
|
AND tc.table_schema = $1
|
|
78
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position
|
|
76
79
|
`;
|
|
77
80
|
const SQL_INDEXES = `
|
|
78
81
|
SELECT tablename, indexname, indexdef
|
|
@@ -153,14 +156,22 @@ export async function introspect(options) {
|
|
|
153
156
|
pkByTable.get(row.table_name).push(row.column_name);
|
|
154
157
|
}
|
|
155
158
|
// ----- Group unique constraints by table -----
|
|
159
|
+
// Group rows by (table_name, constraint_name) to correctly handle multi-column unique constraints
|
|
156
160
|
const uniqueByTable = new Map();
|
|
161
|
+
const uniqueConstraintGroups = new Map();
|
|
157
162
|
for (const row of uniqueResult.rows) {
|
|
158
163
|
if (!tableSet.has(row.table_name))
|
|
159
164
|
continue;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
const key = `${row.table_name}::${row.constraint_name}`;
|
|
166
|
+
if (!uniqueConstraintGroups.has(key)) {
|
|
167
|
+
uniqueConstraintGroups.set(key, { table: row.table_name, columns: [] });
|
|
168
|
+
}
|
|
169
|
+
uniqueConstraintGroups.get(key).columns.push(row.column_name);
|
|
170
|
+
}
|
|
171
|
+
for (const { table, columns } of uniqueConstraintGroups.values()) {
|
|
172
|
+
if (!uniqueByTable.has(table))
|
|
173
|
+
uniqueByTable.set(table, []);
|
|
174
|
+
uniqueByTable.get(table).push(columns);
|
|
164
175
|
}
|
|
165
176
|
// ----- Group indexes by table -----
|
|
166
177
|
const indexesByTable = new Map();
|
|
@@ -187,17 +198,25 @@ export async function introspect(options) {
|
|
|
187
198
|
enums[row.typname] = [];
|
|
188
199
|
enums[row.typname].push(row.enumlabel);
|
|
189
200
|
}
|
|
190
|
-
const
|
|
201
|
+
const fkGroups = new Map();
|
|
191
202
|
for (const row of fkResult.rows) {
|
|
192
203
|
if (!tableSet.has(row.source_table) || !tableSet.has(row.target_table))
|
|
193
204
|
continue;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
const key = row.constraint_name;
|
|
206
|
+
if (!fkGroups.has(key)) {
|
|
207
|
+
fkGroups.set(key, {
|
|
208
|
+
sourceTable: row.source_table,
|
|
209
|
+
sourceColumns: [],
|
|
210
|
+
targetTable: row.target_table,
|
|
211
|
+
targetColumns: [],
|
|
212
|
+
constraintName: key,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const entry = fkGroups.get(key);
|
|
216
|
+
entry.sourceColumns.push(row.source_column);
|
|
217
|
+
entry.targetColumns.push(row.target_column);
|
|
200
218
|
}
|
|
219
|
+
const foreignKeys = Array.from(fkGroups.values());
|
|
201
220
|
// ----- Build relations from foreign keys -----
|
|
202
221
|
// Count FKs per (source, target) pair for disambiguation
|
|
203
222
|
const fkCounts = new Map();
|
|
@@ -209,10 +228,17 @@ export async function introspect(options) {
|
|
|
209
228
|
for (const fk of foreignKeys) {
|
|
210
229
|
const pairKey = `${fk.sourceTable}→${fk.targetTable}`;
|
|
211
230
|
const needsDisambiguation = (fkCounts.get(pairKey) ?? 0) > 1;
|
|
231
|
+
// For single-column FKs, keep string form for backwards compatibility.
|
|
232
|
+
// For multi-column (composite) FKs, use array form.
|
|
233
|
+
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
234
|
+
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
212
235
|
// --- belongsTo on the source (child) table ---
|
|
213
236
|
// e.g. posts.user_id → users.id creates posts.user (belongsTo)
|
|
237
|
+
// For composite FKs with disambiguation, use the constraint name
|
|
214
238
|
const belongsToName = needsDisambiguation
|
|
215
|
-
?
|
|
239
|
+
? fk.sourceColumns.length === 1
|
|
240
|
+
? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
241
|
+
: snakeToCamel(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
|
|
216
242
|
: singularize(snakeToCamel(fk.targetTable));
|
|
217
243
|
if (!relationsByTable.has(fk.sourceTable))
|
|
218
244
|
relationsByTable.set(fk.sourceTable, {});
|
|
@@ -221,13 +247,15 @@ export async function introspect(options) {
|
|
|
221
247
|
name: belongsToName,
|
|
222
248
|
from: fk.sourceTable,
|
|
223
249
|
to: fk.targetTable,
|
|
224
|
-
foreignKey
|
|
225
|
-
referenceKey
|
|
250
|
+
foreignKey,
|
|
251
|
+
referenceKey,
|
|
226
252
|
};
|
|
227
253
|
// --- hasMany on the target (parent) table ---
|
|
228
254
|
// e.g. posts.user_id → users.id creates users.posts (hasMany)
|
|
229
255
|
const hasManyName = needsDisambiguation
|
|
230
|
-
?
|
|
256
|
+
? fk.sourceColumns.length === 1
|
|
257
|
+
? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
258
|
+
: snakeToCamel(`${fk.sourceTable}_by_${fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, '')}`)
|
|
231
259
|
: snakeToCamel(fk.sourceTable);
|
|
232
260
|
if (!relationsByTable.has(fk.targetTable))
|
|
233
261
|
relationsByTable.set(fk.targetTable, {});
|
|
@@ -236,8 +264,8 @@ export async function introspect(options) {
|
|
|
236
264
|
name: hasManyName,
|
|
237
265
|
from: fk.targetTable,
|
|
238
266
|
to: fk.sourceTable,
|
|
239
|
-
foreignKey
|
|
240
|
-
referenceKey
|
|
267
|
+
foreignKey,
|
|
268
|
+
referenceKey,
|
|
241
269
|
};
|
|
242
270
|
}
|
|
243
271
|
// ----- Assemble TableMetadata for each table -----
|
package/dist/query/builder.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { CircularRelationError, NotFoundError, RelationError, TimeoutError, ValidationError, wrapPgError, } from '../errors.js';
|
|
14
14
|
import { camelToSnake, snakeToCamel } from '../schema.js';
|
|
15
|
-
import { escapeLike, escSingleQuote, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
15
|
+
import { buildCorrelation, escapeLike, escSingleQuote, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Internal detection helpers — used by QueryInterface
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
@@ -96,6 +96,7 @@ function findArrayUniqueKey(value) {
|
|
|
96
96
|
}
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
|
|
99
100
|
export class QueryInterface {
|
|
100
101
|
pool;
|
|
101
102
|
table;
|
|
@@ -258,6 +259,7 @@ export class QueryInterface {
|
|
|
258
259
|
// -------------------------------------------------------------------------
|
|
259
260
|
// findUnique
|
|
260
261
|
// -------------------------------------------------------------------------
|
|
262
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
261
263
|
async findUnique(args) {
|
|
262
264
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
263
265
|
const deferred = this.buildFindUnique(args);
|
|
@@ -265,6 +267,7 @@ export class QueryInterface {
|
|
|
265
267
|
return deferred.transform(result);
|
|
266
268
|
});
|
|
267
269
|
}
|
|
270
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
268
271
|
buildFindUnique(args) {
|
|
269
272
|
const columnsList = this.resolveColumns(args.select, args.omit);
|
|
270
273
|
const whereObj = args.where;
|
|
@@ -360,6 +363,7 @@ export class QueryInterface {
|
|
|
360
363
|
// -------------------------------------------------------------------------
|
|
361
364
|
// findMany
|
|
362
365
|
// -------------------------------------------------------------------------
|
|
366
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
363
367
|
async findMany(args) {
|
|
364
368
|
this.maybeWarnUnlimited(args);
|
|
365
369
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -417,6 +421,7 @@ export class QueryInterface {
|
|
|
417
421
|
}
|
|
418
422
|
return maxDepth;
|
|
419
423
|
}
|
|
424
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
420
425
|
buildFindMany(args) {
|
|
421
426
|
const columnsList = this.resolveColumns(args?.select, args?.omit);
|
|
422
427
|
const colKey = columnsList ? columnsList.join(',') : '*';
|
|
@@ -560,6 +565,7 @@ export class QueryInterface {
|
|
|
560
565
|
* }
|
|
561
566
|
* ```
|
|
562
567
|
*/
|
|
568
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
563
569
|
async *findManyStream(args) {
|
|
564
570
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
565
571
|
const hasRelations = !!args?.with;
|
|
@@ -616,6 +622,7 @@ export class QueryInterface {
|
|
|
616
622
|
// -------------------------------------------------------------------------
|
|
617
623
|
// findFirst — like findMany but returns a single row or null
|
|
618
624
|
// -------------------------------------------------------------------------
|
|
625
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
619
626
|
async findFirst(args) {
|
|
620
627
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
621
628
|
const deferred = this.buildFindFirst(args);
|
|
@@ -623,6 +630,7 @@ export class QueryInterface {
|
|
|
623
630
|
return deferred.transform(result);
|
|
624
631
|
});
|
|
625
632
|
}
|
|
633
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
626
634
|
buildFindFirst(args) {
|
|
627
635
|
// Reuse findMany's SQL builder but force LIMIT 1
|
|
628
636
|
const findManyArgs = { ...args, limit: 1 };
|
|
@@ -640,6 +648,7 @@ export class QueryInterface {
|
|
|
640
648
|
// -------------------------------------------------------------------------
|
|
641
649
|
// findFirstOrThrow — like findFirst but throws if no record found
|
|
642
650
|
// -------------------------------------------------------------------------
|
|
651
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
643
652
|
async findFirstOrThrow(args) {
|
|
644
653
|
return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
|
|
645
654
|
const deferred = this.buildFindFirstOrThrow(args);
|
|
@@ -647,6 +656,7 @@ export class QueryInterface {
|
|
|
647
656
|
return deferred.transform(result);
|
|
648
657
|
});
|
|
649
658
|
}
|
|
659
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
650
660
|
buildFindFirstOrThrow(args) {
|
|
651
661
|
const inner = this.buildFindFirst(args);
|
|
652
662
|
return {
|
|
@@ -669,6 +679,7 @@ export class QueryInterface {
|
|
|
669
679
|
// -------------------------------------------------------------------------
|
|
670
680
|
// findUniqueOrThrow — like findUnique but throws if no record found
|
|
671
681
|
// -------------------------------------------------------------------------
|
|
682
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
672
683
|
async findUniqueOrThrow(args) {
|
|
673
684
|
return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
|
|
674
685
|
const deferred = this.buildFindUniqueOrThrow(args);
|
|
@@ -676,6 +687,7 @@ export class QueryInterface {
|
|
|
676
687
|
return deferred.transform(result);
|
|
677
688
|
});
|
|
678
689
|
}
|
|
690
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
679
691
|
buildFindUniqueOrThrow(args) {
|
|
680
692
|
const inner = this.buildFindUnique(args);
|
|
681
693
|
return {
|
|
@@ -2013,15 +2025,15 @@ export class QueryInterface {
|
|
|
2013
2025
|
const qt = quoteIdent(targetTable);
|
|
2014
2026
|
const qSelf = quoteIdent(this.table);
|
|
2015
2027
|
const clauses = [];
|
|
2016
|
-
// Correlation: link child table to parent table
|
|
2028
|
+
// Correlation: link child table to parent table (supports composite FKs)
|
|
2017
2029
|
let correlation;
|
|
2018
2030
|
if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
2019
2031
|
// parent.pk = child.fk
|
|
2020
|
-
correlation =
|
|
2032
|
+
correlation = buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
|
|
2021
2033
|
}
|
|
2022
2034
|
else {
|
|
2023
2035
|
// belongsTo: parent.fk = child.pk
|
|
2024
|
-
correlation =
|
|
2036
|
+
correlation = buildCorrelation(qt, relDef.referenceKey, qSelf, relDef.foreignKey);
|
|
2025
2037
|
}
|
|
2026
2038
|
// "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter)
|
|
2027
2039
|
if (filterObj.some !== undefined) {
|
|
@@ -2064,6 +2076,10 @@ export class QueryInterface {
|
|
|
2064
2076
|
if (value === undefined)
|
|
2065
2077
|
continue;
|
|
2066
2078
|
const col = meta.columnMap[field] ?? camelToSnake(field);
|
|
2079
|
+
if (!meta.allColumns.includes(col)) {
|
|
2080
|
+
throw new ValidationError(`[turbine] Unknown field "${field}" in relation filter for table "${targetTable}". ` +
|
|
2081
|
+
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
2082
|
+
}
|
|
2067
2083
|
const qCol = `${qt}.${quoteIdent(col)}`;
|
|
2068
2084
|
if (value === null) {
|
|
2069
2085
|
conditions.push(`${qCol} IS NULL`);
|
|
@@ -2471,12 +2487,13 @@ export class QueryInterface {
|
|
|
2471
2487
|
// Build WHERE — correlate to parent via parentRef (alias or table name).
|
|
2472
2488
|
// For hasMany: target has FK, so alias.fk = parentRef.pk
|
|
2473
2489
|
// For belongsTo: source has FK, so alias.pk = parentRef.fk (reversed)
|
|
2490
|
+
// Supports composite foreign keys (string[]) via buildCorrelation.
|
|
2474
2491
|
let whereClause;
|
|
2475
2492
|
if (relDef.type === 'belongsTo' || relDef.type === 'hasOne') {
|
|
2476
|
-
whereClause =
|
|
2493
|
+
whereClause = buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey);
|
|
2477
2494
|
}
|
|
2478
2495
|
else {
|
|
2479
|
-
whereClause =
|
|
2496
|
+
whereClause = buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
|
|
2480
2497
|
}
|
|
2481
2498
|
// Additional filters — properly parameterized
|
|
2482
2499
|
if (spec !== true && spec.where) {
|
package/dist/query/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export type { AggregateArgs, AggregateResult, ArrayFilter, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, JsonFilter, OrderDirection, RelationDescriptor, RelationFilter, TypedWithClause, UpdateArgs, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
|
|
9
9
|
export type { SqlCacheEntry } from './utils.js';
|
|
10
|
-
export { escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
10
|
+
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
11
11
|
export type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './builder.js';
|
|
12
12
|
export { QueryInterface } from './builder.js';
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/query/index.js
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
* `import { … } from './query/index.js'` is a drop-in replacement for the
|
|
6
6
|
* former monolithic `import { … } from './query.js'`.
|
|
7
7
|
*/
|
|
8
|
-
export { escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
8
|
+
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
9
9
|
export { QueryInterface } from './builder.js';
|
|
10
10
|
//# sourceMappingURL=index.js.map
|
package/dist/query/utils.d.ts
CHANGED
|
@@ -57,4 +57,12 @@ export declare function fnv1a64Hex(s: string): string;
|
|
|
57
57
|
export declare function sqlToPreparedName(sql: string): string;
|
|
58
58
|
/** Known operator keys — used to detect operator objects vs plain values */
|
|
59
59
|
export declare const OPERATOR_KEYS: Set<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Build a correlation clause joining columns between two table references.
|
|
62
|
+
* Handles both single-column (string) and multi-column (string[]) foreign keys.
|
|
63
|
+
*
|
|
64
|
+
* For single-column: `"alias"."col" = "parent"."col"`
|
|
65
|
+
* For multi-column: `"alias"."col_a" = "parent"."ref_a" AND "alias"."col_b" = "parent"."ref_b"`
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildCorrelation(leftRef: string, leftColumns: string | string[], rightRef: string, rightColumns: string | string[]): string;
|
|
60
68
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/query/utils.js
CHANGED
|
@@ -111,4 +111,21 @@ export const OPERATOR_KEYS = new Set([
|
|
|
111
111
|
'endsWith',
|
|
112
112
|
'mode',
|
|
113
113
|
]);
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Composite key correlation helper
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* Build a correlation clause joining columns between two table references.
|
|
119
|
+
* Handles both single-column (string) and multi-column (string[]) foreign keys.
|
|
120
|
+
*
|
|
121
|
+
* For single-column: `"alias"."col" = "parent"."col"`
|
|
122
|
+
* For multi-column: `"alias"."col_a" = "parent"."ref_a" AND "alias"."col_b" = "parent"."ref_b"`
|
|
123
|
+
*/
|
|
124
|
+
export function buildCorrelation(leftRef, leftColumns, rightRef, rightColumns) {
|
|
125
|
+
const leftCols = Array.isArray(leftColumns) ? leftColumns : [leftColumns];
|
|
126
|
+
const rightCols = Array.isArray(rightColumns) ? rightColumns : [rightColumns];
|
|
127
|
+
return leftCols
|
|
128
|
+
.map((col, i) => `${leftRef}.${quoteIdent(col)} = ${rightRef}.${quoteIdent(rightCols[i])}`)
|
|
129
|
+
.join(' AND ');
|
|
130
|
+
}
|
|
114
131
|
//# sourceMappingURL=utils.js.map
|
package/dist/schema.d.ts
CHANGED
|
@@ -62,11 +62,13 @@ export interface RelationDef {
|
|
|
62
62
|
from: string;
|
|
63
63
|
/** Target table */
|
|
64
64
|
to: string;
|
|
65
|
-
/** FK column on the "many" / "child" side (snake_case) */
|
|
66
|
-
foreignKey: string;
|
|
67
|
-
/** Referenced column on the "one" / "parent" side (snake_case) */
|
|
68
|
-
referenceKey: string;
|
|
65
|
+
/** FK column(s) on the "many" / "child" side (snake_case). Array for composite FKs. */
|
|
66
|
+
foreignKey: string | string[];
|
|
67
|
+
/** Referenced column(s) on the "one" / "parent" side (snake_case). Array for composite FKs. */
|
|
68
|
+
referenceKey: string | string[];
|
|
69
69
|
}
|
|
70
|
+
/** Normalize foreignKey/referenceKey to always be an array for uniform processing */
|
|
71
|
+
export declare function normalizeKeyColumns(key: string | string[]): string[];
|
|
70
72
|
export interface IndexMetadata {
|
|
71
73
|
name: string;
|
|
72
74
|
columns: string[];
|
package/dist/schema.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* They're used by the query builder, code generator, and CLI.
|
|
6
6
|
*/
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers for composite key handling
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/** Normalize foreignKey/referenceKey to always be an array for uniform processing */
|
|
11
|
+
export function normalizeKeyColumns(key) {
|
|
12
|
+
return Array.isArray(key) ? key : [key];
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
8
15
|
// Type mapping: Postgres → TypeScript
|
|
9
16
|
// ---------------------------------------------------------------------------
|
|
10
17
|
const PG_TO_TS = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. 1 dependency, ~110KB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"import": "./dist/cli/config.js",
|
|
19
19
|
"require": "./dist/cjs/cli/config.js",
|
|
20
20
|
"types": "./dist/cli/config.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./adapters": {
|
|
23
|
+
"import": "./dist/adapters/index.js",
|
|
24
|
+
"require": "./dist/cjs/adapters/index.js",
|
|
25
|
+
"types": "./dist/adapters/index.d.ts"
|
|
21
26
|
}
|
|
22
27
|
},
|
|
23
28
|
"main": "./dist/cjs/index.js",
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
"status": "tsx src/cli/index.ts status",
|
|
45
50
|
"examples": "tsx examples/examples.ts",
|
|
46
51
|
"test": "tsx --test src/test/*.test.ts",
|
|
47
|
-
"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/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts",
|
|
52
|
+
"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/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",
|
|
48
53
|
"test:coverage": "c8 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/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts",
|
|
49
54
|
"lint": "biome check src/",
|
|
50
55
|
"lint:fix": "biome check --write src/",
|