@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
package/dist/query-CWZ1cfjo.js
DELETED
|
@@ -1,856 +0,0 @@
|
|
|
1
|
-
import { __export } from "./chunk-DLY2FNSh.js";
|
|
2
|
-
import { SqlSafetyEngine } from "@danceroutine/tango-core";
|
|
3
|
-
|
|
4
|
-
//#region src/query/domain/internal/InternalRelationKind.ts
|
|
5
|
-
const InternalRelationKind = {
|
|
6
|
-
HAS_MANY: "hasMany",
|
|
7
|
-
BELONGS_TO: "belongsTo",
|
|
8
|
-
HAS_ONE: "hasOne",
|
|
9
|
-
MANY_TO_MANY: "manyToMany"
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/query/domain/internal/InternalDialect.ts
|
|
14
|
-
const InternalDialect = {
|
|
15
|
-
POSTGRES: "postgres",
|
|
16
|
-
SQLITE: "sqlite"
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
//#endregion
|
|
20
|
-
//#region src/query/domain/internal/InternalQNodeType.ts
|
|
21
|
-
const InternalQNodeType = {
|
|
22
|
-
ATOM: "atom",
|
|
23
|
-
AND: "and",
|
|
24
|
-
OR: "or",
|
|
25
|
-
NOT: "not"
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
//#endregion
|
|
29
|
-
//#region src/query/domain/internal/InternalLookupType.ts
|
|
30
|
-
const InternalLookupType = {
|
|
31
|
-
EXACT: "exact",
|
|
32
|
-
LT: "lt",
|
|
33
|
-
LTE: "lte",
|
|
34
|
-
GT: "gt",
|
|
35
|
-
GTE: "gte",
|
|
36
|
-
IN: "in",
|
|
37
|
-
ISNULL: "isnull",
|
|
38
|
-
CONTAINS: "contains",
|
|
39
|
-
ICONTAINS: "icontains",
|
|
40
|
-
STARTSWITH: "startswith",
|
|
41
|
-
ISTARTSWITH: "istartswith",
|
|
42
|
-
ENDSWITH: "endswith",
|
|
43
|
-
IENDSWITH: "iendswith"
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
//#endregion
|
|
47
|
-
//#region src/validation/OrmSqlSafetyAdapter.ts
|
|
48
|
-
const ALLOWED_LOOKUPS = Object.values(InternalLookupType);
|
|
49
|
-
var OrmSqlSafetyAdapter = class OrmSqlSafetyAdapter {
|
|
50
|
-
static BRAND = "tango.orm.orm_sql_safety_adapter";
|
|
51
|
-
__tangoBrand = OrmSqlSafetyAdapter.BRAND;
|
|
52
|
-
constructor(engine = new SqlSafetyEngine()) {
|
|
53
|
-
this.engine = engine;
|
|
54
|
-
}
|
|
55
|
-
validate(plan) {
|
|
56
|
-
switch (plan.kind) {
|
|
57
|
-
case "select": {
|
|
58
|
-
const meta = this.validateTableMeta(plan.meta, plan.relationNames ?? []);
|
|
59
|
-
return {
|
|
60
|
-
kind: "select",
|
|
61
|
-
meta,
|
|
62
|
-
selectFields: Object.fromEntries((plan.selectFields ?? []).map((field) => [field, `${meta.table}.${this.resolveColumn(meta, field)}`])),
|
|
63
|
-
filterKeys: Object.fromEntries((plan.filterKeys ?? []).map((rawKey) => [rawKey, this.validateFilterKey(meta, rawKey)])),
|
|
64
|
-
orderFields: Object.fromEntries((plan.orderFields ?? []).map((field) => [field, `${meta.table}.${this.resolveColumn(meta, field)}`])),
|
|
65
|
-
relations: Object.fromEntries((plan.relationNames ?? []).map((relationName) => [relationName, this.resolveRelation(meta, relationName)]))
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
case "insert": {
|
|
69
|
-
const meta = this.validateTableMeta(plan.meta);
|
|
70
|
-
return {
|
|
71
|
-
kind: "insert",
|
|
72
|
-
meta,
|
|
73
|
-
writeKeys: plan.writeKeys.map((key) => this.resolveColumn(meta, key))
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
case "update": {
|
|
77
|
-
const meta = this.validateTableMeta(plan.meta);
|
|
78
|
-
return {
|
|
79
|
-
kind: "update",
|
|
80
|
-
meta,
|
|
81
|
-
writeKeys: plan.writeKeys.map((key) => this.resolveColumn(meta, key))
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
case "delete": return {
|
|
85
|
-
kind: "delete",
|
|
86
|
-
meta: this.validateTableMeta(plan.meta)
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
validateTableMeta(meta, relationNames = []) {
|
|
91
|
-
const columnNames = Object.keys(meta.columns);
|
|
92
|
-
const validated = this.engine.validate({ identifiers: [
|
|
93
|
-
{
|
|
94
|
-
key: "table",
|
|
95
|
-
role: "table",
|
|
96
|
-
value: meta.table
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
key: "pk",
|
|
100
|
-
role: "primaryKey",
|
|
101
|
-
value: meta.pk,
|
|
102
|
-
allowlist: columnNames
|
|
103
|
-
},
|
|
104
|
-
...columnNames.map((column) => ({
|
|
105
|
-
key: `column:${column}`,
|
|
106
|
-
role: "column",
|
|
107
|
-
value: column
|
|
108
|
-
}))
|
|
109
|
-
] });
|
|
110
|
-
const validatedMeta = {
|
|
111
|
-
table: validated.identifiers.table.value,
|
|
112
|
-
pk: validated.identifiers.pk.value,
|
|
113
|
-
columns: Object.fromEntries(columnNames.map((column) => [validated.identifiers[`column:${column}`].value, meta.columns[column]]))
|
|
114
|
-
};
|
|
115
|
-
if (!(validatedMeta.pk in validatedMeta.columns)) throw new Error(`Unknown column '${validatedMeta.pk}' for table '${validatedMeta.table}'.`);
|
|
116
|
-
if (relationNames.length > 0) validatedMeta.relations = Object.fromEntries(relationNames.map((relationName) => [relationName, this.validateRelationMeta(validatedMeta, relationName, meta.relations)]));
|
|
117
|
-
return validatedMeta;
|
|
118
|
-
}
|
|
119
|
-
validateRelationMeta(meta, relationName, relations) {
|
|
120
|
-
const relation = relations?.[relationName];
|
|
121
|
-
if (!relation) throw new Error(`Unknown relation '${relationName}' for table '${meta.table}'.`);
|
|
122
|
-
if (!(relation.targetKey in relation.targetColumns)) throw new Error(`Unknown relation target key '${relation.targetKey}' for relation '${relationName}'.`);
|
|
123
|
-
const validated = this.engine.validate({ identifiers: [
|
|
124
|
-
{
|
|
125
|
-
key: "table",
|
|
126
|
-
role: "relationTable",
|
|
127
|
-
value: relation.table
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
key: "alias",
|
|
131
|
-
role: "alias",
|
|
132
|
-
value: relation.alias
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
key: "targetKey",
|
|
136
|
-
role: "relationTargetPrimaryKey",
|
|
137
|
-
value: relation.targetKey
|
|
138
|
-
},
|
|
139
|
-
...Object.keys(relation.targetColumns).map((column) => ({
|
|
140
|
-
key: `targetColumn:${column}`,
|
|
141
|
-
role: "column",
|
|
142
|
-
value: column
|
|
143
|
-
}))
|
|
144
|
-
] });
|
|
145
|
-
return {
|
|
146
|
-
...relation,
|
|
147
|
-
table: validated.identifiers.table.value,
|
|
148
|
-
alias: validated.identifiers.alias.value,
|
|
149
|
-
sourceKey: this.resolveColumn(meta, relation.sourceKey),
|
|
150
|
-
targetKey: validated.identifiers.targetKey.value,
|
|
151
|
-
targetColumns: Object.fromEntries(Object.keys(relation.targetColumns).map((column) => [validated.identifiers[`targetColumn:${column}`].value, relation.targetColumns[column]]))
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
validateFilterKey(meta, rawKey) {
|
|
155
|
-
const segments = rawKey.split("__");
|
|
156
|
-
if (segments.length > 2) throw new Error(`Invalid SQL lookup key: '${rawKey}'.`);
|
|
157
|
-
const field = segments[0];
|
|
158
|
-
const lookup = segments[1] ?? InternalLookupType.EXACT;
|
|
159
|
-
const validated = this.engine.validate({ lookupTokens: [{
|
|
160
|
-
key: rawKey,
|
|
161
|
-
lookup,
|
|
162
|
-
allowed: ALLOWED_LOOKUPS
|
|
163
|
-
}] });
|
|
164
|
-
return {
|
|
165
|
-
rawKey,
|
|
166
|
-
field,
|
|
167
|
-
lookup: validated.lookupTokens[rawKey].lookup,
|
|
168
|
-
qualifiedColumn: `${meta.table}.${this.resolveColumn(meta, field)}`
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
resolveColumn(meta, field) {
|
|
172
|
-
if (!(field in meta.columns)) throw new Error(`Unknown column '${field}' for table '${meta.table}'.`);
|
|
173
|
-
return field;
|
|
174
|
-
}
|
|
175
|
-
resolveRelation(meta, relationName) {
|
|
176
|
-
const relation = meta.relations?.[relationName];
|
|
177
|
-
if (!relation) throw new Error(`Unknown relation '${relationName}' for table '${meta.table}'.`);
|
|
178
|
-
return relation;
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
//#endregion
|
|
183
|
-
//#region src/query/compiler/QueryCompiler.ts
|
|
184
|
-
const sqlSafetyAdapter = new OrmSqlSafetyAdapter();
|
|
185
|
-
var QueryCompiler = class QueryCompiler {
|
|
186
|
-
static BRAND = "tango.orm.query_compiler";
|
|
187
|
-
__tangoBrand = QueryCompiler.BRAND;
|
|
188
|
-
/**
|
|
189
|
-
* Build a compiler for the given repository metadata and SQL dialect.
|
|
190
|
-
*/
|
|
191
|
-
constructor(meta, dialect = InternalDialect.POSTGRES) {
|
|
192
|
-
this.meta = meta;
|
|
193
|
-
this.dialect = dialect;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Narrow an unknown value to `QueryCompiler`.
|
|
197
|
-
*/
|
|
198
|
-
static isQueryCompiler(value) {
|
|
199
|
-
return typeof value === "object" && value !== null && value.__tangoBrand === QueryCompiler.BRAND;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Compile a query state tree into a SQL statement and bound parameters.
|
|
203
|
-
*/
|
|
204
|
-
compile(state) {
|
|
205
|
-
const selectRelationNames = state.selectRelated ?? [];
|
|
206
|
-
const prefetchRelationNames = state.prefetchRelated ?? [];
|
|
207
|
-
const relationNames = [...new Set([...selectRelationNames, ...prefetchRelationNames])];
|
|
208
|
-
const validatedPlan = sqlSafetyAdapter.validate({
|
|
209
|
-
kind: "select",
|
|
210
|
-
meta: this.meta,
|
|
211
|
-
selectFields: state.select?.map(String),
|
|
212
|
-
filterKeys: this.collectStateFilterKeys(state),
|
|
213
|
-
orderFields: state.order?.map((order) => String(order.by)),
|
|
214
|
-
relationNames
|
|
215
|
-
});
|
|
216
|
-
const table = validatedPlan.meta.table;
|
|
217
|
-
const whereParts = [];
|
|
218
|
-
const params = [];
|
|
219
|
-
if (state.q) {
|
|
220
|
-
const result = this.compileQNode(state.q, params.length + 1, validatedPlan.filterKeys);
|
|
221
|
-
if (result.sql) {
|
|
222
|
-
whereParts.push(result.sql);
|
|
223
|
-
params.push(...result.params);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
state.excludes?.forEach((exclude) => {
|
|
227
|
-
const result = this.compileQNode({
|
|
228
|
-
kind: InternalQNodeType.NOT,
|
|
229
|
-
node: exclude
|
|
230
|
-
}, params.length + 1, validatedPlan.filterKeys);
|
|
231
|
-
if (result.sql) {
|
|
232
|
-
whereParts.push(result.sql);
|
|
233
|
-
params.push(...result.params);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
const baseSelect = state.select?.length ? state.select.map((field) => validatedPlan.selectFields[String(field)]).join(", ") : `${table}.*`;
|
|
237
|
-
const relationSelects = selectRelationNames.flatMap((relationName) => {
|
|
238
|
-
const relation = validatedPlan.relations[relationName];
|
|
239
|
-
return Object.keys(relation.targetColumns).map((column) => `${relation.alias}.${column} AS ${this.buildHydrationColumnAlias(relation.alias, column)}`);
|
|
240
|
-
});
|
|
241
|
-
const prefetchSourceSelects = state.select?.length && prefetchRelationNames.length ? prefetchRelationNames.map((relationName) => validatedPlan.relations[relationName]).filter((relation) => !state.select.map(String).includes(relation.sourceKey)).map((relation) => `${table}.${relation.sourceKey} AS ${this.buildPrefetchSourceAlias(relation.sourceKey)}`) : [];
|
|
242
|
-
const select = [
|
|
243
|
-
baseSelect,
|
|
244
|
-
...relationSelects,
|
|
245
|
-
...prefetchSourceSelects
|
|
246
|
-
].join(", ");
|
|
247
|
-
const joins = selectRelationNames.map((rel) => {
|
|
248
|
-
const relation = validatedPlan.relations[rel];
|
|
249
|
-
if (relation.kind !== InternalRelationKind.BELONGS_TO && relation.kind !== InternalRelationKind.HAS_ONE) throw new Error(`Relation '${rel}' cannot be loaded with selectRelated(...).`);
|
|
250
|
-
return `LEFT JOIN ${relation.table} ${relation.alias} ON ${relation.alias}.${relation.targetKey} = ${table}.${relation.sourceKey}`;
|
|
251
|
-
}).filter(Boolean).join(" ");
|
|
252
|
-
for (const rel of prefetchRelationNames) {
|
|
253
|
-
const relation = validatedPlan.relations[rel];
|
|
254
|
-
if (relation?.kind !== InternalRelationKind.HAS_MANY) throw new Error(`Relation '${rel}' cannot be loaded with prefetchRelated(...).`);
|
|
255
|
-
}
|
|
256
|
-
const whereSQL = whereParts.length ? ` WHERE ${whereParts.join(" AND ")}` : "";
|
|
257
|
-
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`}`;
|
|
258
|
-
const limitSQL = state.limit ? ` LIMIT ${state.limit}` : "";
|
|
259
|
-
const offsetSQL = state.offset ? ` OFFSET ${state.offset}` : "";
|
|
260
|
-
const sql = `SELECT ${select} FROM ${table}${joins ? ` ${joins}` : ""}${whereSQL}${orderSQL}${limitSQL}${offsetSQL}`;
|
|
261
|
-
return {
|
|
262
|
-
sql,
|
|
263
|
-
params,
|
|
264
|
-
hydrations: selectRelationNames.map((relationName) => {
|
|
265
|
-
const relation = validatedPlan.relations[relationName];
|
|
266
|
-
return {
|
|
267
|
-
relationName,
|
|
268
|
-
alias: relation.alias,
|
|
269
|
-
columns: Object.fromEntries(Object.keys(relation.targetColumns).map((column) => [column, this.buildHydrationColumnAlias(relation.alias, column)]))
|
|
270
|
-
};
|
|
271
|
-
}),
|
|
272
|
-
prefetches: prefetchRelationNames.map((relationName) => {
|
|
273
|
-
const relation = validatedPlan.relations[relationName];
|
|
274
|
-
const needsAlias = !!state.select?.length && !state.select.map(String).includes(relation.sourceKey);
|
|
275
|
-
return {
|
|
276
|
-
relationName,
|
|
277
|
-
sourceKey: relation.sourceKey,
|
|
278
|
-
sourceKeyAlias: needsAlias ? this.buildPrefetchSourceAlias(relation.sourceKey) : undefined,
|
|
279
|
-
table: relation.table,
|
|
280
|
-
targetKey: relation.targetKey,
|
|
281
|
-
targetColumns: relation.targetColumns
|
|
282
|
-
};
|
|
283
|
-
})
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Compile the follow-up query used by `prefetchRelated(...)`.
|
|
288
|
-
*
|
|
289
|
-
* The base query cannot bind source values until after it has returned rows,
|
|
290
|
-
* but SQL rendering and validation still belong to the compiler.
|
|
291
|
-
*/
|
|
292
|
-
compilePrefetch(prefetch, sourceValues) {
|
|
293
|
-
const validatedPlan = sqlSafetyAdapter.validate({
|
|
294
|
-
kind: "select",
|
|
295
|
-
meta: this.meta,
|
|
296
|
-
relationNames: [prefetch.relationName]
|
|
297
|
-
});
|
|
298
|
-
const relation = validatedPlan.relations[prefetch.relationName];
|
|
299
|
-
const compiledTargetColumns = Object.keys(prefetch.targetColumns).sort();
|
|
300
|
-
const validatedTargetColumns = Object.keys(relation.targetColumns).sort();
|
|
301
|
-
const compiledMatchesValidated = prefetch.sourceKey === relation.sourceKey && prefetch.table === relation.table && prefetch.targetKey === relation.targetKey && compiledTargetColumns.length === validatedTargetColumns.length && compiledTargetColumns.every((column, index) => column === validatedTargetColumns[index] && prefetch.targetColumns[column] === relation.targetColumns[column]);
|
|
302
|
-
if (!compiledMatchesValidated) throw new Error(`Compiled prefetch metadata for relation '${prefetch.relationName}' failed validation.`);
|
|
303
|
-
const columns = Object.keys(relation.targetColumns);
|
|
304
|
-
const placeholders = this.dialect === InternalDialect.POSTGRES ? sourceValues.map((_, index) => `$${index + 1}`).join(", ") : sourceValues.map(() => "?").join(", ");
|
|
305
|
-
return {
|
|
306
|
-
sql: `SELECT ${columns.join(", ")} FROM ${relation.table} WHERE ${relation.targetKey} IN (${placeholders}) ORDER BY ${relation.targetKey} ASC`,
|
|
307
|
-
params: sourceValues,
|
|
308
|
-
targetKey: relation.targetKey,
|
|
309
|
-
targetColumns: relation.targetColumns
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
buildHydrationColumnAlias(alias, column) {
|
|
313
|
-
return this.assertInternalAliasDoesNotCollide(`__tango_hydrate_${alias}_${column}`);
|
|
314
|
-
}
|
|
315
|
-
buildPrefetchSourceAlias(sourceKey) {
|
|
316
|
-
return this.assertInternalAliasDoesNotCollide(`__tango_prefetch_${sourceKey}`);
|
|
317
|
-
}
|
|
318
|
-
assertInternalAliasDoesNotCollide(alias) {
|
|
319
|
-
if (alias in this.meta.columns) throw new Error(`Internal query alias '${alias}' collides with a field on table '${this.meta.table}'.`);
|
|
320
|
-
return alias;
|
|
321
|
-
}
|
|
322
|
-
compileQNode(node, paramIndex, filterKeys) {
|
|
323
|
-
switch (node.kind) {
|
|
324
|
-
case InternalQNodeType.ATOM: return this.compileAtom(node.where || {}, paramIndex, filterKeys);
|
|
325
|
-
case InternalQNodeType.AND: return this.compileAnd(node.nodes || [], paramIndex, filterKeys);
|
|
326
|
-
case InternalQNodeType.OR: return this.compileOr(node.nodes || [], paramIndex, filterKeys);
|
|
327
|
-
case InternalQNodeType.NOT: return this.compileNot(node.node, paramIndex, filterKeys);
|
|
328
|
-
default: return {
|
|
329
|
-
sql: "",
|
|
330
|
-
params: []
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
compileAtom(where, paramIndex, filterKeys) {
|
|
335
|
-
const entries = Object.entries(where).filter(([, value]) => value !== undefined);
|
|
336
|
-
const { parts, params } = entries.reduce((accumulator, [key, value]) => {
|
|
337
|
-
const descriptor = filterKeys[String(key)];
|
|
338
|
-
const idx = paramIndex + accumulator.params.length;
|
|
339
|
-
const clause = this.lookupToSQL(descriptor.qualifiedColumn, descriptor.lookup, value, idx);
|
|
340
|
-
accumulator.parts.push(clause.sql);
|
|
341
|
-
accumulator.params.push(...clause.params);
|
|
342
|
-
return accumulator;
|
|
343
|
-
}, {
|
|
344
|
-
parts: [],
|
|
345
|
-
params: []
|
|
346
|
-
});
|
|
347
|
-
return {
|
|
348
|
-
sql: parts.length ? `(${parts.join(" AND ")})` : "",
|
|
349
|
-
params
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
compileAnd(nodes, paramIndex, filterKeys) {
|
|
353
|
-
const { parts, params } = nodes.reduce((accumulator, node) => {
|
|
354
|
-
const result = this.compileQNode(node, paramIndex + accumulator.params.length, filterKeys);
|
|
355
|
-
if (result.sql) {
|
|
356
|
-
accumulator.parts.push(result.sql);
|
|
357
|
-
accumulator.params.push(...result.params);
|
|
358
|
-
}
|
|
359
|
-
return accumulator;
|
|
360
|
-
}, {
|
|
361
|
-
parts: [],
|
|
362
|
-
params: []
|
|
363
|
-
});
|
|
364
|
-
return {
|
|
365
|
-
sql: parts.length ? `(${parts.join(" AND ")})` : "",
|
|
366
|
-
params
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
compileOr(nodes, paramIndex, filterKeys) {
|
|
370
|
-
const { parts, params } = nodes.reduce((accumulator, node) => {
|
|
371
|
-
const result = this.compileQNode(node, paramIndex + accumulator.params.length, filterKeys);
|
|
372
|
-
if (result.sql) {
|
|
373
|
-
accumulator.parts.push(result.sql);
|
|
374
|
-
accumulator.params.push(...result.params);
|
|
375
|
-
}
|
|
376
|
-
return accumulator;
|
|
377
|
-
}, {
|
|
378
|
-
parts: [],
|
|
379
|
-
params: []
|
|
380
|
-
});
|
|
381
|
-
return {
|
|
382
|
-
sql: parts.length ? `(${parts.join(" OR ")})` : "",
|
|
383
|
-
params
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
compileNot(node, paramIndex, filterKeys) {
|
|
387
|
-
const result = this.compileQNode(node, paramIndex, filterKeys);
|
|
388
|
-
if (!result.sql) return {
|
|
389
|
-
sql: "",
|
|
390
|
-
params: []
|
|
391
|
-
};
|
|
392
|
-
return {
|
|
393
|
-
sql: `(NOT ${result.sql})`,
|
|
394
|
-
params: result.params
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
lookupToSQL(col, lookup, value, paramIndex) {
|
|
398
|
-
const placeholder = this.dialect === InternalDialect.POSTGRES ? `$${paramIndex}` : "?";
|
|
399
|
-
const normalized = this.normalizeParam(value);
|
|
400
|
-
switch (lookup) {
|
|
401
|
-
case InternalLookupType.EXACT:
|
|
402
|
-
if (value === null) return {
|
|
403
|
-
sql: `${col} IS NULL`,
|
|
404
|
-
params: []
|
|
405
|
-
};
|
|
406
|
-
return {
|
|
407
|
-
sql: `${col} = ${placeholder}`,
|
|
408
|
-
params: [normalized]
|
|
409
|
-
};
|
|
410
|
-
case InternalLookupType.LT: return {
|
|
411
|
-
sql: `${col} < ${placeholder}`,
|
|
412
|
-
params: [normalized]
|
|
413
|
-
};
|
|
414
|
-
case InternalLookupType.LTE: return {
|
|
415
|
-
sql: `${col} <= ${placeholder}`,
|
|
416
|
-
params: [normalized]
|
|
417
|
-
};
|
|
418
|
-
case InternalLookupType.GT: return {
|
|
419
|
-
sql: `${col} > ${placeholder}`,
|
|
420
|
-
params: [normalized]
|
|
421
|
-
};
|
|
422
|
-
case InternalLookupType.GTE: return {
|
|
423
|
-
sql: `${col} >= ${placeholder}`,
|
|
424
|
-
params: [normalized]
|
|
425
|
-
};
|
|
426
|
-
case InternalLookupType.IN: {
|
|
427
|
-
const entries = (Array.isArray(value) ? value : [value]).map((entry) => this.normalizeParam(entry));
|
|
428
|
-
if (entries.length === 0) return {
|
|
429
|
-
sql: "1=0",
|
|
430
|
-
params: []
|
|
431
|
-
};
|
|
432
|
-
const placeholders = this.dialect === InternalDialect.POSTGRES ? entries.map((_, index) => `$${paramIndex + index}`).join(", ") : entries.map(() => "?").join(", ");
|
|
433
|
-
return {
|
|
434
|
-
sql: `${col} IN (${placeholders})`,
|
|
435
|
-
params: entries
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
case InternalLookupType.ISNULL: return {
|
|
439
|
-
sql: value ? `${col} IS NULL` : `${col} IS NOT NULL`,
|
|
440
|
-
params: []
|
|
441
|
-
};
|
|
442
|
-
case InternalLookupType.CONTAINS: return {
|
|
443
|
-
sql: `${col} LIKE ${placeholder}`,
|
|
444
|
-
params: [`%${value}%`]
|
|
445
|
-
};
|
|
446
|
-
case InternalLookupType.ICONTAINS: {
|
|
447
|
-
const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
|
|
448
|
-
return {
|
|
449
|
-
sql: `${lowerCol} LIKE ${placeholder}`,
|
|
450
|
-
params: [`%${String(value).toLowerCase()}%`]
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
case InternalLookupType.STARTSWITH: return {
|
|
454
|
-
sql: `${col} LIKE ${placeholder}`,
|
|
455
|
-
params: [`${value}%`]
|
|
456
|
-
};
|
|
457
|
-
case InternalLookupType.ISTARTSWITH: {
|
|
458
|
-
const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
|
|
459
|
-
return {
|
|
460
|
-
sql: `${lowerCol} LIKE ${placeholder}`,
|
|
461
|
-
params: [`${String(value).toLowerCase()}%`]
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
case InternalLookupType.ENDSWITH: return {
|
|
465
|
-
sql: `${col} LIKE ${placeholder}`,
|
|
466
|
-
params: [`%${value}`]
|
|
467
|
-
};
|
|
468
|
-
case InternalLookupType.IENDSWITH: {
|
|
469
|
-
const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
|
|
470
|
-
return {
|
|
471
|
-
sql: `${lowerCol} LIKE ${placeholder}`,
|
|
472
|
-
params: [`%${String(value).toLowerCase()}`]
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
default: throw new Error(`Unknown lookup: ${lookup}`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
normalizeParam(value) {
|
|
479
|
-
if (this.dialect === InternalDialect.SQLITE && typeof value === "boolean") return value ? 1 : 0;
|
|
480
|
-
return value;
|
|
481
|
-
}
|
|
482
|
-
collectStateFilterKeys(state) {
|
|
483
|
-
const filterKeys = new Set();
|
|
484
|
-
if (state.q) this.collectNodeFilterKeys(state.q, filterKeys);
|
|
485
|
-
state.excludes?.forEach((exclude) => this.collectNodeFilterKeys(exclude, filterKeys));
|
|
486
|
-
return [...filterKeys];
|
|
487
|
-
}
|
|
488
|
-
collectNodeFilterKeys(node, filterKeys) {
|
|
489
|
-
Object.keys(node.where ?? {}).forEach((key) => filterKeys.add(key));
|
|
490
|
-
node.nodes?.forEach((child) => this.collectNodeFilterKeys(child, filterKeys));
|
|
491
|
-
if (node.node) this.collectNodeFilterKeys(node.node, filterKeys);
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
//#endregion
|
|
496
|
-
//#region src/query/compiler/index.ts
|
|
497
|
-
var compiler_exports = {};
|
|
498
|
-
__export(compiler_exports, { QueryCompiler: () => QueryCompiler });
|
|
499
|
-
|
|
500
|
-
//#endregion
|
|
501
|
-
//#region src/query/domain/RelationTyping.ts
|
|
502
|
-
const InternalRelationHydrationCardinality = {
|
|
503
|
-
SINGLE: "single",
|
|
504
|
-
MANY: "many"
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
//#endregion
|
|
508
|
-
//#region src/query/domain/index.ts
|
|
509
|
-
var domain_exports = {};
|
|
510
|
-
__export(domain_exports, { InternalRelationHydrationCardinality: () => InternalRelationHydrationCardinality });
|
|
511
|
-
|
|
512
|
-
//#endregion
|
|
513
|
-
//#region src/query/domain/internal/InternalDirection.ts
|
|
514
|
-
const InternalDirection = {
|
|
515
|
-
ASC: "asc",
|
|
516
|
-
DESC: "desc"
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
//#endregion
|
|
520
|
-
//#region src/query/QBuilder.ts
|
|
521
|
-
var QBuilder = class QBuilder {
|
|
522
|
-
static BRAND = "tango.orm.q_builder";
|
|
523
|
-
__tangoBrand = QBuilder.BRAND;
|
|
524
|
-
/**
|
|
525
|
-
* Narrow an unknown value to `QBuilder`.
|
|
526
|
-
*/
|
|
527
|
-
static isQBuilder(value) {
|
|
528
|
-
return typeof value === "object" && value !== null && value.__tangoBrand === QBuilder.BRAND;
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Combine multiple filter fragments using logical `AND`.
|
|
532
|
-
*/
|
|
533
|
-
static and(...nodes) {
|
|
534
|
-
return {
|
|
535
|
-
kind: InternalQNodeType.AND,
|
|
536
|
-
nodes: nodes.map(QBuilder.wrapNode)
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Combine multiple filter fragments using logical `OR`.
|
|
541
|
-
*/
|
|
542
|
-
static or(...nodes) {
|
|
543
|
-
return {
|
|
544
|
-
kind: InternalQNodeType.OR,
|
|
545
|
-
nodes: nodes.map(QBuilder.wrapNode)
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* Negate a filter fragment using logical `NOT`.
|
|
550
|
-
*/
|
|
551
|
-
static not(node) {
|
|
552
|
-
return {
|
|
553
|
-
kind: InternalQNodeType.NOT,
|
|
554
|
-
node: QBuilder.wrapNode(node)
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
static wrapNode(input) {
|
|
558
|
-
if (input.kind) return input;
|
|
559
|
-
return {
|
|
560
|
-
kind: InternalQNodeType.ATOM,
|
|
561
|
-
where: input
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
//#endregion
|
|
567
|
-
//#region src/query/QuerySet.ts
|
|
568
|
-
var QuerySet = class QuerySet {
|
|
569
|
-
static BRAND = "tango.orm.query_set";
|
|
570
|
-
__tangoBrand = QuerySet.BRAND;
|
|
571
|
-
constructor(executor, state = {}) {
|
|
572
|
-
this.executor = executor;
|
|
573
|
-
this.state = state;
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Narrow an unknown value to `QuerySet`.
|
|
577
|
-
*/
|
|
578
|
-
static isQuerySet(value) {
|
|
579
|
-
return typeof value === "object" && value !== null && value.__tangoBrand === QuerySet.BRAND;
|
|
580
|
-
}
|
|
581
|
-
/**
|
|
582
|
-
* Add a filter expression to the query.
|
|
583
|
-
*
|
|
584
|
-
* Multiple `filter()` calls are composed with `AND`.
|
|
585
|
-
*/
|
|
586
|
-
filter(q) {
|
|
587
|
-
const wrapped = q.kind ? q : {
|
|
588
|
-
kind: InternalQNodeType.ATOM,
|
|
589
|
-
where: q
|
|
590
|
-
};
|
|
591
|
-
const merged = this.state.q ? QBuilder.and(this.state.q, wrapped) : wrapped;
|
|
592
|
-
return new QuerySet(this.executor, {
|
|
593
|
-
...this.state,
|
|
594
|
-
q: merged
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Add an exclusion expression to the query.
|
|
599
|
-
*
|
|
600
|
-
* Exclusions are translated to `NOT (...)` predicates.
|
|
601
|
-
*/
|
|
602
|
-
exclude(q) {
|
|
603
|
-
const wrapped = q.kind ? q : {
|
|
604
|
-
kind: InternalQNodeType.ATOM,
|
|
605
|
-
where: q
|
|
606
|
-
};
|
|
607
|
-
const excludes = [...this.state.excludes ?? [], wrapped];
|
|
608
|
-
return new QuerySet(this.executor, {
|
|
609
|
-
...this.state,
|
|
610
|
-
excludes
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Apply ordering tokens such as `'name'` or `'-createdAt'`.
|
|
615
|
-
*/
|
|
616
|
-
orderBy(...tokens) {
|
|
617
|
-
const order = tokens.map((t) => {
|
|
618
|
-
const str = String(t);
|
|
619
|
-
if (str.startsWith("-")) return {
|
|
620
|
-
by: str.slice(1),
|
|
621
|
-
dir: InternalDirection.DESC
|
|
622
|
-
};
|
|
623
|
-
return {
|
|
624
|
-
by: t,
|
|
625
|
-
dir: InternalDirection.ASC
|
|
626
|
-
};
|
|
627
|
-
});
|
|
628
|
-
return new QuerySet(this.executor, {
|
|
629
|
-
...this.state,
|
|
630
|
-
order
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Limit the maximum number of rows returned.
|
|
635
|
-
*/
|
|
636
|
-
limit(n) {
|
|
637
|
-
return new QuerySet(this.executor, {
|
|
638
|
-
...this.state,
|
|
639
|
-
limit: n
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Skip the first `n` rows.
|
|
644
|
-
*/
|
|
645
|
-
offset(n) {
|
|
646
|
-
return new QuerySet(this.executor, {
|
|
647
|
-
...this.state,
|
|
648
|
-
offset: n
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
select(fields) {
|
|
652
|
-
return new QuerySet(this.executor, {
|
|
653
|
-
...this.state,
|
|
654
|
-
select: [...fields]
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Hydrate single-valued relations through SQL joins.
|
|
659
|
-
*
|
|
660
|
-
* Forward `belongsTo` relations can be inferred from the source model's
|
|
661
|
-
* field-authored relation metadata. Reverse `hasOne` relations can be
|
|
662
|
-
* selected with a target model generic when the target model points back to
|
|
663
|
-
* the source model.
|
|
664
|
-
*/
|
|
665
|
-
selectRelated(...rels) {
|
|
666
|
-
return new QuerySet(this.executor, {
|
|
667
|
-
...this.state,
|
|
668
|
-
selectRelated: [...rels]
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Hydrate collection relations with a follow-up query.
|
|
673
|
-
*
|
|
674
|
-
* Reverse `hasMany` relations can be prefetched with a target model generic
|
|
675
|
-
* when the target model points back to the source model.
|
|
676
|
-
*/
|
|
677
|
-
prefetchRelated(...rels) {
|
|
678
|
-
return new QuerySet(this.executor, {
|
|
679
|
-
...this.state,
|
|
680
|
-
prefetchRelated: [...rels]
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
async fetch(shape) {
|
|
684
|
-
this.validateHydrationState();
|
|
685
|
-
const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
|
|
686
|
-
const compiled = compiler.compile(this.state);
|
|
687
|
-
const rows = await this.executor.run(compiled);
|
|
688
|
-
const normalizedRows = this.normalizeRowsForSchemaParsing(rows, shape);
|
|
689
|
-
const hydratedRows = await this.hydrateRows(normalizedRows, compiled);
|
|
690
|
-
const projectedRows = hydratedRows;
|
|
691
|
-
const results = !shape ? projectedRows : typeof shape === "function" ? projectedRows.map(shape) : projectedRows.map((r) => shape.parse(r));
|
|
692
|
-
return {
|
|
693
|
-
results,
|
|
694
|
-
nextCursor: null
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
async fetchOne(shape) {
|
|
698
|
-
const limited = this.limit(1);
|
|
699
|
-
const result = !shape ? await limited.fetch() : typeof shape === "function" ? await limited.fetch(shape) : await limited.fetch(shape);
|
|
700
|
-
return result.results[0] ?? null;
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Execute a `COUNT(*)` query for the current filtered state.
|
|
704
|
-
*/
|
|
705
|
-
async count() {
|
|
706
|
-
this.validateHydrationState();
|
|
707
|
-
const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
|
|
708
|
-
const compiled = compiler.compile(this.state);
|
|
709
|
-
const countQuery = `SELECT COUNT(*) as count FROM (${compiled.sql}) AS tango_count_subquery`;
|
|
710
|
-
const rows = await this.executor.client.query(countQuery, compiled.params);
|
|
711
|
-
return Number(rows.rows[0]?.count ?? 0);
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Return whether at least one row matches the current query state.
|
|
715
|
-
*/
|
|
716
|
-
async exists() {
|
|
717
|
-
const count = await this.count();
|
|
718
|
-
return count > 0;
|
|
719
|
-
}
|
|
720
|
-
normalizeRowsForSchemaParsing(rows, shape) {
|
|
721
|
-
if (!shape || typeof shape === "function" || this.executor.dialect !== InternalDialect.SQLITE) return rows;
|
|
722
|
-
const booleanColumns = Object.entries(this.executor.meta.columns).filter(([, value]) => this.isBooleanColumnType(value)).map(([column]) => column);
|
|
723
|
-
if (booleanColumns.length === 0) return rows;
|
|
724
|
-
return rows.map((row) => this.normalizeBooleanColumns(row, booleanColumns));
|
|
725
|
-
}
|
|
726
|
-
validateHydrationState() {
|
|
727
|
-
const seen = new Set();
|
|
728
|
-
for (const relationName of [...this.state.selectRelated ?? [], ...this.state.prefetchRelated ?? []]) {
|
|
729
|
-
if (seen.has(relationName)) throw new Error(`Relation '${relationName}' was requested more than once.`);
|
|
730
|
-
seen.add(relationName);
|
|
731
|
-
const relation = this.executor.meta.relations?.[relationName];
|
|
732
|
-
if (!relation) throw new Error(`Unknown relation '${relationName}' for table '${this.executor.meta.table}'.`);
|
|
733
|
-
if (relation.kind === InternalRelationKind.MANY_TO_MANY) throw new Error(`Relation '${relationName}' is many-to-many and cannot be hydrated yet.`);
|
|
734
|
-
if (relationName in this.executor.meta.columns && !(relation.kind === InternalRelationKind.BELONGS_TO && relationName === relation.sourceKey)) throw new Error(`Relation '${relationName}' on table '${this.executor.meta.table}' collides with an existing field.`);
|
|
735
|
-
}
|
|
736
|
-
for (const relationName of this.state.selectRelated ?? []) {
|
|
737
|
-
const relation = this.executor.meta.relations[relationName];
|
|
738
|
-
if (relation.kind !== InternalRelationKind.BELONGS_TO && relation.kind !== InternalRelationKind.HAS_ONE) throw new Error(`Relation '${relationName}' cannot be loaded with selectRelated(...).`);
|
|
739
|
-
}
|
|
740
|
-
for (const relationName of this.state.prefetchRelated ?? []) {
|
|
741
|
-
const relation = this.executor.meta.relations[relationName];
|
|
742
|
-
if (relation.kind !== InternalRelationKind.HAS_MANY) throw new Error(`Relation '${relationName}' cannot be loaded with prefetchRelated(...).`);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
async hydrateRows(rows, compiled) {
|
|
746
|
-
const selectedRows = this.hydrateSelectedRows(rows, compiled);
|
|
747
|
-
return this.hydratePrefetchedRows(selectedRows, compiled);
|
|
748
|
-
}
|
|
749
|
-
hydrateSelectedRows(rows, compiled) {
|
|
750
|
-
const hydrations = compiled.hydrations;
|
|
751
|
-
if (!hydrations?.length) return rows;
|
|
752
|
-
return rows.map((row) => {
|
|
753
|
-
const next = { ...row };
|
|
754
|
-
for (const hydration of hydrations) {
|
|
755
|
-
const target = {};
|
|
756
|
-
let hasTargetValue = false;
|
|
757
|
-
for (const [column, alias] of Object.entries(hydration.columns)) {
|
|
758
|
-
const value = next[alias];
|
|
759
|
-
delete next[alias];
|
|
760
|
-
target[column] = this.normalizeTargetValue(hydration.relationName, column, value);
|
|
761
|
-
if (value !== null && value !== undefined) hasTargetValue = true;
|
|
762
|
-
}
|
|
763
|
-
next[hydration.relationName] = hasTargetValue ? target : null;
|
|
764
|
-
}
|
|
765
|
-
return next;
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
async hydratePrefetchedRows(rows, compiled) {
|
|
769
|
-
if (!compiled.prefetches?.length || rows.length === 0) return rows;
|
|
770
|
-
const prefetchGroups = await Promise.all(compiled.prefetches.map(async (prefetch) => {
|
|
771
|
-
const sourceValues = rows.map((row) => row[prefetch.sourceKeyAlias ?? prefetch.sourceKey]).filter((value) => typeof value === "string" || typeof value === "number");
|
|
772
|
-
const uniqueSourceValues = [...new Set(sourceValues)];
|
|
773
|
-
return {
|
|
774
|
-
prefetch,
|
|
775
|
-
grouped: await this.fetchPrefetchGroup(prefetch, uniqueSourceValues)
|
|
776
|
-
};
|
|
777
|
-
}));
|
|
778
|
-
const hiddenSourceAliases = new Set(compiled.prefetches.map((prefetch) => prefetch.sourceKeyAlias).filter((alias) => typeof alias === "string"));
|
|
779
|
-
return rows.map((row) => {
|
|
780
|
-
const next = { ...row };
|
|
781
|
-
for (const { prefetch, grouped } of prefetchGroups) {
|
|
782
|
-
const sourceValue = row[prefetch.sourceKeyAlias ?? prefetch.sourceKey];
|
|
783
|
-
next[prefetch.relationName] = typeof sourceValue === "string" || typeof sourceValue === "number" ? grouped.get(sourceValue) ?? [] : [];
|
|
784
|
-
}
|
|
785
|
-
for (const alias of hiddenSourceAliases) delete next[alias];
|
|
786
|
-
return next;
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
async fetchPrefetchGroup(prefetch, sourceValues) {
|
|
790
|
-
const compiledPrefetch = new QueryCompiler(this.executor.meta, this.executor.dialect).compilePrefetch(prefetch, sourceValues);
|
|
791
|
-
const grouped = new Map();
|
|
792
|
-
if (sourceValues.length === 0) return grouped;
|
|
793
|
-
const result = await this.executor.client.query(compiledPrefetch.sql, compiledPrefetch.params);
|
|
794
|
-
for (const row of result.rows) {
|
|
795
|
-
const normalized = this.normalizeTargetRow(compiledPrefetch, row);
|
|
796
|
-
const key = normalized[compiledPrefetch.targetKey];
|
|
797
|
-
if (typeof key !== "string" && typeof key !== "number") continue;
|
|
798
|
-
const bucket = grouped.get(key) ?? [];
|
|
799
|
-
bucket.push(normalized);
|
|
800
|
-
grouped.set(key, bucket);
|
|
801
|
-
}
|
|
802
|
-
return grouped;
|
|
803
|
-
}
|
|
804
|
-
normalizeTargetRow(prefetch, row) {
|
|
805
|
-
if (this.executor.dialect !== InternalDialect.SQLITE) return row;
|
|
806
|
-
let normalized = null;
|
|
807
|
-
for (const [column, type] of Object.entries(prefetch.targetColumns)) {
|
|
808
|
-
if (!this.isBooleanColumnType(type)) continue;
|
|
809
|
-
const next = this.normalizeSqliteBoolean(row[column]);
|
|
810
|
-
if (next === row[column]) continue;
|
|
811
|
-
normalized ??= { ...row };
|
|
812
|
-
normalized[column] = next;
|
|
813
|
-
}
|
|
814
|
-
return normalized ?? row;
|
|
815
|
-
}
|
|
816
|
-
normalizeTargetValue(relationName, column, value) {
|
|
817
|
-
if (this.executor.dialect !== InternalDialect.SQLITE) return value;
|
|
818
|
-
const relation = this.executor.meta.relations[relationName];
|
|
819
|
-
return this.isBooleanColumnType(relation.targetColumns[column]) ? this.normalizeSqliteBoolean(value) : value;
|
|
820
|
-
}
|
|
821
|
-
isBooleanColumnType(value) {
|
|
822
|
-
return typeof value === "string" && ["bool", "boolean"].includes(value.trim().toLowerCase());
|
|
823
|
-
}
|
|
824
|
-
normalizeSqliteBoolean(value) {
|
|
825
|
-
if (value === 0 || value === "0") return false;
|
|
826
|
-
if (value === 1 || value === "1") return true;
|
|
827
|
-
return value;
|
|
828
|
-
}
|
|
829
|
-
normalizeBooleanColumns(row, columns) {
|
|
830
|
-
let normalized = null;
|
|
831
|
-
for (const column of columns) {
|
|
832
|
-
const current = row[column];
|
|
833
|
-
const next = this.normalizeSqliteBoolean(current);
|
|
834
|
-
if (next === current) continue;
|
|
835
|
-
if (!normalized) normalized = { ...row };
|
|
836
|
-
normalized[column] = next;
|
|
837
|
-
}
|
|
838
|
-
return normalized ?? row;
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
//#endregion
|
|
843
|
-
//#region src/query/index.ts
|
|
844
|
-
var query_exports = {};
|
|
845
|
-
__export(query_exports, {
|
|
846
|
-
Q: () => QBuilder,
|
|
847
|
-
QBuilder: () => QBuilder,
|
|
848
|
-
QueryCompiler: () => QueryCompiler,
|
|
849
|
-
QuerySet: () => QuerySet,
|
|
850
|
-
compiler: () => compiler_exports,
|
|
851
|
-
domain: () => domain_exports
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
//#endregion
|
|
855
|
-
export { InternalRelationKind, OrmSqlSafetyAdapter, QBuilder, QueryCompiler, QuerySet, compiler_exports, domain_exports, query_exports };
|
|
856
|
-
//# sourceMappingURL=query-CWZ1cfjo.js.map
|