linkgress-orm 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.md +196 -196
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +7 -5
- package/dist/entity/db-context.js.map +1 -1
- package/dist/migration/db-schema-manager.d.ts +5 -0
- package/dist/migration/db-schema-manager.d.ts.map +1 -1
- package/dist/migration/db-schema-manager.js +147 -79
- package/dist/migration/db-schema-manager.js.map +1 -1
- package/dist/migration/enum-migrator.js +6 -6
- package/dist/query/collection-strategy.interface.d.ts +38 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/cte-builder.d.ts +18 -1
- package/dist/query/cte-builder.d.ts.map +1 -1
- package/dist/query/cte-builder.js +102 -11
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +24 -1
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +260 -71
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/join-builder.d.ts.map +1 -1
- package/dist/query/join-builder.js +10 -14
- package/dist/query/join-builder.js.map +1 -1
- package/dist/query/query-builder.d.ts +65 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +566 -167
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts +4 -0
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +169 -78
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +4 -0
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +86 -28
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +97 -97
- package/dist/schema/table-builder.d.ts +16 -0
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +23 -1
- package/dist/schema/table-builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,117 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = void 0;
|
|
4
|
+
exports.getColumnNameMapForSchema = getColumnNameMapForSchema;
|
|
5
|
+
exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
|
|
6
|
+
exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
|
|
7
|
+
exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
|
|
4
8
|
const conditions_1 = require("./conditions");
|
|
5
9
|
const query_utils_1 = require("./query-utils");
|
|
6
10
|
const subquery_1 = require("./subquery");
|
|
7
11
|
const grouped_query_1 = require("./grouped-query");
|
|
8
12
|
const cte_builder_1 = require("./cte-builder");
|
|
9
13
|
const collection_strategy_factory_1 = require("./collection-strategy.factory");
|
|
14
|
+
/**
|
|
15
|
+
* Performance utility: Get column name map from schema, using cached version if available
|
|
16
|
+
*/
|
|
17
|
+
function getColumnNameMapForSchema(schema) {
|
|
18
|
+
if (schema.columnNameMap) {
|
|
19
|
+
return schema.columnNameMap;
|
|
20
|
+
}
|
|
21
|
+
// Fallback: build the map (for schemas that weren't built with the new TableBuilder)
|
|
22
|
+
const map = new Map();
|
|
23
|
+
for (const [colName, colBuilder] of Object.entries(schema.columns)) {
|
|
24
|
+
map.set(colName, colBuilder.build().name);
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Performance utility: Get relation entries array from schema, using cached version if available
|
|
30
|
+
*/
|
|
31
|
+
function getRelationEntriesForSchema(schema) {
|
|
32
|
+
if (schema.relationEntries) {
|
|
33
|
+
return schema.relationEntries;
|
|
34
|
+
}
|
|
35
|
+
// Fallback: build the array (for schemas that weren't built with the new TableBuilder)
|
|
36
|
+
return Object.entries(schema.relations);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Performance utility: Get target schema for a relation, using cached version if available
|
|
40
|
+
*/
|
|
41
|
+
function getTargetSchemaForRelation(schema, relName, relConfig) {
|
|
42
|
+
// Try cached version first
|
|
43
|
+
if (schema.relationSchemaCache) {
|
|
44
|
+
const cached = schema.relationSchemaCache.get(relName);
|
|
45
|
+
if (cached)
|
|
46
|
+
return cached;
|
|
47
|
+
}
|
|
48
|
+
// Fallback: build the schema
|
|
49
|
+
if (relConfig.targetTableBuilder) {
|
|
50
|
+
return relConfig.targetTableBuilder.build();
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
// Performance: Cache nested field ref proxies per table alias
|
|
55
|
+
const nestedFieldRefProxyCache = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Creates a nested proxy that supports accessing properties at any depth.
|
|
58
|
+
* This allows patterns like `p.product.priceMode` to work even without full schema information.
|
|
59
|
+
* Each property access returns an object that is both a FieldRef and can be further accessed.
|
|
60
|
+
*
|
|
61
|
+
* @param tableAlias The table alias to use for the FieldRef
|
|
62
|
+
* @returns A proxy that creates FieldRefs for any property access
|
|
63
|
+
*/
|
|
64
|
+
function createNestedFieldRefProxy(tableAlias) {
|
|
65
|
+
// Return cached proxy if available
|
|
66
|
+
const cached = nestedFieldRefProxyCache.get(tableAlias);
|
|
67
|
+
if (cached)
|
|
68
|
+
return cached;
|
|
69
|
+
const handler = {
|
|
70
|
+
get: (_target, prop) => {
|
|
71
|
+
// Handle Symbol.toPrimitive for string conversion (used in template literals)
|
|
72
|
+
if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') {
|
|
73
|
+
return () => `[NestedFieldRefProxy:${tableAlias}]`;
|
|
74
|
+
}
|
|
75
|
+
if (typeof prop === 'symbol')
|
|
76
|
+
return undefined;
|
|
77
|
+
// Return an object that is both a FieldRef AND a proxy for further nesting
|
|
78
|
+
const fieldRef = {
|
|
79
|
+
__fieldName: prop,
|
|
80
|
+
__dbColumnName: prop,
|
|
81
|
+
__tableAlias: tableAlias,
|
|
82
|
+
};
|
|
83
|
+
// Return a proxy that acts as both the FieldRef and allows further property access
|
|
84
|
+
return new Proxy(fieldRef, {
|
|
85
|
+
get: (fieldTarget, nestedProp) => {
|
|
86
|
+
// Handle Symbol.toPrimitive for string conversion (used in template literals)
|
|
87
|
+
if (nestedProp === Symbol.toPrimitive || nestedProp === 'toString' || nestedProp === 'valueOf') {
|
|
88
|
+
return () => fieldTarget.__dbColumnName;
|
|
89
|
+
}
|
|
90
|
+
if (typeof nestedProp === 'symbol')
|
|
91
|
+
return undefined;
|
|
92
|
+
// If accessing FieldRef properties, return them
|
|
93
|
+
if (nestedProp === '__fieldName' || nestedProp === '__dbColumnName' || nestedProp === '__tableAlias') {
|
|
94
|
+
return fieldTarget[nestedProp];
|
|
95
|
+
}
|
|
96
|
+
// Otherwise, treat as nested navigation and create a new nested proxy
|
|
97
|
+
// The nested table alias is the property name (e.g., 'product' for p.product)
|
|
98
|
+
return createNestedFieldRefProxy(prop)[nestedProp];
|
|
99
|
+
},
|
|
100
|
+
has: (_fieldTarget, _nestedProp) => true,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
has: (_target, prop) => {
|
|
104
|
+
// The outer proxy doesn't have FieldRef properties - only field names
|
|
105
|
+
if (prop === '__fieldName' || prop === '__dbColumnName' || prop === '__tableAlias') {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const proxy = new Proxy({}, handler);
|
|
112
|
+
nestedFieldRefProxyCache.set(tableAlias, proxy);
|
|
113
|
+
return proxy;
|
|
114
|
+
}
|
|
10
115
|
/**
|
|
11
116
|
* Cached regex for numeric string detection
|
|
12
117
|
* Used to convert PostgreSQL NUMERIC/BIGINT strings to numbers
|
|
@@ -16,7 +121,7 @@ const NUMERIC_REGEX = /^-?\d+(\.\d+)?$/;
|
|
|
16
121
|
* Query builder for a table
|
|
17
122
|
*/
|
|
18
123
|
class QueryBuilder {
|
|
19
|
-
constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy) {
|
|
124
|
+
constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy, schemaRegistry) {
|
|
20
125
|
this.orderByFields = [];
|
|
21
126
|
this.manualJoins = [];
|
|
22
127
|
this.joinCounter = 0;
|
|
@@ -30,6 +135,7 @@ class QueryBuilder {
|
|
|
30
135
|
this.manualJoins = manualJoins || [];
|
|
31
136
|
this.joinCounter = joinCounter || 0;
|
|
32
137
|
this.collectionStrategy = collectionStrategy;
|
|
138
|
+
this.schemaRegistry = schemaRegistry;
|
|
33
139
|
}
|
|
34
140
|
/**
|
|
35
141
|
* Get qualified table name with schema prefix if specified
|
|
@@ -43,7 +149,7 @@ class QueryBuilder {
|
|
|
43
149
|
*/
|
|
44
150
|
select(selector) {
|
|
45
151
|
return new SelectQueryBuilder(this.schema, this.client, selector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
|
|
46
|
-
|
|
152
|
+
this.schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
47
153
|
[], // ctes - start with empty array
|
|
48
154
|
this.collectionStrategy);
|
|
49
155
|
}
|
|
@@ -66,7 +172,8 @@ class QueryBuilder {
|
|
|
66
172
|
* Add CTEs (Common Table Expressions) to the query
|
|
67
173
|
*/
|
|
68
174
|
with(...ctes) {
|
|
69
|
-
return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false,
|
|
175
|
+
return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, this.schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
176
|
+
ctes, this.collectionStrategy);
|
|
70
177
|
}
|
|
71
178
|
/**
|
|
72
179
|
* Create mock row for analysis
|
|
@@ -77,14 +184,10 @@ class QueryBuilder {
|
|
|
77
184
|
return this._cachedMockRow;
|
|
78
185
|
}
|
|
79
186
|
const mock = {};
|
|
80
|
-
// Performance:
|
|
81
|
-
const
|
|
82
|
-
const columnConfigs = new Map();
|
|
83
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
84
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
85
|
-
}
|
|
187
|
+
// Performance: Use pre-computed column name map if available
|
|
188
|
+
const columnNameMap = getColumnNameMapForSchema(this.schema);
|
|
86
189
|
// Add columns as FieldRef objects - type-safe with property name and database column name
|
|
87
|
-
for (const [colName, dbColumnName] of
|
|
190
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
88
191
|
Object.defineProperty(mock, colName, {
|
|
89
192
|
get: () => ({
|
|
90
193
|
__fieldName: colName,
|
|
@@ -95,20 +198,20 @@ class QueryBuilder {
|
|
|
95
198
|
configurable: true,
|
|
96
199
|
});
|
|
97
200
|
}
|
|
98
|
-
// Performance:
|
|
99
|
-
const
|
|
100
|
-
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
101
|
-
if (relConfig.targetTableBuilder) {
|
|
102
|
-
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
103
|
-
}
|
|
104
|
-
}
|
|
201
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
202
|
+
const relationEntries = getRelationEntriesForSchema(this.schema);
|
|
105
203
|
// Add relations (both collections and single references)
|
|
106
|
-
for (const [relName, relConfig] of
|
|
204
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
205
|
+
// Performance: Use cached target schema, but prefer registry lookup for full relations
|
|
206
|
+
let targetSchema = this.schemaRegistry?.get(relConfig.targetTable);
|
|
207
|
+
if (!targetSchema) {
|
|
208
|
+
targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
|
|
209
|
+
}
|
|
107
210
|
if (relConfig.type === 'many') {
|
|
108
|
-
const targetSchema = relationSchemas.get(relName);
|
|
109
211
|
Object.defineProperty(mock, relName, {
|
|
110
212
|
get: () => {
|
|
111
|
-
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema
|
|
213
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
214
|
+
);
|
|
112
215
|
},
|
|
113
216
|
enumerable: true,
|
|
114
217
|
configurable: true,
|
|
@@ -116,10 +219,10 @@ class QueryBuilder {
|
|
|
116
219
|
}
|
|
117
220
|
else {
|
|
118
221
|
// Single reference navigation (many-to-one, one-to-one)
|
|
119
|
-
const targetSchema = relationSchemas.get(relName);
|
|
120
222
|
Object.defineProperty(mock, relName, {
|
|
121
223
|
get: () => {
|
|
122
|
-
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema
|
|
224
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
225
|
+
);
|
|
123
226
|
return refBuilder.createMockTargetRow();
|
|
124
227
|
},
|
|
125
228
|
enumerable: true,
|
|
@@ -234,14 +337,10 @@ class QueryBuilder {
|
|
|
234
337
|
*/
|
|
235
338
|
createMockRowForTable(schema, alias) {
|
|
236
339
|
const mock = {};
|
|
237
|
-
// Performance:
|
|
238
|
-
const
|
|
239
|
-
const columnConfigs = new Map();
|
|
240
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
241
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
242
|
-
}
|
|
340
|
+
// Performance: Use pre-computed column name map if available
|
|
341
|
+
const columnNameMap = getColumnNameMapForSchema(schema);
|
|
243
342
|
// Add columns as FieldRef objects with table alias
|
|
244
|
-
for (const [colName, dbColumnName] of
|
|
343
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
245
344
|
Object.defineProperty(mock, colName, {
|
|
246
345
|
get: () => ({
|
|
247
346
|
__fieldName: colName,
|
|
@@ -252,18 +351,14 @@ class QueryBuilder {
|
|
|
252
351
|
configurable: true,
|
|
253
352
|
});
|
|
254
353
|
}
|
|
255
|
-
// Performance:
|
|
256
|
-
const
|
|
257
|
-
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
258
|
-
if (relConfig.targetTableBuilder) {
|
|
259
|
-
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
260
|
-
}
|
|
261
|
-
}
|
|
354
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
355
|
+
const relationEntries = getRelationEntriesForSchema(schema);
|
|
262
356
|
// Add navigation properties (single references and collections)
|
|
263
|
-
for (const [relName, relConfig] of
|
|
357
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
358
|
+
// Performance: Use cached target schema
|
|
359
|
+
const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
|
|
264
360
|
if (relConfig.type === 'many') {
|
|
265
361
|
// Collection navigation
|
|
266
|
-
const targetSchema = relationSchemas.get(relName);
|
|
267
362
|
Object.defineProperty(mock, relName, {
|
|
268
363
|
get: () => {
|
|
269
364
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
@@ -274,7 +369,6 @@ class QueryBuilder {
|
|
|
274
369
|
}
|
|
275
370
|
else {
|
|
276
371
|
// Single reference navigation
|
|
277
|
-
const targetSchema = relationSchemas.get(relName);
|
|
278
372
|
Object.defineProperty(mock, relName, {
|
|
279
373
|
get: () => {
|
|
280
374
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
@@ -383,7 +477,12 @@ class SelectQueryBuilder {
|
|
|
383
477
|
* .toList();
|
|
384
478
|
*/
|
|
385
479
|
with(...ctes) {
|
|
386
|
-
|
|
480
|
+
// Add CTEs, avoiding duplicates by name
|
|
481
|
+
for (const cte of ctes) {
|
|
482
|
+
if (!this.ctes.some(existing => existing.name === cte.name)) {
|
|
483
|
+
this.ctes.push(cte);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
387
486
|
return this;
|
|
388
487
|
}
|
|
389
488
|
/**
|
|
@@ -610,14 +709,10 @@ class SelectQueryBuilder {
|
|
|
610
709
|
*/
|
|
611
710
|
createMockRowForTable(schema, alias) {
|
|
612
711
|
const mock = {};
|
|
613
|
-
// Performance:
|
|
614
|
-
const
|
|
615
|
-
const columnConfigs = new Map();
|
|
616
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
617
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
618
|
-
}
|
|
712
|
+
// Performance: Use pre-computed column name map if available
|
|
713
|
+
const columnNameMap = getColumnNameMapForSchema(schema);
|
|
619
714
|
// Add columns as FieldRef objects with table alias
|
|
620
|
-
for (const [colName, dbColumnName] of
|
|
715
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
621
716
|
Object.defineProperty(mock, colName, {
|
|
622
717
|
get: () => ({
|
|
623
718
|
__fieldName: colName,
|
|
@@ -628,18 +723,14 @@ class SelectQueryBuilder {
|
|
|
628
723
|
configurable: true,
|
|
629
724
|
});
|
|
630
725
|
}
|
|
631
|
-
// Performance:
|
|
632
|
-
const
|
|
633
|
-
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
634
|
-
if (relConfig.targetTableBuilder) {
|
|
635
|
-
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
636
|
-
}
|
|
637
|
-
}
|
|
726
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
727
|
+
const relationEntries = getRelationEntriesForSchema(schema);
|
|
638
728
|
// Add navigation properties (single references and collections)
|
|
639
|
-
for (const [relName, relConfig] of
|
|
729
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
730
|
+
// Performance: Use cached target schema
|
|
731
|
+
const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
|
|
640
732
|
if (relConfig.type === 'many') {
|
|
641
733
|
// Collection navigation
|
|
642
|
-
const targetSchema = relationSchemas.get(relName);
|
|
643
734
|
Object.defineProperty(mock, relName, {
|
|
644
735
|
get: () => {
|
|
645
736
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
@@ -650,7 +741,6 @@ class SelectQueryBuilder {
|
|
|
650
741
|
}
|
|
651
742
|
else {
|
|
652
743
|
// Single reference navigation
|
|
653
|
-
const targetSchema = relationSchemas.get(relName);
|
|
654
744
|
Object.defineProperty(mock, relName, {
|
|
655
745
|
get: () => {
|
|
656
746
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
@@ -723,7 +813,7 @@ class SelectQueryBuilder {
|
|
|
723
813
|
get(target, prop) {
|
|
724
814
|
if (typeof prop === 'symbol')
|
|
725
815
|
return undefined;
|
|
726
|
-
// If we have selection metadata, check if this property has a mapper
|
|
816
|
+
// If we have selection metadata, check if this property has a mapper or is an aggregation array
|
|
727
817
|
if (cte.selectionMetadata && prop in cte.selectionMetadata) {
|
|
728
818
|
const value = cte.selectionMetadata[prop];
|
|
729
819
|
// If it's a SqlFragment with a mapper, preserve it
|
|
@@ -736,6 +826,16 @@ class SelectQueryBuilder {
|
|
|
736
826
|
getMapper: () => value.getMapper(),
|
|
737
827
|
};
|
|
738
828
|
}
|
|
829
|
+
// If it's a CTE aggregation array marker, preserve it with inner metadata
|
|
830
|
+
if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
|
|
831
|
+
return {
|
|
832
|
+
__fieldName: prop,
|
|
833
|
+
__dbColumnName: prop,
|
|
834
|
+
__tableAlias: cte.name,
|
|
835
|
+
__isAggregationArray: true,
|
|
836
|
+
__innerSelectionMetadata: value.__innerSelectionMetadata,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
739
839
|
}
|
|
740
840
|
// Return a regular FieldRef for any property accessed
|
|
741
841
|
return {
|
|
@@ -1238,9 +1338,10 @@ class SelectQueryBuilder {
|
|
|
1238
1338
|
*/
|
|
1239
1339
|
createMockRow() {
|
|
1240
1340
|
const mock = {};
|
|
1341
|
+
// Performance: Use pre-computed column name map if available
|
|
1342
|
+
const columnNameMap = getColumnNameMapForSchema(this.schema);
|
|
1241
1343
|
// Add columns as FieldRef objects - type-safe with property name and database column name
|
|
1242
|
-
for (const [colName,
|
|
1243
|
-
const dbColumnName = colBuilder.build().name;
|
|
1344
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
1244
1345
|
Object.defineProperty(mock, colName, {
|
|
1245
1346
|
get: () => ({
|
|
1246
1347
|
__fieldName: colName,
|
|
@@ -1257,13 +1358,12 @@ class SelectQueryBuilder {
|
|
|
1257
1358
|
if (join.isSubquery || !join.schema) {
|
|
1258
1359
|
continue;
|
|
1259
1360
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1361
|
+
// Performance: Use pre-computed column name map for joined schema
|
|
1362
|
+
const joinColumnNameMap = getColumnNameMapForSchema(join.schema);
|
|
1363
|
+
if (!mock[join.alias]) {
|
|
1364
|
+
mock[join.alias] = {};
|
|
1365
|
+
}
|
|
1366
|
+
for (const [colName, dbColumnName] of joinColumnNameMap) {
|
|
1267
1367
|
Object.defineProperty(mock[join.alias], colName, {
|
|
1268
1368
|
get: () => ({
|
|
1269
1369
|
__fieldName: colName,
|
|
@@ -1275,15 +1375,18 @@ class SelectQueryBuilder {
|
|
|
1275
1375
|
});
|
|
1276
1376
|
}
|
|
1277
1377
|
}
|
|
1378
|
+
// Performance: Use pre-computed relation entries
|
|
1379
|
+
const relationEntries = getRelationEntriesForSchema(this.schema);
|
|
1278
1380
|
// Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
|
|
1279
|
-
for (const [relName, relConfig] of
|
|
1280
|
-
// Try to get target schema from registry (preferred, has full relations) or
|
|
1381
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
1382
|
+
// Try to get target schema from registry (preferred, has full relations) or cached schema
|
|
1281
1383
|
let targetSchema;
|
|
1282
1384
|
if (this.schemaRegistry) {
|
|
1283
1385
|
targetSchema = this.schemaRegistry.get(relConfig.targetTable);
|
|
1284
1386
|
}
|
|
1285
|
-
if (!targetSchema
|
|
1286
|
-
|
|
1387
|
+
if (!targetSchema) {
|
|
1388
|
+
// Performance: Use cached target schema
|
|
1389
|
+
targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
|
|
1287
1390
|
}
|
|
1288
1391
|
if (relConfig.type === 'many') {
|
|
1289
1392
|
Object.defineProperty(mock, relName, {
|
|
@@ -1376,40 +1479,142 @@ class SelectQueryBuilder {
|
|
|
1376
1479
|
}
|
|
1377
1480
|
/**
|
|
1378
1481
|
* Detect navigation property references in selection and add necessary JOINs
|
|
1482
|
+
* Supports multi-level navigation like task.level.createdBy
|
|
1379
1483
|
*/
|
|
1380
1484
|
detectAndAddJoinsFromSelection(selection, joins) {
|
|
1381
1485
|
if (!selection || typeof selection !== 'object') {
|
|
1382
1486
|
return;
|
|
1383
1487
|
}
|
|
1384
|
-
|
|
1488
|
+
// First pass: collect all table aliases
|
|
1489
|
+
const allTableAliases = new Set();
|
|
1490
|
+
this.collectTableAliasesFromSelection(selection, allTableAliases);
|
|
1491
|
+
// Second pass: resolve all joins through the schema graph
|
|
1492
|
+
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Collect all table aliases from a selection
|
|
1496
|
+
*/
|
|
1497
|
+
collectTableAliasesFromSelection(selection, allTableAliases) {
|
|
1498
|
+
if (!selection || typeof selection !== 'object') {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
for (const [_key, value] of Object.entries(selection)) {
|
|
1385
1502
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
1386
|
-
// This is a FieldRef with a table alias
|
|
1503
|
+
// This is a FieldRef with a table alias
|
|
1387
1504
|
const tableAlias = value.__tableAlias;
|
|
1388
|
-
if (tableAlias !== this.schema.name
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1505
|
+
if (tableAlias && tableAlias !== this.schema.name) {
|
|
1506
|
+
allTableAliases.add(tableAlias);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
1510
|
+
// SqlFragment may contain navigation property references
|
|
1511
|
+
const fieldRefs = value.getFieldRefs();
|
|
1512
|
+
for (const fieldRef of fieldRefs) {
|
|
1513
|
+
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
1514
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
1515
|
+
if (tableAlias && tableAlias !== this.schema.name) {
|
|
1516
|
+
allTableAliases.add(tableAlias);
|
|
1397
1517
|
}
|
|
1398
|
-
// Add a JOIN for this reference
|
|
1399
|
-
joins.push({
|
|
1400
|
-
alias: tableAlias,
|
|
1401
|
-
targetTable: relation.targetTable,
|
|
1402
|
-
targetSchema,
|
|
1403
|
-
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1404
|
-
matches: relation.matches || [],
|
|
1405
|
-
isMandatory: relation.isMandatory ?? false,
|
|
1406
|
-
});
|
|
1407
1518
|
}
|
|
1408
1519
|
}
|
|
1409
1520
|
}
|
|
1410
|
-
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1521
|
+
else if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof CollectionQueryBuilder)) {
|
|
1411
1522
|
// Recursively check nested objects
|
|
1412
|
-
this.
|
|
1523
|
+
this.collectTableAliasesFromSelection(value, allTableAliases);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
1529
|
+
* This handles multi-level navigation like task.level.createdBy
|
|
1530
|
+
*/
|
|
1531
|
+
resolveJoinsForTableAliases(allTableAliases, joins) {
|
|
1532
|
+
if (allTableAliases.size === 0) {
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
// Keep resolving until we've resolved all aliases or can't make progress
|
|
1536
|
+
const resolved = new Set();
|
|
1537
|
+
let maxIterations = allTableAliases.size * 3; // Prevent infinite loops
|
|
1538
|
+
while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
|
|
1539
|
+
// Build a map of already joined schemas for path resolution
|
|
1540
|
+
const joinedSchemas = new Map();
|
|
1541
|
+
joinedSchemas.set(this.schema.name, this.schema);
|
|
1542
|
+
for (const join of joins) {
|
|
1543
|
+
let schema;
|
|
1544
|
+
if (this.schemaRegistry) {
|
|
1545
|
+
schema = this.schemaRegistry.get(join.targetTable);
|
|
1546
|
+
}
|
|
1547
|
+
if (schema) {
|
|
1548
|
+
joinedSchemas.set(join.alias, schema);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
// Try to resolve each unresolved alias
|
|
1552
|
+
for (const alias of allTableAliases) {
|
|
1553
|
+
if (resolved.has(alias) || joins.some(j => j.alias === alias)) {
|
|
1554
|
+
resolved.add(alias);
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
// Look for this alias in any of the already joined schemas
|
|
1558
|
+
for (const [sourceAlias, schema] of joinedSchemas) {
|
|
1559
|
+
if (schema.relations && schema.relations[alias]) {
|
|
1560
|
+
const relation = schema.relations[alias];
|
|
1561
|
+
if (relation.type === 'one') {
|
|
1562
|
+
// Get target schema
|
|
1563
|
+
let targetSchema;
|
|
1564
|
+
let targetSchemaName;
|
|
1565
|
+
if (this.schemaRegistry) {
|
|
1566
|
+
targetSchema = this.schemaRegistry.get(relation.targetTable);
|
|
1567
|
+
targetSchemaName = targetSchema?.schema;
|
|
1568
|
+
}
|
|
1569
|
+
if (!targetSchema && relation.targetTableBuilder) {
|
|
1570
|
+
targetSchema = relation.targetTableBuilder.build();
|
|
1571
|
+
targetSchemaName = targetSchema?.schema;
|
|
1572
|
+
}
|
|
1573
|
+
joins.push({
|
|
1574
|
+
alias,
|
|
1575
|
+
targetTable: relation.targetTable,
|
|
1576
|
+
targetSchema: targetSchemaName,
|
|
1577
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1578
|
+
matches: relation.matches || ['id'],
|
|
1579
|
+
isMandatory: relation.isMandatory ?? false,
|
|
1580
|
+
sourceAlias, // Track where this join comes from
|
|
1581
|
+
});
|
|
1582
|
+
resolved.add(alias);
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Add a JOIN for a FieldRef if it references a related table
|
|
1592
|
+
* @deprecated Use detectAndAddJoinsFromSelection with multi-level resolution instead
|
|
1593
|
+
*/
|
|
1594
|
+
addJoinForFieldRef(fieldRef, joins) {
|
|
1595
|
+
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
1599
|
+
if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
1600
|
+
// This references a related table - find the relation and add a JOIN
|
|
1601
|
+
const relation = this.schema.relations[tableAlias];
|
|
1602
|
+
if (relation && relation.type === 'one') {
|
|
1603
|
+
// Get target schema from targetTableBuilder if available
|
|
1604
|
+
let targetSchema;
|
|
1605
|
+
if (relation.targetTableBuilder) {
|
|
1606
|
+
const targetTableSchema = relation.targetTableBuilder.build();
|
|
1607
|
+
targetSchema = targetTableSchema.schema;
|
|
1608
|
+
}
|
|
1609
|
+
// Add a JOIN for this reference
|
|
1610
|
+
joins.push({
|
|
1611
|
+
alias: tableAlias,
|
|
1612
|
+
targetTable: relation.targetTable,
|
|
1613
|
+
targetSchema,
|
|
1614
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1615
|
+
matches: relation.matches || [],
|
|
1616
|
+
isMandatory: relation.isMandatory ?? false,
|
|
1617
|
+
});
|
|
1413
1618
|
}
|
|
1414
1619
|
}
|
|
1415
1620
|
}
|
|
@@ -1420,35 +1625,19 @@ class SelectQueryBuilder {
|
|
|
1420
1625
|
if (!condition) {
|
|
1421
1626
|
return;
|
|
1422
1627
|
}
|
|
1423
|
-
//
|
|
1628
|
+
// Collect all table aliases from the condition
|
|
1629
|
+
const allTableAliases = new Set();
|
|
1424
1630
|
const fieldRefs = condition.getFieldRefs();
|
|
1425
1631
|
for (const fieldRef of fieldRefs) {
|
|
1426
1632
|
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
1427
1633
|
const tableAlias = fieldRef.__tableAlias;
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
// Find the relation config for this navigation
|
|
1431
|
-
const relation = this.schema.relations[tableAlias];
|
|
1432
|
-
if (relation && relation.type === 'one') {
|
|
1433
|
-
// Get target schema from targetTableBuilder if available
|
|
1434
|
-
let targetSchema;
|
|
1435
|
-
if (relation.targetTableBuilder) {
|
|
1436
|
-
const targetTableSchema = relation.targetTableBuilder.build();
|
|
1437
|
-
targetSchema = targetTableSchema.schema;
|
|
1438
|
-
}
|
|
1439
|
-
// Add a JOIN for this reference
|
|
1440
|
-
joins.push({
|
|
1441
|
-
alias: tableAlias,
|
|
1442
|
-
targetTable: relation.targetTable,
|
|
1443
|
-
targetSchema,
|
|
1444
|
-
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1445
|
-
matches: relation.matches || [],
|
|
1446
|
-
isMandatory: relation.isMandatory ?? false,
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1634
|
+
if (tableAlias !== this.schema.name) {
|
|
1635
|
+
allTableAliases.add(tableAlias);
|
|
1449
1636
|
}
|
|
1450
1637
|
}
|
|
1451
1638
|
}
|
|
1639
|
+
// Resolve all joins through the schema graph
|
|
1640
|
+
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
1452
1641
|
}
|
|
1453
1642
|
/**
|
|
1454
1643
|
* Build SQL query
|
|
@@ -1530,6 +1719,7 @@ class SelectQueryBuilder {
|
|
|
1530
1719
|
if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
|
|
1531
1720
|
// This is a field from a joined table
|
|
1532
1721
|
const tableAlias = value.__tableAlias;
|
|
1722
|
+
const columnName = value.__dbColumnName;
|
|
1533
1723
|
// Find the relation config for this navigation
|
|
1534
1724
|
const relConfig = this.schema.relations[tableAlias];
|
|
1535
1725
|
if (relConfig) {
|
|
@@ -1551,7 +1741,15 @@ class SelectQueryBuilder {
|
|
|
1551
1741
|
});
|
|
1552
1742
|
}
|
|
1553
1743
|
}
|
|
1554
|
-
|
|
1744
|
+
// Check if this is a CTE aggregation column that needs COALESCE
|
|
1745
|
+
const cteJoin = this.manualJoins.find(j => j.cte && j.cte.name === tableAlias);
|
|
1746
|
+
if (cteJoin && cteJoin.cte && cteJoin.cte.isAggregationColumn(columnName)) {
|
|
1747
|
+
// CTE aggregation column - wrap with COALESCE to return empty array instead of null
|
|
1748
|
+
selectParts.push(`COALESCE("${tableAlias}"."${columnName}", '[]'::jsonb) as "${key}"`);
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
selectParts.push(`"${tableAlias}"."${columnName}" as "${key}"`);
|
|
1752
|
+
}
|
|
1555
1753
|
}
|
|
1556
1754
|
else {
|
|
1557
1755
|
// Regular field from the main table
|
|
@@ -1599,9 +1797,10 @@ class SelectQueryBuilder {
|
|
|
1599
1797
|
// Select all columns from the target table and group them
|
|
1600
1798
|
// We'll need to use JSON object building in SQL
|
|
1601
1799
|
const fieldParts = [];
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1800
|
+
// Performance: Use cached column name map
|
|
1801
|
+
const targetColMap = getColumnNameMapForSchema(targetSchema);
|
|
1802
|
+
for (const [colKey, dbColName] of targetColMap) {
|
|
1803
|
+
fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
|
|
1605
1804
|
}
|
|
1606
1805
|
selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
|
|
1607
1806
|
}
|
|
@@ -1631,11 +1830,8 @@ class SelectQueryBuilder {
|
|
|
1631
1830
|
const relConfig = this.schema.relations[alias];
|
|
1632
1831
|
if (relConfig && relConfig.type === 'one') {
|
|
1633
1832
|
// This is a reference navigation - select all fields from the target table
|
|
1634
|
-
//
|
|
1635
|
-
|
|
1636
|
-
if (relConfig.targetTableBuilder) {
|
|
1637
|
-
targetSchema = relConfig.targetTableBuilder.build();
|
|
1638
|
-
}
|
|
1833
|
+
// Performance: Use cached target schema
|
|
1834
|
+
const targetSchema = getTargetSchemaForRelation(this.schema, alias, relConfig);
|
|
1639
1835
|
if (targetSchema) {
|
|
1640
1836
|
// Add JOIN if not already added
|
|
1641
1837
|
if (!joins.find(j => j.alias === alias)) {
|
|
@@ -1654,9 +1850,10 @@ class SelectQueryBuilder {
|
|
|
1654
1850
|
}
|
|
1655
1851
|
// Select all columns from the target table and group them into a JSON object
|
|
1656
1852
|
const fieldParts = [];
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1853
|
+
// Performance: Use cached column name map
|
|
1854
|
+
const targetColMap = getColumnNameMapForSchema(targetSchema);
|
|
1855
|
+
for (const [colKey, dbColName] of targetColMap) {
|
|
1856
|
+
fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
|
|
1660
1857
|
}
|
|
1661
1858
|
selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
|
|
1662
1859
|
continue;
|
|
@@ -1811,11 +2008,14 @@ class SelectQueryBuilder {
|
|
|
1811
2008
|
for (const join of joins) {
|
|
1812
2009
|
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1813
2010
|
// Build ON clause for the join
|
|
2011
|
+
// For multi-level navigation, use the sourceAlias (the intermediate table)
|
|
2012
|
+
// For direct navigation, use the main table name
|
|
2013
|
+
const sourceTable = join.sourceAlias || this.schema.name;
|
|
1814
2014
|
const onConditions = [];
|
|
1815
2015
|
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
1816
2016
|
const fk = join.foreignKeys[i];
|
|
1817
2017
|
const match = join.matches[i];
|
|
1818
|
-
onConditions.push(`"${
|
|
2018
|
+
onConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
|
|
1819
2019
|
}
|
|
1820
2020
|
// Use schema-qualified table name if schema is specified
|
|
1821
2021
|
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
@@ -1947,6 +2147,17 @@ class SelectQueryBuilder {
|
|
|
1947
2147
|
}
|
|
1948
2148
|
}
|
|
1949
2149
|
}
|
|
2150
|
+
else if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
|
|
2151
|
+
// CTE withAggregation array - apply mappers to items inside
|
|
2152
|
+
const collectionItems = row[key] || [];
|
|
2153
|
+
const innerMetadata = value.__innerSelectionMetadata;
|
|
2154
|
+
if (innerMetadata && !disableMappers) {
|
|
2155
|
+
result[key] = this.transformCteAggregationItems(collectionItems, innerMetadata);
|
|
2156
|
+
}
|
|
2157
|
+
else {
|
|
2158
|
+
result[key] = collectionItems;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
1950
2161
|
else if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
1951
2162
|
// SqlFragment with custom mapper (check this BEFORE FieldRef to handle subquery/CTE fields with mappers)
|
|
1952
2163
|
const rawValue = row[key];
|
|
@@ -2059,6 +2270,54 @@ class SelectQueryBuilder {
|
|
|
2059
2270
|
return transformedItem;
|
|
2060
2271
|
});
|
|
2061
2272
|
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Transform CTE aggregation items applying fromDriver mappers from selection metadata
|
|
2275
|
+
*/
|
|
2276
|
+
transformCteAggregationItems(items, selectionMetadata) {
|
|
2277
|
+
if (!items || items.length === 0) {
|
|
2278
|
+
return [];
|
|
2279
|
+
}
|
|
2280
|
+
// Build mapper cache from selection metadata
|
|
2281
|
+
const mapperCache = {};
|
|
2282
|
+
for (const [key, value] of Object.entries(selectionMetadata)) {
|
|
2283
|
+
// Check if value has getMapper (SqlFragment or field with mapper)
|
|
2284
|
+
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
2285
|
+
let mapper = value.getMapper();
|
|
2286
|
+
// If mapper is a CustomTypeBuilder, get the actual type
|
|
2287
|
+
if (mapper && typeof mapper.getType === 'function') {
|
|
2288
|
+
mapper = mapper.getType();
|
|
2289
|
+
}
|
|
2290
|
+
if (mapper && typeof mapper.fromDriver === 'function') {
|
|
2291
|
+
mapperCache[key] = mapper;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
// Check if it's a FieldRef with schema column mapper
|
|
2295
|
+
else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
2296
|
+
const fieldName = value.__fieldName;
|
|
2297
|
+
const column = this.schema.columns[fieldName];
|
|
2298
|
+
if (column) {
|
|
2299
|
+
const config = column.build();
|
|
2300
|
+
if (config.mapper && typeof config.mapper.fromDriver === 'function') {
|
|
2301
|
+
mapperCache[key] = config.mapper;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
// Transform items
|
|
2307
|
+
return items.map(item => {
|
|
2308
|
+
const transformedItem = {};
|
|
2309
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2310
|
+
const mapper = mapperCache[key];
|
|
2311
|
+
if (mapper && value !== null && value !== undefined) {
|
|
2312
|
+
transformedItem[key] = mapper.fromDriver(value);
|
|
2313
|
+
}
|
|
2314
|
+
else {
|
|
2315
|
+
transformedItem[key] = value;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return transformedItem;
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2062
2321
|
/**
|
|
2063
2322
|
* Build aggregation query (MIN, MAX, SUM)
|
|
2064
2323
|
*/
|
|
@@ -2301,9 +2560,9 @@ class ReferenceQueryBuilder {
|
|
|
2301
2560
|
createMockTargetRow() {
|
|
2302
2561
|
if (this.targetTableSchema) {
|
|
2303
2562
|
const mock = {};
|
|
2304
|
-
// Add columns
|
|
2305
|
-
|
|
2306
|
-
|
|
2563
|
+
// Add columns - use pre-computed column name map if available
|
|
2564
|
+
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
2565
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
2307
2566
|
Object.defineProperty(mock, colName, {
|
|
2308
2567
|
get: () => ({
|
|
2309
2568
|
__fieldName: colName,
|
|
@@ -2356,15 +2615,8 @@ class ReferenceQueryBuilder {
|
|
|
2356
2615
|
return mock;
|
|
2357
2616
|
}
|
|
2358
2617
|
else {
|
|
2359
|
-
// Fallback:
|
|
2360
|
-
|
|
2361
|
-
get: (target, prop) => ({
|
|
2362
|
-
__fieldName: prop,
|
|
2363
|
-
__dbColumnName: prop,
|
|
2364
|
-
__tableAlias: this.relationName,
|
|
2365
|
-
}),
|
|
2366
|
-
};
|
|
2367
|
-
return new Proxy({}, handler);
|
|
2618
|
+
// Fallback: use the shared nested proxy that supports deep property access
|
|
2619
|
+
return createNestedFieldRefProxy(this.relationName);
|
|
2368
2620
|
}
|
|
2369
2621
|
}
|
|
2370
2622
|
}
|
|
@@ -2399,7 +2651,8 @@ class CollectionQueryBuilder {
|
|
|
2399
2651
|
* Select specific fields from collection items
|
|
2400
2652
|
*/
|
|
2401
2653
|
select(selector) {
|
|
2402
|
-
const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema
|
|
2654
|
+
const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
2655
|
+
);
|
|
2403
2656
|
newBuilder.selector = selector;
|
|
2404
2657
|
newBuilder.whereCond = this.whereCond;
|
|
2405
2658
|
newBuilder.limitValue = this.limitValue;
|
|
@@ -2444,14 +2697,10 @@ class CollectionQueryBuilder {
|
|
|
2444
2697
|
if (this.targetTableSchema) {
|
|
2445
2698
|
// If we have schema information, create a properly typed mock
|
|
2446
2699
|
const mock = {};
|
|
2447
|
-
// Performance:
|
|
2448
|
-
const
|
|
2449
|
-
const columnConfigs = new Map();
|
|
2450
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
2451
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
2452
|
-
}
|
|
2700
|
+
// Performance: Use pre-computed column name map if available
|
|
2701
|
+
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
2453
2702
|
// Add columns
|
|
2454
|
-
for (const [colName, dbColumnName] of
|
|
2703
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
2455
2704
|
Object.defineProperty(mock, colName, {
|
|
2456
2705
|
get: () => ({
|
|
2457
2706
|
__fieldName: colName,
|
|
@@ -2500,14 +2749,8 @@ class CollectionQueryBuilder {
|
|
|
2500
2749
|
return mock;
|
|
2501
2750
|
}
|
|
2502
2751
|
else {
|
|
2503
|
-
// Fallback:
|
|
2504
|
-
|
|
2505
|
-
get: (target, prop) => ({
|
|
2506
|
-
__fieldName: prop,
|
|
2507
|
-
__dbColumnName: prop,
|
|
2508
|
-
}),
|
|
2509
|
-
};
|
|
2510
|
-
return new Proxy({}, handler);
|
|
2752
|
+
// Fallback: use the shared nested proxy that supports deep property access
|
|
2753
|
+
return createNestedFieldRefProxy(this.targetTable);
|
|
2511
2754
|
}
|
|
2512
2755
|
}
|
|
2513
2756
|
/**
|
|
@@ -2642,6 +2885,151 @@ class CollectionQueryBuilder {
|
|
|
2642
2885
|
getFlattenResultType() {
|
|
2643
2886
|
return this.flattenResultType;
|
|
2644
2887
|
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Detect navigation property references in the selected fields and add necessary JOINs
|
|
2890
|
+
* This supports multi-level navigation like p.task.level.createdBy.username
|
|
2891
|
+
*/
|
|
2892
|
+
detectNavigationJoins(selection, joins, currentSourceAlias, currentSchema) {
|
|
2893
|
+
if (!selection || typeof selection !== 'object') {
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
// Collect all table aliases referenced in the selection
|
|
2897
|
+
const allTableAliases = new Set();
|
|
2898
|
+
// Helper to collect from a single selection
|
|
2899
|
+
const collectFromSelection = (sel) => {
|
|
2900
|
+
if (!sel || typeof sel !== 'object') {
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
// Handle single FieldRef
|
|
2904
|
+
if ('__tableAlias' in sel && '__dbColumnName' in sel) {
|
|
2905
|
+
this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
// Handle object with multiple fields
|
|
2909
|
+
for (const [_key, value] of Object.entries(sel)) {
|
|
2910
|
+
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
2911
|
+
// This is a FieldRef with a table alias
|
|
2912
|
+
this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
2913
|
+
}
|
|
2914
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
2915
|
+
// SqlFragment may contain navigation property references
|
|
2916
|
+
const fieldRefs = value.getFieldRefs();
|
|
2917
|
+
for (const fieldRef of fieldRefs) {
|
|
2918
|
+
this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
else if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof CollectionQueryBuilder)) {
|
|
2922
|
+
// Recursively check nested objects
|
|
2923
|
+
collectFromSelection(value);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
// First pass: collect all table aliases
|
|
2928
|
+
collectFromSelection(selection);
|
|
2929
|
+
// Second pass: resolve all navigation joins by finding the correct path through schemas
|
|
2930
|
+
if (allTableAliases.size > 0) {
|
|
2931
|
+
this.resolveNavigationJoins(allTableAliases, joins, currentSchema);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Add a navigation JOIN for a FieldRef if it references a related table
|
|
2936
|
+
* Handles multi-level navigation by recursively resolving the join chain
|
|
2937
|
+
*/
|
|
2938
|
+
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases) {
|
|
2939
|
+
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
2943
|
+
// If this references the target table directly, no join needed
|
|
2944
|
+
if (!tableAlias || tableAlias === this.targetTable) {
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
// Collect this table alias for later resolution
|
|
2948
|
+
allTableAliases.add(tableAlias);
|
|
2949
|
+
// Check if we already have this join
|
|
2950
|
+
if (joins.some(j => j.alias === tableAlias)) {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
// Find the relation in the current schema
|
|
2954
|
+
const relation = sourceSchema.relations?.[tableAlias];
|
|
2955
|
+
if (relation && relation.type === 'one') {
|
|
2956
|
+
this.addNavigationJoin(tableAlias, relation, joins, sourceAlias);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Add a navigation join and return the target schema
|
|
2961
|
+
*/
|
|
2962
|
+
addNavigationJoin(alias, relation, joins, sourceAlias) {
|
|
2963
|
+
// Check if already added
|
|
2964
|
+
if (joins.some(j => j.alias === alias)) {
|
|
2965
|
+
return undefined;
|
|
2966
|
+
}
|
|
2967
|
+
// Get the target table schema
|
|
2968
|
+
let targetSchema;
|
|
2969
|
+
let targetSchemaName;
|
|
2970
|
+
if (this.schemaRegistry) {
|
|
2971
|
+
targetSchema = this.schemaRegistry.get(relation.targetTable);
|
|
2972
|
+
targetSchemaName = targetSchema?.schema;
|
|
2973
|
+
}
|
|
2974
|
+
if (!targetSchema && relation.targetTableBuilder) {
|
|
2975
|
+
targetSchema = relation.targetTableBuilder.build();
|
|
2976
|
+
targetSchemaName = targetSchema?.schema;
|
|
2977
|
+
}
|
|
2978
|
+
// Build the join info
|
|
2979
|
+
const foreignKeys = relation.foreignKeys || [relation.foreignKey || ''];
|
|
2980
|
+
const matches = relation.matches || ['id']; // Default to 'id' as the PK
|
|
2981
|
+
joins.push({
|
|
2982
|
+
alias,
|
|
2983
|
+
targetTable: relation.targetTable,
|
|
2984
|
+
targetSchema: targetSchemaName,
|
|
2985
|
+
foreignKeys,
|
|
2986
|
+
matches,
|
|
2987
|
+
isMandatory: relation.isMandatory ?? false,
|
|
2988
|
+
sourceAlias,
|
|
2989
|
+
});
|
|
2990
|
+
return targetSchema;
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
2994
|
+
* This handles multi-level navigation like task.level.createdBy
|
|
2995
|
+
*/
|
|
2996
|
+
resolveNavigationJoins(allTableAliases, joins, startSchema) {
|
|
2997
|
+
// Keep resolving until we've resolved all aliases or can't make progress
|
|
2998
|
+
let resolved = new Set();
|
|
2999
|
+
let maxIterations = allTableAliases.size * 2; // Prevent infinite loops
|
|
3000
|
+
while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
|
|
3001
|
+
// Build a map of already joined schemas for path resolution
|
|
3002
|
+
const joinedSchemas = new Map();
|
|
3003
|
+
joinedSchemas.set(this.targetTable, startSchema);
|
|
3004
|
+
for (const join of joins) {
|
|
3005
|
+
let schema;
|
|
3006
|
+
if (this.schemaRegistry) {
|
|
3007
|
+
schema = this.schemaRegistry.get(join.targetTable);
|
|
3008
|
+
}
|
|
3009
|
+
if (schema) {
|
|
3010
|
+
joinedSchemas.set(join.alias, schema);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
// Try to resolve each unresolved alias
|
|
3014
|
+
for (const alias of allTableAliases) {
|
|
3015
|
+
if (resolved.has(alias) || joins.some(j => j.alias === alias)) {
|
|
3016
|
+
resolved.add(alias);
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
// Look for this alias in any of the already joined schemas
|
|
3020
|
+
for (const [schemaAlias, schema] of joinedSchemas) {
|
|
3021
|
+
if (schema.relations && schema.relations[alias]) {
|
|
3022
|
+
const relation = schema.relations[alias];
|
|
3023
|
+
if (relation.type === 'one') {
|
|
3024
|
+
this.addNavigationJoin(alias, relation, joins, schemaAlias);
|
|
3025
|
+
resolved.add(alias);
|
|
3026
|
+
break;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
2645
3033
|
/**
|
|
2646
3034
|
* Build CTE for this collection query
|
|
2647
3035
|
* Now delegates to collection strategy pattern
|
|
@@ -2676,8 +3064,13 @@ class CollectionQueryBuilder {
|
|
|
2676
3064
|
return { alias, expression: fragmentSql };
|
|
2677
3065
|
}
|
|
2678
3066
|
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
2679
|
-
// FieldRef object - use database column name
|
|
3067
|
+
// FieldRef object - use database column name with optional table alias
|
|
2680
3068
|
const dbColumnName = field.__dbColumnName;
|
|
3069
|
+
const tableAlias = field.__tableAlias;
|
|
3070
|
+
// If tableAlias differs from the target table, it's a navigation property reference
|
|
3071
|
+
if (tableAlias && tableAlias !== this.targetTable) {
|
|
3072
|
+
return { alias, expression: `"${tableAlias}"."${dbColumnName}"` };
|
|
3073
|
+
}
|
|
2681
3074
|
return { alias, expression: `"${dbColumnName}"` };
|
|
2682
3075
|
}
|
|
2683
3076
|
else if (typeof field === 'string') {
|
|
@@ -2724,9 +3117,9 @@ class CollectionQueryBuilder {
|
|
|
2724
3117
|
else {
|
|
2725
3118
|
// No selector - select all fields from the target table schema
|
|
2726
3119
|
if (this.targetTableSchema && this.targetTableSchema.columns) {
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3120
|
+
// Performance: Use cached column name map
|
|
3121
|
+
const colNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
3122
|
+
for (const [colName, dbColumnName] of colNameMap) {
|
|
2730
3123
|
selectedFieldConfigs.push({
|
|
2731
3124
|
alias: colName,
|
|
2732
3125
|
expression: `"${dbColumnName}"`,
|
|
@@ -2756,13 +3149,11 @@ class CollectionQueryBuilder {
|
|
|
2756
3149
|
// Step 3: Build ORDER BY clause SQL (without ORDER BY keyword)
|
|
2757
3150
|
let orderByClause;
|
|
2758
3151
|
if (this.orderByFields.length > 0) {
|
|
3152
|
+
// Performance: Pre-compute column name map for ORDER BY lookups
|
|
3153
|
+
const colNameMap = this.targetTableSchema ? getColumnNameMapForSchema(this.targetTableSchema) : null;
|
|
2759
3154
|
const orderParts = this.orderByFields.map(({ field, direction }) => {
|
|
2760
|
-
// Look up the database column name from the
|
|
2761
|
-
|
|
2762
|
-
if (this.targetTableSchema && this.targetTableSchema.columns[field]) {
|
|
2763
|
-
const colBuilder = this.targetTableSchema.columns[field];
|
|
2764
|
-
dbColumnName = colBuilder.build().name;
|
|
2765
|
-
}
|
|
3155
|
+
// Look up the database column name from the cached map if available
|
|
3156
|
+
const dbColumnName = colNameMap?.get(field) ?? field;
|
|
2766
3157
|
return `"${dbColumnName}" ${direction}`;
|
|
2767
3158
|
});
|
|
2768
3159
|
orderByClause = orderParts.join(', ');
|
|
@@ -2802,7 +3193,14 @@ class CollectionQueryBuilder {
|
|
|
2802
3193
|
aggregationType = 'jsonb';
|
|
2803
3194
|
defaultValue = "'[]'::jsonb";
|
|
2804
3195
|
}
|
|
2805
|
-
// Step 5:
|
|
3196
|
+
// Step 5: Detect navigation joins from the selected fields
|
|
3197
|
+
const navigationJoins = [];
|
|
3198
|
+
if (this.selector && this.targetTableSchema) {
|
|
3199
|
+
const mockItem = this.createMockItem();
|
|
3200
|
+
const selectedFields = this.selector(mockItem);
|
|
3201
|
+
this.detectNavigationJoins(selectedFields, navigationJoins, this.targetTable, this.targetTableSchema);
|
|
3202
|
+
}
|
|
3203
|
+
// Step 6: Build CollectionAggregationConfig object
|
|
2806
3204
|
const config = {
|
|
2807
3205
|
relationName: this.relationName,
|
|
2808
3206
|
targetTable: this.targetTable,
|
|
@@ -2821,6 +3219,7 @@ class CollectionQueryBuilder {
|
|
|
2821
3219
|
arrayField,
|
|
2822
3220
|
defaultValue,
|
|
2823
3221
|
counter: context.cteCounter++,
|
|
3222
|
+
navigationJoins: navigationJoins.length > 0 ? navigationJoins : undefined,
|
|
2824
3223
|
};
|
|
2825
3224
|
// Step 6: Call the strategy
|
|
2826
3225
|
const result = strategy.buildAggregation(config, context, client);
|