@danceroutine/tango-orm 1.4.0 → 1.6.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/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/manager/ModelManager.d.ts +11 -11
- package/dist/manager/index.js +2 -2
- package/dist/manager-C6oJ2tAF.js +1 -1
- package/dist/query/QuerySet.d.ts +38 -16
- package/dist/query/compiler/QueryCompiler.d.ts +14 -21
- package/dist/query/domain/CompiledQuery.d.ts +26 -9
- package/dist/query/domain/QueryResult.d.ts +34 -3
- package/dist/query/domain/RelationMeta.d.ts +37 -0
- package/dist/query/domain/RelationTyping.d.ts +42 -0
- package/dist/query/domain/TableMeta.d.ts +9 -0
- package/dist/query/domain/TableMetaFactory.d.ts +23 -0
- package/dist/query/domain/index.d.ts +3 -2
- package/dist/query/index.d.ts +1 -0
- package/dist/query/index.js +2 -2
- package/dist/query/planning/QueryPlanner.d.ts +16 -0
- package/dist/query/planning/domain/QueryHydrationPlan.d.ts +20 -0
- package/dist/query/planning/index.d.ts +2 -0
- package/dist/query-C6So1r6H.js +1198 -0
- package/dist/query-C6So1r6H.js.map +1 -0
- package/dist/{registerModelObjects-Bva_f-Qh.js → registerModelObjects-BKMpfc4Z.js} +4 -28
- package/dist/registerModelObjects-BKMpfc4Z.js.map +1 -0
- package/dist/runtime/index.js +2 -2
- package/dist/runtime-ByXbpVBS.js +1 -1
- package/package.json +6 -6
- package/dist/query-CWZ1cfjo.js +0 -856
- package/dist/query-CWZ1cfjo.js.map +0 -1
- package/dist/registerModelObjects-Bva_f-Qh.js.map +0 -1
|
@@ -0,0 +1,1198 @@
|
|
|
1
|
+
import { __export } from "./chunk-DLY2FNSh.js";
|
|
2
|
+
import { SqlSafetyEngine, getLogger, isError } from "@danceroutine/tango-core";
|
|
3
|
+
import { ModelRegistry } from "@danceroutine/tango-schema";
|
|
4
|
+
|
|
5
|
+
//#region src/query/domain/internal/InternalRelationKind.ts
|
|
6
|
+
const InternalRelationKind = {
|
|
7
|
+
HAS_MANY: "hasMany",
|
|
8
|
+
BELONGS_TO: "belongsTo",
|
|
9
|
+
HAS_ONE: "hasOne",
|
|
10
|
+
MANY_TO_MANY: "manyToMany"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/query/domain/TableMetaFactory.ts
|
|
15
|
+
var TableMetaFactory = class TableMetaFactory {
|
|
16
|
+
static create(model) {
|
|
17
|
+
const owner = model.metadata.key ? ModelRegistry.getOwner(model) : undefined;
|
|
18
|
+
const cache = new Map();
|
|
19
|
+
return TableMetaFactory.createWithCache(model, owner, cache);
|
|
20
|
+
}
|
|
21
|
+
static createWithCache(model, owner, cache) {
|
|
22
|
+
if (model.metadata.key) {
|
|
23
|
+
const cached = cache.get(model.metadata.key);
|
|
24
|
+
if (cached) return cached;
|
|
25
|
+
}
|
|
26
|
+
const pkField = model.metadata.fields.find((field) => field.primaryKey);
|
|
27
|
+
if (!pkField) throw new Error(`Model '${model.metadata.name}' cannot attach a manager without a primary key field.`);
|
|
28
|
+
const tableMeta = {
|
|
29
|
+
modelKey: model.metadata.key,
|
|
30
|
+
table: model.metadata.table,
|
|
31
|
+
pk: pkField.name,
|
|
32
|
+
columns: Object.fromEntries(model.metadata.fields.map((field) => [field.name, field.type]))
|
|
33
|
+
};
|
|
34
|
+
if (model.metadata.key) cache.set(model.metadata.key, tableMeta);
|
|
35
|
+
if (!model.metadata.key || !owner) return tableMeta;
|
|
36
|
+
const relations = owner.getResolvedRelationGraph().byModel.get(model.metadata.key);
|
|
37
|
+
if (!relations || relations.size === 0) return tableMeta;
|
|
38
|
+
tableMeta.relations = Object.fromEntries(Array.from(relations.entries()).filter(([, relation]) => relation.capabilities.queryable && relation.capabilities.hydratable).map(([name, relation]) => {
|
|
39
|
+
const targetModel = owner.getByKey(relation.targetModelKey);
|
|
40
|
+
const targetMeta = TableMetaFactory.createWithCache(targetModel, owner, cache);
|
|
41
|
+
const { queryable, hydratable } = relation.capabilities;
|
|
42
|
+
const isSingleRelation = relation.kind === InternalRelationKind.BELONGS_TO || relation.kind === InternalRelationKind.HAS_ONE;
|
|
43
|
+
const sourceKey = relation.kind === InternalRelationKind.BELONGS_TO ? relation.localFieldName : relation.targetFieldName;
|
|
44
|
+
const targetKey = relation.kind === InternalRelationKind.BELONGS_TO ? relation.targetFieldName : relation.localFieldName;
|
|
45
|
+
const targetColumns = Object.fromEntries(targetModel.metadata.fields.map((field) => [field.name, field.type]));
|
|
46
|
+
const capabilities = {
|
|
47
|
+
queryable,
|
|
48
|
+
hydratable,
|
|
49
|
+
joinable: isSingleRelation && queryable && hydratable,
|
|
50
|
+
prefetchable: queryable && hydratable
|
|
51
|
+
};
|
|
52
|
+
return [name, {
|
|
53
|
+
edgeId: relation.edgeId,
|
|
54
|
+
sourceModelKey: relation.sourceModelKey,
|
|
55
|
+
targetModelKey: relation.targetModelKey,
|
|
56
|
+
kind: relation.kind,
|
|
57
|
+
cardinality: isSingleRelation ? "single" : "many",
|
|
58
|
+
capabilities,
|
|
59
|
+
table: targetModel.metadata.table,
|
|
60
|
+
sourceKey,
|
|
61
|
+
targetKey,
|
|
62
|
+
targetPrimaryKey: targetMeta.pk,
|
|
63
|
+
targetColumns,
|
|
64
|
+
alias: relation.alias,
|
|
65
|
+
targetMeta
|
|
66
|
+
}];
|
|
67
|
+
}));
|
|
68
|
+
return tableMeta;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/query/domain/RelationMeta.ts
|
|
74
|
+
const InternalRelationHydrationLoadMode = {
|
|
75
|
+
JOIN: "join",
|
|
76
|
+
PREFETCH: "prefetch"
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/query/domain/internal/InternalDialect.ts
|
|
81
|
+
const InternalDialect = {
|
|
82
|
+
POSTGRES: "postgres",
|
|
83
|
+
SQLITE: "sqlite"
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/query/domain/internal/InternalQNodeType.ts
|
|
88
|
+
const InternalQNodeType = {
|
|
89
|
+
ATOM: "atom",
|
|
90
|
+
AND: "and",
|
|
91
|
+
OR: "or",
|
|
92
|
+
NOT: "not"
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/query/domain/internal/InternalLookupType.ts
|
|
97
|
+
const InternalLookupType = {
|
|
98
|
+
EXACT: "exact",
|
|
99
|
+
LT: "lt",
|
|
100
|
+
LTE: "lte",
|
|
101
|
+
GT: "gt",
|
|
102
|
+
GTE: "gte",
|
|
103
|
+
IN: "in",
|
|
104
|
+
ISNULL: "isnull",
|
|
105
|
+
CONTAINS: "contains",
|
|
106
|
+
ICONTAINS: "icontains",
|
|
107
|
+
STARTSWITH: "startswith",
|
|
108
|
+
ISTARTSWITH: "istartswith",
|
|
109
|
+
ENDSWITH: "endswith",
|
|
110
|
+
IENDSWITH: "iendswith"
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/validation/OrmSqlSafetyAdapter.ts
|
|
115
|
+
const ALLOWED_LOOKUPS = Object.values(InternalLookupType);
|
|
116
|
+
var OrmSqlSafetyAdapter = class OrmSqlSafetyAdapter {
|
|
117
|
+
static BRAND = "tango.orm.orm_sql_safety_adapter";
|
|
118
|
+
__tangoBrand = OrmSqlSafetyAdapter.BRAND;
|
|
119
|
+
constructor(engine = new SqlSafetyEngine()) {
|
|
120
|
+
this.engine = engine;
|
|
121
|
+
}
|
|
122
|
+
validate(plan) {
|
|
123
|
+
switch (plan.kind) {
|
|
124
|
+
case "select": {
|
|
125
|
+
const meta = this.validateTableMeta(plan.meta, plan.relationNames ?? []);
|
|
126
|
+
return {
|
|
127
|
+
kind: "select",
|
|
128
|
+
meta,
|
|
129
|
+
selectFields: Object.fromEntries((plan.selectFields ?? []).map((field) => [field, `${meta.table}.${this.resolveColumn(meta, field)}`])),
|
|
130
|
+
filterKeys: Object.fromEntries((plan.filterKeys ?? []).map((rawKey) => [rawKey, this.validateFilterKey(meta, rawKey)])),
|
|
131
|
+
orderFields: Object.fromEntries((plan.orderFields ?? []).map((field) => [field, `${meta.table}.${this.resolveColumn(meta, field)}`])),
|
|
132
|
+
relations: Object.fromEntries((plan.relationNames ?? []).map((relationName) => [relationName, this.resolveRelation(meta, relationName)]))
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
case "insert": {
|
|
136
|
+
const meta = this.validateTableMeta(plan.meta);
|
|
137
|
+
return {
|
|
138
|
+
kind: "insert",
|
|
139
|
+
meta,
|
|
140
|
+
writeKeys: plan.writeKeys.map((key) => this.resolveColumn(meta, key))
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
case "update": {
|
|
144
|
+
const meta = this.validateTableMeta(plan.meta);
|
|
145
|
+
return {
|
|
146
|
+
kind: "update",
|
|
147
|
+
meta,
|
|
148
|
+
writeKeys: plan.writeKeys.map((key) => this.resolveColumn(meta, key))
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
case "delete": return {
|
|
152
|
+
kind: "delete",
|
|
153
|
+
meta: this.validateTableMeta(plan.meta)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
validateTableMeta(meta, relationNames = []) {
|
|
158
|
+
const columnNames = Object.keys(meta.columns);
|
|
159
|
+
const validated = this.engine.validate({ identifiers: [
|
|
160
|
+
{
|
|
161
|
+
key: "table",
|
|
162
|
+
role: "table",
|
|
163
|
+
value: meta.table
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
key: "pk",
|
|
167
|
+
role: "primaryKey",
|
|
168
|
+
value: meta.pk,
|
|
169
|
+
allowlist: columnNames
|
|
170
|
+
},
|
|
171
|
+
...columnNames.map((column) => ({
|
|
172
|
+
key: `column:${column}`,
|
|
173
|
+
role: "column",
|
|
174
|
+
value: column
|
|
175
|
+
}))
|
|
176
|
+
] });
|
|
177
|
+
const validatedMeta = {
|
|
178
|
+
table: validated.identifiers.table.value,
|
|
179
|
+
pk: validated.identifiers.pk.value,
|
|
180
|
+
columns: Object.fromEntries(columnNames.map((column) => [validated.identifiers[`column:${column}`].value, meta.columns[column]]))
|
|
181
|
+
};
|
|
182
|
+
if (!(validatedMeta.pk in validatedMeta.columns)) throw new Error(`Unknown column '${validatedMeta.pk}' for table '${validatedMeta.table}'.`);
|
|
183
|
+
if (relationNames.length > 0) validatedMeta.relations = Object.fromEntries(relationNames.map((relationName) => [relationName, this.validateRelationMeta(validatedMeta, relationName, meta.relations)]));
|
|
184
|
+
return validatedMeta;
|
|
185
|
+
}
|
|
186
|
+
validateRelationMeta(meta, relationName, relations) {
|
|
187
|
+
const relation = relations?.[relationName];
|
|
188
|
+
if (!relation) throw new Error(`Unknown relation '${relationName}' for table '${meta.table}'.`);
|
|
189
|
+
if (!(relation.targetKey in relation.targetColumns)) throw new Error(`Unknown relation target key '${relation.targetKey}' for relation '${relationName}'.`);
|
|
190
|
+
const validated = this.engine.validate({ identifiers: [
|
|
191
|
+
{
|
|
192
|
+
key: "table",
|
|
193
|
+
role: "relationTable",
|
|
194
|
+
value: relation.table
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: "alias",
|
|
198
|
+
role: "alias",
|
|
199
|
+
value: relation.alias
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: "targetKey",
|
|
203
|
+
role: "relationTargetPrimaryKey",
|
|
204
|
+
value: relation.targetKey
|
|
205
|
+
},
|
|
206
|
+
...Object.keys(relation.targetColumns).map((column) => ({
|
|
207
|
+
key: `targetColumn:${column}`,
|
|
208
|
+
role: "column",
|
|
209
|
+
value: column
|
|
210
|
+
}))
|
|
211
|
+
] });
|
|
212
|
+
return {
|
|
213
|
+
...relation,
|
|
214
|
+
table: validated.identifiers.table.value,
|
|
215
|
+
alias: validated.identifiers.alias.value,
|
|
216
|
+
sourceKey: this.resolveColumn(meta, relation.sourceKey),
|
|
217
|
+
targetKey: validated.identifiers.targetKey.value,
|
|
218
|
+
targetColumns: Object.fromEntries(Object.keys(relation.targetColumns).map((column) => [validated.identifiers[`targetColumn:${column}`].value, relation.targetColumns[column]]))
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
validateFilterKey(meta, rawKey) {
|
|
222
|
+
const segments = rawKey.split("__");
|
|
223
|
+
if (segments.length > 2) throw new Error(`Invalid SQL lookup key: '${rawKey}'.`);
|
|
224
|
+
const field = segments[0];
|
|
225
|
+
const lookup = segments[1] ?? InternalLookupType.EXACT;
|
|
226
|
+
const validated = this.engine.validate({ lookupTokens: [{
|
|
227
|
+
key: rawKey,
|
|
228
|
+
lookup,
|
|
229
|
+
allowed: ALLOWED_LOOKUPS
|
|
230
|
+
}] });
|
|
231
|
+
return {
|
|
232
|
+
rawKey,
|
|
233
|
+
field,
|
|
234
|
+
lookup: validated.lookupTokens[rawKey].lookup,
|
|
235
|
+
qualifiedColumn: `${meta.table}.${this.resolveColumn(meta, field)}`
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
resolveColumn(meta, field) {
|
|
239
|
+
if (!(field in meta.columns)) throw new Error(`Unknown column '${field}' for table '${meta.table}'.`);
|
|
240
|
+
return field;
|
|
241
|
+
}
|
|
242
|
+
resolveRelation(meta, relationName) {
|
|
243
|
+
const relation = meta.relations?.[relationName];
|
|
244
|
+
if (!relation) throw new Error(`Unknown relation '${relationName}' for table '${meta.table}'.`);
|
|
245
|
+
return relation;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/query/domain/RelationTyping.ts
|
|
251
|
+
const InternalRelationHydrationCardinality = {
|
|
252
|
+
SINGLE: "single",
|
|
253
|
+
MANY: "many"
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/query/planning/QueryPlanner.ts
|
|
258
|
+
var QueryPlanner = class QueryPlanner {
|
|
259
|
+
static BRAND = "tango.orm.query_planner";
|
|
260
|
+
__tangoBrand = QueryPlanner.BRAND;
|
|
261
|
+
constructor(meta) {
|
|
262
|
+
this.meta = meta;
|
|
263
|
+
}
|
|
264
|
+
static isQueryPlanner(value) {
|
|
265
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === QueryPlanner.BRAND;
|
|
266
|
+
}
|
|
267
|
+
plan(state) {
|
|
268
|
+
const requestedPaths = Array.from(new Set([...state.selectRelated ?? [], ...state.prefetchRelated ?? []]));
|
|
269
|
+
if (requestedPaths.length === 0) return {
|
|
270
|
+
joinNodes: [],
|
|
271
|
+
prefetchNodes: [],
|
|
272
|
+
requestedPaths: []
|
|
273
|
+
};
|
|
274
|
+
const rootChildren = new Map();
|
|
275
|
+
for (const relationPath of new Set(state.selectRelated ?? [])) this.addPath(rootChildren, relationPath, "select");
|
|
276
|
+
for (const relationPath of new Set(state.prefetchRelated ?? [])) this.addPath(rootChildren, relationPath, "prefetch");
|
|
277
|
+
const { joinNodes, prefetchNodes } = this.buildPlannedChildren(rootChildren);
|
|
278
|
+
return {
|
|
279
|
+
joinNodes,
|
|
280
|
+
prefetchNodes,
|
|
281
|
+
requestedPaths
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
addPath(rootChildren, relationPath, mode) {
|
|
285
|
+
const segments = relationPath.split("__").filter(Boolean);
|
|
286
|
+
if (segments.length === 0) throw new Error(`Invalid empty relation path '${relationPath}'.`);
|
|
287
|
+
let currentMeta = this.meta;
|
|
288
|
+
let currentChildren = rootChildren;
|
|
289
|
+
let builtPath = "";
|
|
290
|
+
let containsCollection = false;
|
|
291
|
+
for (const segment of segments) {
|
|
292
|
+
const relation = currentMeta.relations?.[segment];
|
|
293
|
+
if (!relation) throw new Error(`Unknown relation path '${relationPath}' for table '${currentMeta.table}'.`);
|
|
294
|
+
if (segment in currentMeta.columns && relation.sourceKey !== segment) throw new Error(`Relation path '${relationPath}' collides with an existing field on table '${currentMeta.table}'.`);
|
|
295
|
+
if (relation.kind === InternalRelationKind.MANY_TO_MANY) throw new Error(`Relation path '${relationPath}' uses unsupported many-to-many hydration.`);
|
|
296
|
+
if (!relation.capabilities.queryable || !relation.capabilities.hydratable) throw new Error(`Relation path '${relationPath}' cannot be hydrated.`);
|
|
297
|
+
if (mode === "select") {
|
|
298
|
+
if (relation.cardinality !== InternalRelationHydrationCardinality.SINGLE || !relation.capabilities.joinable) throw new Error(`Relation path '${relationPath}' cannot be loaded with selectRelated(...).`);
|
|
299
|
+
} else if (relation.cardinality === InternalRelationHydrationCardinality.MANY) {
|
|
300
|
+
if (!relation.capabilities.prefetchable) throw new Error(`Relation path '${relationPath}' cannot be loaded with prefetchRelated(...).`);
|
|
301
|
+
containsCollection = true;
|
|
302
|
+
} else if (!relation.capabilities.joinable) throw new Error(`Relation path '${relationPath}' cannot be loaded with prefetchRelated(...).`);
|
|
303
|
+
const targetMeta = relation.targetMeta;
|
|
304
|
+
if (!targetMeta) throw new Error(`Relation path '${relationPath}' is missing target metadata.`);
|
|
305
|
+
builtPath = builtPath.length > 0 ? `${builtPath}__${segment}` : segment;
|
|
306
|
+
const existing = currentChildren.get(segment);
|
|
307
|
+
const nextNode = existing ?? {
|
|
308
|
+
segment,
|
|
309
|
+
relationEdge: relation,
|
|
310
|
+
relationPath: builtPath,
|
|
311
|
+
targetMeta,
|
|
312
|
+
provenance: new Set(),
|
|
313
|
+
children: new Map()
|
|
314
|
+
};
|
|
315
|
+
nextNode.provenance.add(relationPath);
|
|
316
|
+
currentChildren.set(segment, nextNode);
|
|
317
|
+
currentChildren = nextNode.children;
|
|
318
|
+
currentMeta = targetMeta;
|
|
319
|
+
}
|
|
320
|
+
if (mode === "prefetch" && !containsCollection) throw new Error(`Relation path '${relationPath}' cannot be loaded with prefetchRelated(...).`);
|
|
321
|
+
}
|
|
322
|
+
buildPlannedChildren(children) {
|
|
323
|
+
const joinNodes = [];
|
|
324
|
+
const prefetchNodes = [];
|
|
325
|
+
for (const child of children.values()) {
|
|
326
|
+
const { joinNodes: joinChildren, prefetchNodes: prefetchChildren } = this.buildPlannedChildren(child.children);
|
|
327
|
+
const plannedNode = {
|
|
328
|
+
nodeId: child.relationPath,
|
|
329
|
+
relationName: child.segment,
|
|
330
|
+
relationPath: child.relationPath,
|
|
331
|
+
ownerModelKey: child.relationEdge.sourceModelKey,
|
|
332
|
+
relationEdge: child.relationEdge,
|
|
333
|
+
targetModelKey: child.relationEdge.targetModelKey,
|
|
334
|
+
loadMode: child.relationEdge.cardinality === InternalRelationHydrationCardinality.SINGLE ? InternalRelationHydrationLoadMode.JOIN : InternalRelationHydrationLoadMode.PREFETCH,
|
|
335
|
+
cardinality: child.relationEdge.cardinality,
|
|
336
|
+
provenance: [...child.provenance],
|
|
337
|
+
joinChildren,
|
|
338
|
+
prefetchChildren
|
|
339
|
+
};
|
|
340
|
+
if (plannedNode.loadMode === InternalRelationHydrationLoadMode.JOIN) joinNodes.push(plannedNode);
|
|
341
|
+
else prefetchNodes.push(plannedNode);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
joinNodes,
|
|
345
|
+
prefetchNodes
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/query/compiler/QueryCompiler.ts
|
|
352
|
+
const sqlSafetyAdapter = new OrmSqlSafetyAdapter();
|
|
353
|
+
var QueryCompiler = class QueryCompiler {
|
|
354
|
+
static BRAND = "tango.orm.query_compiler";
|
|
355
|
+
__tangoBrand = QueryCompiler.BRAND;
|
|
356
|
+
constructor(meta, dialect = InternalDialect.POSTGRES) {
|
|
357
|
+
this.meta = meta;
|
|
358
|
+
this.dialect = dialect;
|
|
359
|
+
}
|
|
360
|
+
static isQueryCompiler(value) {
|
|
361
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === QueryCompiler.BRAND;
|
|
362
|
+
}
|
|
363
|
+
compile(state) {
|
|
364
|
+
const hydrationPlan = new QueryPlanner(this.meta).plan(state);
|
|
365
|
+
const validatedPlan = sqlSafetyAdapter.validate({
|
|
366
|
+
kind: "select",
|
|
367
|
+
meta: this.meta,
|
|
368
|
+
selectFields: state.select?.map(String),
|
|
369
|
+
filterKeys: this.collectStateFilterKeys(state),
|
|
370
|
+
orderFields: state.order?.map((order) => String(order.by)),
|
|
371
|
+
relationNames: []
|
|
372
|
+
});
|
|
373
|
+
const table = validatedPlan.meta.table;
|
|
374
|
+
const whereParts = [];
|
|
375
|
+
const params = [];
|
|
376
|
+
if (state.q) {
|
|
377
|
+
const result = this.compileQNode(state.q, params.length + 1, validatedPlan.filterKeys);
|
|
378
|
+
if (result.sql) {
|
|
379
|
+
whereParts.push(result.sql);
|
|
380
|
+
params.push(...result.params);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
state.excludes?.forEach((exclude) => {
|
|
384
|
+
const result = this.compileQNode({
|
|
385
|
+
kind: InternalQNodeType.NOT,
|
|
386
|
+
node: exclude
|
|
387
|
+
}, params.length + 1, validatedPlan.filterKeys);
|
|
388
|
+
if (result.sql) {
|
|
389
|
+
whereParts.push(result.sql);
|
|
390
|
+
params.push(...result.params);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
const baseSelects = state.select?.length ? state.select.map((field) => validatedPlan.selectFields[String(field)]) : [`${table}.*`];
|
|
394
|
+
const joinCollection = {
|
|
395
|
+
selects: [],
|
|
396
|
+
joins: []
|
|
397
|
+
};
|
|
398
|
+
const hiddenRootAliases = [];
|
|
399
|
+
const compiledJoinNodes = hydrationPlan.joinNodes.map((node) => this.compileHydrationNode(node, {
|
|
400
|
+
rootTable: table,
|
|
401
|
+
ownerMeta: this.meta,
|
|
402
|
+
ownerAlias: table,
|
|
403
|
+
collectRootJoins: true,
|
|
404
|
+
rootSelectedFields: state.select?.map(String) ?? undefined,
|
|
405
|
+
hiddenRootAliases,
|
|
406
|
+
joinCollection
|
|
407
|
+
}));
|
|
408
|
+
const compiledPrefetchNodes = hydrationPlan.prefetchNodes.map((node) => this.compileHydrationNode(node, {
|
|
409
|
+
rootTable: table,
|
|
410
|
+
ownerMeta: this.meta,
|
|
411
|
+
ownerAlias: table,
|
|
412
|
+
collectRootJoins: false,
|
|
413
|
+
rootSelectedFields: state.select?.map(String) ?? undefined,
|
|
414
|
+
hiddenRootAliases,
|
|
415
|
+
joinCollection
|
|
416
|
+
}));
|
|
417
|
+
const select = [
|
|
418
|
+
...baseSelects,
|
|
419
|
+
...joinCollection.selects,
|
|
420
|
+
...this.buildRootHiddenSelects(compiledPrefetchNodes, table)
|
|
421
|
+
].join(", ");
|
|
422
|
+
const whereSQL = whereParts.length ? ` WHERE ${whereParts.join(" AND ")}` : "";
|
|
423
|
+
const orderSQL = ` ORDER BY ${state.order?.length ? state.order.map((order) => `${validatedPlan.orderFields[String(order.by)]} ${order.dir.toUpperCase()}`).join(", ") : `${table}.${validatedPlan.meta.pk} ASC`}`;
|
|
424
|
+
const limitSQL = state.limit ? ` LIMIT ${state.limit}` : "";
|
|
425
|
+
const offsetSQL = state.offset ? ` OFFSET ${state.offset}` : "";
|
|
426
|
+
const sql = `SELECT ${select} FROM ${table}${joinCollection.joins.length ? ` ${joinCollection.joins.join(" ")}` : ""}${whereSQL}${orderSQL}${limitSQL}${offsetSQL}`;
|
|
427
|
+
const compiledHydrationPlan = compiledJoinNodes.length > 0 || compiledPrefetchNodes.length > 0 ? {
|
|
428
|
+
requestedPaths: hydrationPlan.requestedPaths,
|
|
429
|
+
hiddenRootAliases: [...new Set(hiddenRootAliases)],
|
|
430
|
+
joinNodes: compiledJoinNodes,
|
|
431
|
+
prefetchNodes: compiledPrefetchNodes
|
|
432
|
+
} : undefined;
|
|
433
|
+
return {
|
|
434
|
+
sql,
|
|
435
|
+
params,
|
|
436
|
+
hydrationPlan: compiledHydrationPlan
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
compilePrefetch(node, sourceValues) {
|
|
440
|
+
const placeholders = this.dialect === InternalDialect.POSTGRES ? sourceValues.map((_, index) => `$${index + 1}`).join(", ") : sourceValues.map(() => "?").join(", ");
|
|
441
|
+
const validatedTarget = this.validatePrefetchTarget(node);
|
|
442
|
+
const baseAlias = this.buildPrefetchBaseAlias(node.relationPath);
|
|
443
|
+
const joinCollection = {
|
|
444
|
+
selects: [],
|
|
445
|
+
joins: []
|
|
446
|
+
};
|
|
447
|
+
for (const joinChild of node.joinChildren) this.collectNestedJoinSql(joinChild, baseAlias, validatedTarget.columns, joinCollection);
|
|
448
|
+
const baseSelects = Object.keys(validatedTarget.columns).map((column) => `${baseAlias}.${column} AS ${column}`);
|
|
449
|
+
return {
|
|
450
|
+
sql: `SELECT ${[...baseSelects, ...joinCollection.selects].join(", ")} FROM ${validatedTarget.table} ${baseAlias}${joinCollection.joins.length ? ` ${joinCollection.joins.join(" ")}` : ""} WHERE ${baseAlias}.${validatedTarget.targetKey} IN (${placeholders}) ORDER BY ${baseAlias}.${validatedTarget.targetKey} ASC, ${baseAlias}.${validatedTarget.primaryKey} ASC`,
|
|
451
|
+
params: sourceValues,
|
|
452
|
+
targetKey: validatedTarget.targetKey,
|
|
453
|
+
targetColumns: validatedTarget.columns
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
compileHydrationNode(node, context) {
|
|
457
|
+
const validatedRelation = this.validateHydrationRelation(context.ownerMeta, node.relationName);
|
|
458
|
+
const targetColumns = validatedRelation.targetColumns;
|
|
459
|
+
const targetMeta = node.relationEdge.targetMeta;
|
|
460
|
+
if (!targetMeta) throw new Error(`Relation path '${node.relationPath}' is missing target metadata.`);
|
|
461
|
+
const compiledJoinChildren = node.joinChildren.map((child) => this.compileHydrationNode(child, {
|
|
462
|
+
...context,
|
|
463
|
+
ownerMeta: targetMeta,
|
|
464
|
+
ownerAlias: this.buildJoinAlias(node.relationPath),
|
|
465
|
+
collectRootJoins: context.collectRootJoins
|
|
466
|
+
}));
|
|
467
|
+
const compiledPrefetchChildren = node.prefetchChildren.map((child) => this.compileHydrationNode(child, {
|
|
468
|
+
...context,
|
|
469
|
+
ownerMeta: targetMeta,
|
|
470
|
+
ownerAlias: this.buildJoinAlias(node.relationPath),
|
|
471
|
+
collectRootJoins: false
|
|
472
|
+
}));
|
|
473
|
+
let joinDescriptor;
|
|
474
|
+
if (node.loadMode === InternalRelationHydrationLoadMode.JOIN) {
|
|
475
|
+
joinDescriptor = {
|
|
476
|
+
alias: this.buildJoinAlias(node.relationPath),
|
|
477
|
+
columns: Object.fromEntries(Object.keys(targetColumns).map((column) => [column, this.buildHydrationColumnAlias(node.relationPath, column)]))
|
|
478
|
+
};
|
|
479
|
+
if (context.collectRootJoins) {
|
|
480
|
+
context.joinCollection.joins.push(`LEFT JOIN ${validatedRelation.table} ${joinDescriptor.alias} ON ${joinDescriptor.alias}.${validatedRelation.targetKey} = ${context.ownerAlias}.${validatedRelation.sourceKey}`);
|
|
481
|
+
context.joinCollection.selects.push(...Object.entries(joinDescriptor.columns).map(([column, alias]) => `${joinDescriptor.alias}.${column} AS ${alias}`));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const ownerSourceAccessor = node.loadMode === InternalRelationHydrationLoadMode.PREFETCH && context.collectRootJoins === false && context.ownerAlias === context.rootTable && context.rootSelectedFields?.length && !context.rootSelectedFields.includes(validatedRelation.sourceKey) ? this.buildPrefetchSourceAlias(node.relationPath, validatedRelation.sourceKey) : validatedRelation.sourceKey;
|
|
485
|
+
if (node.loadMode === InternalRelationHydrationLoadMode.PREFETCH && ownerSourceAccessor !== validatedRelation.sourceKey) context.hiddenRootAliases.push(ownerSourceAccessor);
|
|
486
|
+
return {
|
|
487
|
+
nodeId: node.nodeId,
|
|
488
|
+
relationName: node.relationName,
|
|
489
|
+
relationPath: node.relationPath,
|
|
490
|
+
ownerModelKey: node.ownerModelKey,
|
|
491
|
+
targetModelKey: node.targetModelKey,
|
|
492
|
+
loadMode: node.loadMode,
|
|
493
|
+
cardinality: node.cardinality,
|
|
494
|
+
sourceKey: validatedRelation.sourceKey,
|
|
495
|
+
ownerSourceAccessor,
|
|
496
|
+
targetKey: validatedRelation.targetKey,
|
|
497
|
+
targetTable: validatedRelation.table,
|
|
498
|
+
targetPrimaryKey: node.relationEdge.targetPrimaryKey,
|
|
499
|
+
targetColumns,
|
|
500
|
+
provenance: node.provenance,
|
|
501
|
+
joinChildren: compiledJoinChildren,
|
|
502
|
+
prefetchChildren: compiledPrefetchChildren,
|
|
503
|
+
join: joinDescriptor
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
validateHydrationRelation(ownerMeta, relationName) {
|
|
507
|
+
return sqlSafetyAdapter.validate({
|
|
508
|
+
kind: "select",
|
|
509
|
+
meta: ownerMeta,
|
|
510
|
+
relationNames: [relationName]
|
|
511
|
+
}).relations[relationName];
|
|
512
|
+
}
|
|
513
|
+
buildRootHiddenSelects(nodes, table) {
|
|
514
|
+
return nodes.flatMap((node) => {
|
|
515
|
+
const select = node.ownerSourceAccessor !== node.sourceKey ? [`${table}.${node.sourceKey} AS ${node.ownerSourceAccessor}`] : [];
|
|
516
|
+
return [...select, ...this.buildRootHiddenSelects(node.prefetchChildren, table)];
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
validatePrefetchTarget(node) {
|
|
520
|
+
try {
|
|
521
|
+
const validated = sqlSafetyAdapter.validate({
|
|
522
|
+
kind: "select",
|
|
523
|
+
meta: {
|
|
524
|
+
table: node.targetTable,
|
|
525
|
+
pk: node.targetPrimaryKey,
|
|
526
|
+
columns: node.targetColumns
|
|
527
|
+
},
|
|
528
|
+
filterKeys: [node.targetKey]
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
table: validated.meta.table,
|
|
532
|
+
primaryKey: validated.meta.pk,
|
|
533
|
+
targetKey: validated.filterKeys[node.targetKey].field,
|
|
534
|
+
columns: validated.meta.columns
|
|
535
|
+
};
|
|
536
|
+
} catch (error) {
|
|
537
|
+
const message = isError(error) ? error.message : String(error);
|
|
538
|
+
throw new Error(`Compiled prefetch query failed validation: ${message}`, { cause: error });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
collectNestedJoinSql(node, ownerAlias, ownerColumns, collection) {
|
|
542
|
+
if (!node.join) return;
|
|
543
|
+
const validatedTarget = this.validatePrefetchJoinTarget(node, ownerColumns);
|
|
544
|
+
const validatedJoinAlias = this.validateInternalAlias(node.join.alias);
|
|
545
|
+
const validatedJoinColumns = Object.fromEntries(Object.entries(node.join.columns).map(([column, alias]) => {
|
|
546
|
+
if (!(column in validatedTarget.columns)) throw new Error(`Compiled prefetch query failed validation: unknown nested join column '${column}'.`);
|
|
547
|
+
return [column, this.validateInternalAlias(alias)];
|
|
548
|
+
}));
|
|
549
|
+
collection.joins.push(`LEFT JOIN ${validatedTarget.table} ${validatedJoinAlias} ON ${validatedJoinAlias}.${validatedTarget.targetKey} = ${ownerAlias}.${node.sourceKey}`);
|
|
550
|
+
collection.selects.push(...Object.entries(validatedJoinColumns).map(([column, alias]) => `${validatedJoinAlias}.${column} AS ${alias}`));
|
|
551
|
+
for (const child of node.joinChildren) this.collectNestedJoinSql(child, validatedJoinAlias, validatedTarget.columns, collection);
|
|
552
|
+
}
|
|
553
|
+
validatePrefetchJoinTarget(node, ownerColumns) {
|
|
554
|
+
if (!(node.sourceKey in ownerColumns)) throw new Error(`Compiled prefetch query failed validation: unknown owner column '${node.sourceKey}' for nested join.`);
|
|
555
|
+
return this.validatePrefetchTarget(node);
|
|
556
|
+
}
|
|
557
|
+
validateInternalAlias(alias) {
|
|
558
|
+
if (!/^__tango_[A-Za-z0-9_]+$/.test(alias)) throw new Error(`Compiled prefetch query failed validation: invalid internal alias '${alias}'.`);
|
|
559
|
+
return alias;
|
|
560
|
+
}
|
|
561
|
+
buildJoinAlias(relationPath) {
|
|
562
|
+
return this.assertInternalAliasDoesNotCollide(`__tango_join_${this.sanitizeRelationPath(relationPath)}`);
|
|
563
|
+
}
|
|
564
|
+
buildPrefetchBaseAlias(relationPath) {
|
|
565
|
+
return this.assertInternalAliasDoesNotCollide(`__tango_prefetch_base_${this.sanitizeRelationPath(relationPath)}`);
|
|
566
|
+
}
|
|
567
|
+
buildHydrationColumnAlias(relationPath, column) {
|
|
568
|
+
return this.assertInternalAliasDoesNotCollide(`__tango_hydrate_${this.sanitizeRelationPath(relationPath)}_${column}`);
|
|
569
|
+
}
|
|
570
|
+
buildPrefetchSourceAlias(relationPath, sourceKey) {
|
|
571
|
+
return this.assertInternalAliasDoesNotCollide(`__tango_prefetch_${this.sanitizeRelationPath(relationPath)}_${sourceKey}`);
|
|
572
|
+
}
|
|
573
|
+
sanitizeRelationPath(relationPath) {
|
|
574
|
+
return relationPath.replace(/[^a-zA-Z0-9]+/g, "_");
|
|
575
|
+
}
|
|
576
|
+
assertInternalAliasDoesNotCollide(alias) {
|
|
577
|
+
if (alias in this.meta.columns) throw new Error(`Internal query alias '${alias}' collides with a field on table '${this.meta.table}'.`);
|
|
578
|
+
return alias;
|
|
579
|
+
}
|
|
580
|
+
compileQNode(node, paramIndex, filterKeys) {
|
|
581
|
+
switch (node.kind) {
|
|
582
|
+
case InternalQNodeType.ATOM: return this.compileAtom(node.where || {}, paramIndex, filterKeys);
|
|
583
|
+
case InternalQNodeType.AND: return this.compileAnd(node.nodes || [], paramIndex, filterKeys);
|
|
584
|
+
case InternalQNodeType.OR: return this.compileOr(node.nodes || [], paramIndex, filterKeys);
|
|
585
|
+
case InternalQNodeType.NOT: return this.compileNot(node.node, paramIndex, filterKeys);
|
|
586
|
+
default: return {
|
|
587
|
+
sql: "",
|
|
588
|
+
params: []
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
compileAtom(where, paramIndex, filterKeys) {
|
|
593
|
+
const entries = Object.entries(where).filter(([, value]) => value !== undefined);
|
|
594
|
+
const { parts, params } = entries.reduce((accumulator, [key, value]) => {
|
|
595
|
+
const descriptor = filterKeys[String(key)];
|
|
596
|
+
const idx = paramIndex + accumulator.params.length;
|
|
597
|
+
const clause = this.lookupToSQL(descriptor.qualifiedColumn, descriptor.lookup, value, idx);
|
|
598
|
+
accumulator.parts.push(clause.sql);
|
|
599
|
+
accumulator.params.push(...clause.params);
|
|
600
|
+
return accumulator;
|
|
601
|
+
}, {
|
|
602
|
+
parts: [],
|
|
603
|
+
params: []
|
|
604
|
+
});
|
|
605
|
+
return {
|
|
606
|
+
sql: parts.length ? `(${parts.join(" AND ")})` : "",
|
|
607
|
+
params
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
compileAnd(nodes, paramIndex, filterKeys) {
|
|
611
|
+
const { parts, params } = nodes.reduce((accumulator, node) => {
|
|
612
|
+
const result = this.compileQNode(node, paramIndex + accumulator.params.length, filterKeys);
|
|
613
|
+
if (result.sql) {
|
|
614
|
+
accumulator.parts.push(result.sql);
|
|
615
|
+
accumulator.params.push(...result.params);
|
|
616
|
+
}
|
|
617
|
+
return accumulator;
|
|
618
|
+
}, {
|
|
619
|
+
parts: [],
|
|
620
|
+
params: []
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
sql: parts.length ? `(${parts.join(" AND ")})` : "",
|
|
624
|
+
params
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
compileOr(nodes, paramIndex, filterKeys) {
|
|
628
|
+
const { parts, params } = nodes.reduce((accumulator, node) => {
|
|
629
|
+
const result = this.compileQNode(node, paramIndex + accumulator.params.length, filterKeys);
|
|
630
|
+
if (result.sql) {
|
|
631
|
+
accumulator.parts.push(result.sql);
|
|
632
|
+
accumulator.params.push(...result.params);
|
|
633
|
+
}
|
|
634
|
+
return accumulator;
|
|
635
|
+
}, {
|
|
636
|
+
parts: [],
|
|
637
|
+
params: []
|
|
638
|
+
});
|
|
639
|
+
return {
|
|
640
|
+
sql: parts.length ? `(${parts.join(" OR ")})` : "",
|
|
641
|
+
params
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
compileNot(node, paramIndex, filterKeys) {
|
|
645
|
+
const result = this.compileQNode(node, paramIndex, filterKeys);
|
|
646
|
+
if (!result.sql) return {
|
|
647
|
+
sql: "",
|
|
648
|
+
params: []
|
|
649
|
+
};
|
|
650
|
+
return {
|
|
651
|
+
sql: `(NOT ${result.sql})`,
|
|
652
|
+
params: result.params
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
lookupToSQL(col, lookup, value, paramIndex) {
|
|
656
|
+
const placeholder = this.dialect === InternalDialect.POSTGRES ? `$${paramIndex}` : "?";
|
|
657
|
+
const normalized = this.normalizeParam(value);
|
|
658
|
+
switch (lookup) {
|
|
659
|
+
case InternalLookupType.EXACT:
|
|
660
|
+
if (value === null) return {
|
|
661
|
+
sql: `${col} IS NULL`,
|
|
662
|
+
params: []
|
|
663
|
+
};
|
|
664
|
+
return {
|
|
665
|
+
sql: `${col} = ${placeholder}`,
|
|
666
|
+
params: [normalized]
|
|
667
|
+
};
|
|
668
|
+
case InternalLookupType.LT: return {
|
|
669
|
+
sql: `${col} < ${placeholder}`,
|
|
670
|
+
params: [normalized]
|
|
671
|
+
};
|
|
672
|
+
case InternalLookupType.LTE: return {
|
|
673
|
+
sql: `${col} <= ${placeholder}`,
|
|
674
|
+
params: [normalized]
|
|
675
|
+
};
|
|
676
|
+
case InternalLookupType.GT: return {
|
|
677
|
+
sql: `${col} > ${placeholder}`,
|
|
678
|
+
params: [normalized]
|
|
679
|
+
};
|
|
680
|
+
case InternalLookupType.GTE: return {
|
|
681
|
+
sql: `${col} >= ${placeholder}`,
|
|
682
|
+
params: [normalized]
|
|
683
|
+
};
|
|
684
|
+
case InternalLookupType.IN: {
|
|
685
|
+
const entries = (Array.isArray(value) ? value : [value]).map((entry) => this.normalizeParam(entry));
|
|
686
|
+
if (entries.length === 0) return {
|
|
687
|
+
sql: "1=0",
|
|
688
|
+
params: []
|
|
689
|
+
};
|
|
690
|
+
const placeholders = this.dialect === InternalDialect.POSTGRES ? entries.map((_, index) => `$${paramIndex + index}`).join(", ") : entries.map(() => "?").join(", ");
|
|
691
|
+
return {
|
|
692
|
+
sql: `${col} IN (${placeholders})`,
|
|
693
|
+
params: entries
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
case InternalLookupType.ISNULL: return {
|
|
697
|
+
sql: value ? `${col} IS NULL` : `${col} IS NOT NULL`,
|
|
698
|
+
params: []
|
|
699
|
+
};
|
|
700
|
+
case InternalLookupType.CONTAINS: return {
|
|
701
|
+
sql: `${col} LIKE ${placeholder}`,
|
|
702
|
+
params: [`%${value}%`]
|
|
703
|
+
};
|
|
704
|
+
case InternalLookupType.ICONTAINS: {
|
|
705
|
+
const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
|
|
706
|
+
return {
|
|
707
|
+
sql: `${lowerCol} LIKE ${placeholder}`,
|
|
708
|
+
params: [`%${String(value).toLowerCase()}%`]
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
case InternalLookupType.STARTSWITH: return {
|
|
712
|
+
sql: `${col} LIKE ${placeholder}`,
|
|
713
|
+
params: [`${value}%`]
|
|
714
|
+
};
|
|
715
|
+
case InternalLookupType.ISTARTSWITH: {
|
|
716
|
+
const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
|
|
717
|
+
return {
|
|
718
|
+
sql: `${lowerCol} LIKE ${placeholder}`,
|
|
719
|
+
params: [`${String(value).toLowerCase()}%`]
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
case InternalLookupType.ENDSWITH: return {
|
|
723
|
+
sql: `${col} LIKE ${placeholder}`,
|
|
724
|
+
params: [`%${value}`]
|
|
725
|
+
};
|
|
726
|
+
case InternalLookupType.IENDSWITH: {
|
|
727
|
+
const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
|
|
728
|
+
return {
|
|
729
|
+
sql: `${lowerCol} LIKE ${placeholder}`,
|
|
730
|
+
params: [`%${String(value).toLowerCase()}`]
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
default: throw new Error(`Unknown lookup: ${lookup}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
normalizeParam(value) {
|
|
737
|
+
if (this.dialect === InternalDialect.SQLITE && typeof value === "boolean") return value ? 1 : 0;
|
|
738
|
+
return value;
|
|
739
|
+
}
|
|
740
|
+
collectStateFilterKeys(state) {
|
|
741
|
+
const filterKeys = new Set();
|
|
742
|
+
if (state.q) this.collectNodeFilterKeys(state.q, filterKeys);
|
|
743
|
+
state.excludes?.forEach((exclude) => this.collectNodeFilterKeys(exclude, filterKeys));
|
|
744
|
+
return [...filterKeys];
|
|
745
|
+
}
|
|
746
|
+
collectNodeFilterKeys(node, filterKeys) {
|
|
747
|
+
Object.keys(node.where ?? {}).forEach((key) => filterKeys.add(key));
|
|
748
|
+
node.nodes?.forEach((child) => this.collectNodeFilterKeys(child, filterKeys));
|
|
749
|
+
if (node.node) this.collectNodeFilterKeys(node.node, filterKeys);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/query/compiler/index.ts
|
|
755
|
+
var compiler_exports = {};
|
|
756
|
+
__export(compiler_exports, { QueryCompiler: () => QueryCompiler });
|
|
757
|
+
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/query/domain/QueryResult.ts
|
|
760
|
+
let didWarnDeprecatedResults = false;
|
|
761
|
+
var QueryResult = class QueryResult {
|
|
762
|
+
static BRAND = "tango.orm.query_result";
|
|
763
|
+
__tangoBrand = QueryResult.BRAND;
|
|
764
|
+
items;
|
|
765
|
+
constructor(items) {
|
|
766
|
+
this.items = items;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Runtime narrowing for values that may be a plain array or a `QueryResult` instance.
|
|
770
|
+
*/
|
|
771
|
+
static isQueryResult(value) {
|
|
772
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === QueryResult.BRAND;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Sync iteration over materialized rows.
|
|
776
|
+
*/
|
|
777
|
+
[Symbol.iterator]() {
|
|
778
|
+
return this.items[Symbol.iterator]();
|
|
779
|
+
}
|
|
780
|
+
/** Number of materialized rows. */
|
|
781
|
+
get length() {
|
|
782
|
+
return this.items.length;
|
|
783
|
+
}
|
|
784
|
+
/** Same as `Array#map` on the materialized rows. */
|
|
785
|
+
map(callbackfn, thisArg) {
|
|
786
|
+
return this.items.map(callbackfn, thisArg);
|
|
787
|
+
}
|
|
788
|
+
/** Indexed read with support for negative indices, like `Array#at`. */
|
|
789
|
+
at(index) {
|
|
790
|
+
return this.items.at(index);
|
|
791
|
+
}
|
|
792
|
+
/** Returns a shallow copy of the materialized rows as a plain array. */
|
|
793
|
+
toArray() {
|
|
794
|
+
return [...this.items];
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* @deprecated Use iteration, `length`, `map`, or `toArray()` instead.
|
|
798
|
+
*/
|
|
799
|
+
get results() {
|
|
800
|
+
if (!didWarnDeprecatedResults) {
|
|
801
|
+
didWarnDeprecatedResults = true;
|
|
802
|
+
getLogger("tango.orm.query_result").warn("`QueryResult.results` is deprecated. Use iteration, `length`, `map`, or `toArray()` instead.");
|
|
803
|
+
}
|
|
804
|
+
return this.items;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/query/domain/index.ts
|
|
810
|
+
var domain_exports = {};
|
|
811
|
+
__export(domain_exports, {
|
|
812
|
+
InternalRelationHydrationCardinality: () => InternalRelationHydrationCardinality,
|
|
813
|
+
QueryResult: () => QueryResult,
|
|
814
|
+
TableMetaFactory: () => TableMetaFactory
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/query/domain/internal/InternalDirection.ts
|
|
819
|
+
const InternalDirection = {
|
|
820
|
+
ASC: "asc",
|
|
821
|
+
DESC: "desc"
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region src/query/QBuilder.ts
|
|
826
|
+
var QBuilder = class QBuilder {
|
|
827
|
+
static BRAND = "tango.orm.q_builder";
|
|
828
|
+
__tangoBrand = QBuilder.BRAND;
|
|
829
|
+
/**
|
|
830
|
+
* Narrow an unknown value to `QBuilder`.
|
|
831
|
+
*/
|
|
832
|
+
static isQBuilder(value) {
|
|
833
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === QBuilder.BRAND;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Combine multiple filter fragments using logical `AND`.
|
|
837
|
+
*/
|
|
838
|
+
static and(...nodes) {
|
|
839
|
+
return {
|
|
840
|
+
kind: InternalQNodeType.AND,
|
|
841
|
+
nodes: nodes.map(QBuilder.wrapNode)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Combine multiple filter fragments using logical `OR`.
|
|
846
|
+
*/
|
|
847
|
+
static or(...nodes) {
|
|
848
|
+
return {
|
|
849
|
+
kind: InternalQNodeType.OR,
|
|
850
|
+
nodes: nodes.map(QBuilder.wrapNode)
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Negate a filter fragment using logical `NOT`.
|
|
855
|
+
*/
|
|
856
|
+
static not(node) {
|
|
857
|
+
return {
|
|
858
|
+
kind: InternalQNodeType.NOT,
|
|
859
|
+
node: QBuilder.wrapNode(node)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
static wrapNode(input) {
|
|
863
|
+
if (input.kind) return input;
|
|
864
|
+
return {
|
|
865
|
+
kind: InternalQNodeType.ATOM,
|
|
866
|
+
where: input
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/query/QuerySet.ts
|
|
873
|
+
var QuerySet = class QuerySet {
|
|
874
|
+
static BRAND = "tango.orm.query_set";
|
|
875
|
+
__tangoBrand = QuerySet.BRAND;
|
|
876
|
+
evaluationCache;
|
|
877
|
+
constructor(executor, state = {}) {
|
|
878
|
+
this.executor = executor;
|
|
879
|
+
this.state = state;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Narrow an unknown value to `QuerySet`.
|
|
883
|
+
*/
|
|
884
|
+
static isQuerySet(value) {
|
|
885
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === QuerySet.BRAND;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Add a filter expression to the query.
|
|
889
|
+
*
|
|
890
|
+
* Multiple `filter()` calls are composed with `AND`.
|
|
891
|
+
*/
|
|
892
|
+
filter(q) {
|
|
893
|
+
const wrapped = q.kind ? q : {
|
|
894
|
+
kind: InternalQNodeType.ATOM,
|
|
895
|
+
where: q
|
|
896
|
+
};
|
|
897
|
+
const merged = this.state.q ? QBuilder.and(this.state.q, wrapped) : wrapped;
|
|
898
|
+
return new QuerySet(this.executor, {
|
|
899
|
+
...this.state,
|
|
900
|
+
q: merged
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Add an exclusion expression to the query.
|
|
905
|
+
*
|
|
906
|
+
* Exclusions are translated to `NOT (...)` predicates.
|
|
907
|
+
*/
|
|
908
|
+
exclude(q) {
|
|
909
|
+
const wrapped = q.kind ? q : {
|
|
910
|
+
kind: InternalQNodeType.ATOM,
|
|
911
|
+
where: q
|
|
912
|
+
};
|
|
913
|
+
const excludes = [...this.state.excludes ?? [], wrapped];
|
|
914
|
+
return new QuerySet(this.executor, {
|
|
915
|
+
...this.state,
|
|
916
|
+
excludes
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Apply ordering tokens such as `'name'` or `'-createdAt'`.
|
|
921
|
+
*/
|
|
922
|
+
orderBy(...tokens) {
|
|
923
|
+
const order = tokens.map((t) => {
|
|
924
|
+
const str = String(t);
|
|
925
|
+
if (str.startsWith("-")) return {
|
|
926
|
+
by: str.slice(1),
|
|
927
|
+
dir: InternalDirection.DESC
|
|
928
|
+
};
|
|
929
|
+
return {
|
|
930
|
+
by: t,
|
|
931
|
+
dir: InternalDirection.ASC
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
return new QuerySet(this.executor, {
|
|
935
|
+
...this.state,
|
|
936
|
+
order
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Limit the maximum number of rows returned.
|
|
941
|
+
*/
|
|
942
|
+
limit(n) {
|
|
943
|
+
return new QuerySet(this.executor, {
|
|
944
|
+
...this.state,
|
|
945
|
+
limit: n
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Skip the first `n` rows.
|
|
950
|
+
*/
|
|
951
|
+
offset(n) {
|
|
952
|
+
return new QuerySet(this.executor, {
|
|
953
|
+
...this.state,
|
|
954
|
+
offset: n
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
select(fields) {
|
|
958
|
+
return new QuerySet(this.executor, {
|
|
959
|
+
...this.state,
|
|
960
|
+
select: [...fields]
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Hydrate single-valued relation paths through SQL joins.
|
|
965
|
+
*
|
|
966
|
+
* Forward `belongsTo` relations can be inferred from the source model's
|
|
967
|
+
* field-authored relation metadata. Reverse `hasOne` relations can be
|
|
968
|
+
* selected with a target model generic when the target model points back to
|
|
969
|
+
* the source model. Generated relation typing also enables nested `__`
|
|
970
|
+
* path keys for applications that keep the app-local registry current.
|
|
971
|
+
*/
|
|
972
|
+
selectRelated(...rels) {
|
|
973
|
+
return new QuerySet(this.executor, {
|
|
974
|
+
...this.state,
|
|
975
|
+
selectRelated: [...rels]
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Hydrate collection-rooted relation paths with follow-up queries.
|
|
980
|
+
*
|
|
981
|
+
* Reverse `hasMany` relations can be prefetched with a target model generic
|
|
982
|
+
* when the target model points back to the source model. Generated relation
|
|
983
|
+
* typing also enables nested `__` path keys for applications that keep the
|
|
984
|
+
* app-local registry current.
|
|
985
|
+
*/
|
|
986
|
+
prefetchRelated(...rels) {
|
|
987
|
+
return new QuerySet(this.executor, {
|
|
988
|
+
...this.state,
|
|
989
|
+
prefetchRelated: [...rels]
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async fetch(shape) {
|
|
993
|
+
const baseResult = await this.getOrCreateEvaluationCache();
|
|
994
|
+
if (!shape) return baseResult;
|
|
995
|
+
const results = typeof shape === "function" ? baseResult.items.map(shape) : this.normalizeHydratedRowsForParserShape(baseResult.items).map((row) => shape.parse(row));
|
|
996
|
+
return new QueryResult(results);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Async iterable surface for `for await (... of queryset)`.
|
|
1000
|
+
*
|
|
1001
|
+
* Evaluates this queryset on first use by awaiting {@link QuerySet.fetch} without arguments, then
|
|
1002
|
+
* yields each element from that {@link QueryResult}. Later async iterations over the same queryset
|
|
1003
|
+
* instance reuse the cached materialized result instead of issuing another database round-trip.
|
|
1004
|
+
*/
|
|
1005
|
+
async *[Symbol.asyncIterator]() {
|
|
1006
|
+
const result = await this.fetch();
|
|
1007
|
+
for (const row of result) yield row;
|
|
1008
|
+
}
|
|
1009
|
+
async fetchOne(shape) {
|
|
1010
|
+
const limited = this.limit(1);
|
|
1011
|
+
const result = !shape ? await limited.fetch() : typeof shape === "function" ? await limited.fetch(shape) : await limited.fetch(shape);
|
|
1012
|
+
for (const row of result) return row;
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Execute a `COUNT(*)` query for the current filtered state.
|
|
1017
|
+
*/
|
|
1018
|
+
async count() {
|
|
1019
|
+
const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
|
|
1020
|
+
const compiled = compiler.compile(this.withoutHydrationState());
|
|
1021
|
+
const countQuery = `SELECT COUNT(*) as count FROM (${compiled.sql}) AS tango_count_subquery`;
|
|
1022
|
+
const rows = await this.executor.client.query(countQuery, compiled.params);
|
|
1023
|
+
return Number(rows.rows[0]?.count ?? 0);
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Return whether at least one row matches the current query state.
|
|
1027
|
+
*/
|
|
1028
|
+
async exists() {
|
|
1029
|
+
const count = await this.count();
|
|
1030
|
+
return count > 0;
|
|
1031
|
+
}
|
|
1032
|
+
getOrCreateEvaluationCache() {
|
|
1033
|
+
if (!this.evaluationCache) this.evaluationCache = this.evaluateRows().catch((error) => {
|
|
1034
|
+
this.evaluationCache = undefined;
|
|
1035
|
+
throw error;
|
|
1036
|
+
});
|
|
1037
|
+
return this.evaluationCache;
|
|
1038
|
+
}
|
|
1039
|
+
async evaluateRows() {
|
|
1040
|
+
const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
|
|
1041
|
+
const compiled = compiler.compile(this.state);
|
|
1042
|
+
const rows = await this.executor.run(compiled);
|
|
1043
|
+
const hydratedRows = await this.hydrateRows(rows, compiled);
|
|
1044
|
+
const projectedRows = hydratedRows;
|
|
1045
|
+
return new QueryResult(projectedRows);
|
|
1046
|
+
}
|
|
1047
|
+
normalizeHydratedRowsForParserShape(rows) {
|
|
1048
|
+
if (this.executor.dialect !== InternalDialect.SQLITE) return [...rows];
|
|
1049
|
+
const booleanColumns = Object.entries(this.executor.meta.columns).filter(([, value]) => this.isBooleanColumnType(value)).map(([column]) => column);
|
|
1050
|
+
if (booleanColumns.length === 0) return [...rows];
|
|
1051
|
+
return rows.map((row) => this.normalizeBooleanColumns(row, booleanColumns));
|
|
1052
|
+
}
|
|
1053
|
+
async hydrateRows(rows, compiled) {
|
|
1054
|
+
if (!compiled.hydrationPlan) return rows;
|
|
1055
|
+
const hydratedRows = rows.map((row) => ({ ...row }));
|
|
1056
|
+
const canonicalEntities = new Map();
|
|
1057
|
+
const queuedJoinPrefetchOwners = new Map();
|
|
1058
|
+
const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
|
|
1059
|
+
for (const row of hydratedRows) this.hydrateJoinNodesForOwner(row, row, compiled.hydrationPlan.joinNodes, canonicalEntities, queuedJoinPrefetchOwners);
|
|
1060
|
+
for (const node of compiled.hydrationPlan.prefetchNodes) await this.hydratePrefetchNode(node, hydratedRows, canonicalEntities, compiler);
|
|
1061
|
+
for (const [node, owners] of queuedJoinPrefetchOwners.entries()) await this.hydratePrefetchNode(node, [...owners], canonicalEntities, compiler);
|
|
1062
|
+
for (const row of hydratedRows) for (const alias of compiled.hydrationPlan.hiddenRootAliases) delete row[alias];
|
|
1063
|
+
return hydratedRows;
|
|
1064
|
+
}
|
|
1065
|
+
hydrateJoinNodesForOwner(owner, rawRow, nodes, canonicalEntities, queuedJoinPrefetchOwners) {
|
|
1066
|
+
for (const node of nodes) {
|
|
1067
|
+
if (!node.join) continue;
|
|
1068
|
+
const target = {};
|
|
1069
|
+
let hasTargetValue = false;
|
|
1070
|
+
for (const [column, alias] of Object.entries(node.join.columns)) {
|
|
1071
|
+
const value = rawRow[alias];
|
|
1072
|
+
delete rawRow[alias];
|
|
1073
|
+
target[column] = this.normalizeColumnValue(node.targetColumns[column], value);
|
|
1074
|
+
if (value !== null && value !== undefined) hasTargetValue = true;
|
|
1075
|
+
}
|
|
1076
|
+
if (!hasTargetValue) {
|
|
1077
|
+
owner[node.relationName] = null;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
const canonical = this.canonicalizeEntity(node, target, canonicalEntities);
|
|
1081
|
+
owner[node.relationName] = canonical;
|
|
1082
|
+
for (const childNode of node.prefetchChildren) {
|
|
1083
|
+
const queuedOwners = queuedJoinPrefetchOwners?.get(childNode);
|
|
1084
|
+
if (queuedOwners) {
|
|
1085
|
+
queuedOwners.add(canonical);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
queuedJoinPrefetchOwners?.set(childNode, new Set([canonical]));
|
|
1089
|
+
}
|
|
1090
|
+
this.hydrateJoinNodesForOwner(canonical, rawRow, node.joinChildren, canonicalEntities, queuedJoinPrefetchOwners);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async hydratePrefetchNode(node, owners, canonicalEntities, compiler) {
|
|
1094
|
+
if (owners.length === 0) return;
|
|
1095
|
+
const groupedOwners = this.groupOwnersByAccessor(owners, node.ownerSourceAccessor);
|
|
1096
|
+
const sourceValues = [...groupedOwners.keys()];
|
|
1097
|
+
for (const owner of owners) owner[node.relationName] = node.cardinality === InternalRelationHydrationCardinality.MANY ? [] : null;
|
|
1098
|
+
if (sourceValues.length === 0) return;
|
|
1099
|
+
const compiledPrefetch = compiler.compilePrefetch(node, sourceValues);
|
|
1100
|
+
const result = await this.executor.client.query(compiledPrefetch.sql, compiledPrefetch.params);
|
|
1101
|
+
const groupedTargets = new Map();
|
|
1102
|
+
const canonicalChildren = new Map();
|
|
1103
|
+
for (const rawResultRow of result.rows) {
|
|
1104
|
+
const normalized = this.normalizeTargetRow(compiledPrefetch, rawResultRow);
|
|
1105
|
+
const canonical = this.canonicalizeEntity(node, normalized, canonicalEntities);
|
|
1106
|
+
this.hydrateJoinNodesForOwner(canonical, normalized, node.joinChildren, canonicalEntities);
|
|
1107
|
+
const key = normalized[compiledPrefetch.targetKey];
|
|
1108
|
+
if (typeof key !== "string" && typeof key !== "number") continue;
|
|
1109
|
+
const bucket = groupedTargets.get(key) ?? [];
|
|
1110
|
+
bucket.push(canonical);
|
|
1111
|
+
groupedTargets.set(key, bucket);
|
|
1112
|
+
const childPrimaryKey = canonical[node.targetPrimaryKey];
|
|
1113
|
+
if (typeof childPrimaryKey === "string" || typeof childPrimaryKey === "number") canonicalChildren.set(childPrimaryKey, canonical);
|
|
1114
|
+
}
|
|
1115
|
+
for (const [sourceValue, grouped] of groupedTargets.entries()) for (const owner of groupedOwners.get(sourceValue) ?? []) owner[node.relationName] = node.cardinality === InternalRelationHydrationCardinality.MANY ? grouped : grouped[0];
|
|
1116
|
+
const childOwners = [...canonicalChildren.values()];
|
|
1117
|
+
for (const childNode of node.prefetchChildren) await this.hydratePrefetchNode(childNode, childOwners, canonicalEntities, compiler);
|
|
1118
|
+
}
|
|
1119
|
+
groupOwnersByAccessor(owners, accessor) {
|
|
1120
|
+
const grouped = new Map();
|
|
1121
|
+
for (const owner of owners) {
|
|
1122
|
+
const key = owner[accessor];
|
|
1123
|
+
if (typeof key !== "string" && typeof key !== "number") continue;
|
|
1124
|
+
const bucket = grouped.get(key) ?? [];
|
|
1125
|
+
bucket.push(owner);
|
|
1126
|
+
grouped.set(key, bucket);
|
|
1127
|
+
}
|
|
1128
|
+
return grouped;
|
|
1129
|
+
}
|
|
1130
|
+
canonicalizeEntity(node, row, canonicalEntities) {
|
|
1131
|
+
const primaryKeyValue = row[node.targetPrimaryKey];
|
|
1132
|
+
if (typeof primaryKeyValue !== "string" && typeof primaryKeyValue !== "number") return row;
|
|
1133
|
+
const byModel = canonicalEntities.get(node.targetModelKey) ?? new Map();
|
|
1134
|
+
const existing = byModel.get(primaryKeyValue);
|
|
1135
|
+
if (existing) {
|
|
1136
|
+
Object.assign(existing, row);
|
|
1137
|
+
return existing;
|
|
1138
|
+
}
|
|
1139
|
+
byModel.set(primaryKeyValue, row);
|
|
1140
|
+
canonicalEntities.set(node.targetModelKey, byModel);
|
|
1141
|
+
return row;
|
|
1142
|
+
}
|
|
1143
|
+
normalizeTargetRow(prefetch, row) {
|
|
1144
|
+
if (this.executor.dialect !== InternalDialect.SQLITE) return row;
|
|
1145
|
+
let normalized = null;
|
|
1146
|
+
for (const [column, type] of Object.entries(prefetch.targetColumns)) {
|
|
1147
|
+
if (!this.isBooleanColumnType(type)) continue;
|
|
1148
|
+
const next = this.normalizeSqliteBoolean(row[column]);
|
|
1149
|
+
if (next === row[column]) continue;
|
|
1150
|
+
normalized ??= { ...row };
|
|
1151
|
+
normalized[column] = next;
|
|
1152
|
+
}
|
|
1153
|
+
return normalized ?? row;
|
|
1154
|
+
}
|
|
1155
|
+
normalizeColumnValue(columnType, value) {
|
|
1156
|
+
return this.executor.dialect === InternalDialect.SQLITE && this.isBooleanColumnType(columnType) ? this.normalizeSqliteBoolean(value) : value;
|
|
1157
|
+
}
|
|
1158
|
+
isBooleanColumnType(value) {
|
|
1159
|
+
return typeof value === "string" && ["bool", "boolean"].includes(value.trim().toLowerCase());
|
|
1160
|
+
}
|
|
1161
|
+
normalizeSqliteBoolean(value) {
|
|
1162
|
+
if (value === 0 || value === "0") return false;
|
|
1163
|
+
if (value === 1 || value === "1") return true;
|
|
1164
|
+
return value;
|
|
1165
|
+
}
|
|
1166
|
+
normalizeBooleanColumns(row, columns) {
|
|
1167
|
+
let normalized = null;
|
|
1168
|
+
for (const column of columns) {
|
|
1169
|
+
const current = row[column];
|
|
1170
|
+
const next = this.normalizeSqliteBoolean(current);
|
|
1171
|
+
if (next === current) continue;
|
|
1172
|
+
if (!normalized) normalized = { ...row };
|
|
1173
|
+
normalized[column] = next;
|
|
1174
|
+
}
|
|
1175
|
+
return normalized ?? row;
|
|
1176
|
+
}
|
|
1177
|
+
withoutHydrationState() {
|
|
1178
|
+
const { selectRelated: _selectRelated, prefetchRelated: _prefetchRelated,...rest } = this.state;
|
|
1179
|
+
return rest;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
//#endregion
|
|
1184
|
+
//#region src/query/index.ts
|
|
1185
|
+
var query_exports = {};
|
|
1186
|
+
__export(query_exports, {
|
|
1187
|
+
Q: () => QBuilder,
|
|
1188
|
+
QBuilder: () => QBuilder,
|
|
1189
|
+
QueryCompiler: () => QueryCompiler,
|
|
1190
|
+
QueryResult: () => QueryResult,
|
|
1191
|
+
QuerySet: () => QuerySet,
|
|
1192
|
+
compiler: () => compiler_exports,
|
|
1193
|
+
domain: () => domain_exports
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
//#endregion
|
|
1197
|
+
export { OrmSqlSafetyAdapter, QBuilder, QueryCompiler, QueryResult, QuerySet, TableMetaFactory, compiler_exports, domain_exports, query_exports };
|
|
1198
|
+
//# sourceMappingURL=query-C6So1r6H.js.map
|