@bunyad/orm 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/casts.d.ts +50 -0
- package/dist/casts.js +309 -0
- package/dist/concerns.d.ts +64 -0
- package/dist/concerns.js +216 -0
- package/dist/decorators.d.ts +7 -0
- package/dist/decorators.js +12 -0
- package/dist/eager-fetch.d.ts +7 -0
- package/dist/eager-fetch.js +38 -0
- package/dist/eager.d.ts +24 -0
- package/dist/eager.js +854 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +265 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/model-events.d.ts +43 -0
- package/dist/model-events.js +138 -0
- package/dist/model-helpers.d.ts +32 -0
- package/dist/model-helpers.js +99 -0
- package/dist/model-query.d.ts +312 -0
- package/dist/model-query.js +3170 -0
- package/dist/model-strictness.d.ts +35 -0
- package/dist/model-strictness.js +114 -0
- package/dist/model.d.ts +490 -0
- package/dist/model.js +1931 -0
- package/dist/orm-collection.d.ts +37 -0
- package/dist/orm-collection.js +150 -0
- package/dist/pivot.d.ts +9 -0
- package/dist/pivot.js +9 -0
- package/dist/relations.d.ts +335 -0
- package/dist/relations.js +1164 -0
- package/dist/scopes.d.ts +36 -0
- package/dist/scopes.js +128 -0
- package/package.json +49 -0
package/dist/eager.js
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eager-load relations and aggregates onto models.
|
|
3
|
+
*/
|
|
4
|
+
import { wrapSqlName } from "@bunyad/database";
|
|
5
|
+
import { OrmCollection } from "./orm-collection.js";
|
|
6
|
+
import { softDeleteAliasSql, uniqueKeys, coerceEagerRelations, } from "./model-helpers.js";
|
|
7
|
+
import { eagerFetchByKeys } from "./eager-fetch.js";
|
|
8
|
+
import { Model } from "./model.js";
|
|
9
|
+
import { enterEagerLoad, leaveEagerLoad } from "./model-strictness.js";
|
|
10
|
+
import { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, aggregateRelation, applyPivotAttributes, resolveMorphType, resolveRelation, } from "./relations.js";
|
|
11
|
+
/** Eager-load named relations onto models (`with` / `load` / collections).
|
|
12
|
+
* Batches BelongsTo / HasMany / HasOne / BelongsToMany / Morph* / ofMany with
|
|
13
|
+
* `WHERE IN`. Nested paths like `parent.children` are supported.
|
|
14
|
+
* Independent top-level relations load in parallel.
|
|
15
|
+
* Constraints from `with({ posts: (q) => q.where(…) })` are applied per relation.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Eager-load relations onto models.
|
|
19
|
+
* Returns void when all work is synchronous (SQLite BelongsTo fast-find),
|
|
20
|
+
* otherwise a Promise — callers must branch like `get()` hydrate does.
|
|
21
|
+
*/
|
|
22
|
+
export function eagerLoadModels(models, relations) {
|
|
23
|
+
const list = models;
|
|
24
|
+
const entries = coerceEagerRelations(relations);
|
|
25
|
+
if (entries.length === 0 || list.length === 0)
|
|
26
|
+
return;
|
|
27
|
+
enterEagerLoad();
|
|
28
|
+
let asyncWork;
|
|
29
|
+
try {
|
|
30
|
+
asyncWork = eagerLoadModelsBody(list, entries);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
leaveEagerLoad();
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
if (asyncWork && typeof asyncWork.then === "function") {
|
|
37
|
+
return asyncWork.finally(() => {
|
|
38
|
+
leaveEagerLoad();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
leaveEagerLoad();
|
|
42
|
+
return asyncWork;
|
|
43
|
+
}
|
|
44
|
+
function eagerLoadModelsBody(list, entries) {
|
|
45
|
+
const grouped = new Map();
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (typeof entry.name !== "string" || !entry.name.trim())
|
|
48
|
+
continue;
|
|
49
|
+
const [head, ...rest] = entry.name.split(".");
|
|
50
|
+
if (!head)
|
|
51
|
+
continue;
|
|
52
|
+
let group = grouped.get(head);
|
|
53
|
+
if (!group) {
|
|
54
|
+
group = { nested: [] };
|
|
55
|
+
grouped.set(head, group);
|
|
56
|
+
}
|
|
57
|
+
if (rest.length > 0) {
|
|
58
|
+
group.nested.push({
|
|
59
|
+
name: rest.join("."),
|
|
60
|
+
constraint: entry.constraint,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (entry.constraint) {
|
|
64
|
+
group.constraint = entry.constraint;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const pending = [];
|
|
68
|
+
for (const [relation, group] of grouped) {
|
|
69
|
+
const result = eagerLoadOneRelation(list, relation, group.constraint);
|
|
70
|
+
if (result)
|
|
71
|
+
pending.push(result);
|
|
72
|
+
}
|
|
73
|
+
const finishNested = () => {
|
|
74
|
+
const nestedPending = [];
|
|
75
|
+
for (const [relation, group] of grouped) {
|
|
76
|
+
if (group.nested.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
const children = [];
|
|
79
|
+
for (const model of list) {
|
|
80
|
+
const value = model[relation];
|
|
81
|
+
if (value == null)
|
|
82
|
+
continue;
|
|
83
|
+
if (value instanceof OrmCollection) {
|
|
84
|
+
for (const child of value)
|
|
85
|
+
children.push(child);
|
|
86
|
+
}
|
|
87
|
+
else if (Array.isArray(value)) {
|
|
88
|
+
children.push(...value);
|
|
89
|
+
}
|
|
90
|
+
else if (typeof value === "object") {
|
|
91
|
+
children.push(value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (children.length > 0) {
|
|
95
|
+
const nestedResult = eagerLoadModels(children, group.nested);
|
|
96
|
+
if (nestedResult)
|
|
97
|
+
nestedPending.push(nestedResult);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (nestedPending.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
return Promise.all(nestedPending).then(() => undefined);
|
|
103
|
+
};
|
|
104
|
+
if (pending.length === 0)
|
|
105
|
+
return finishNested();
|
|
106
|
+
return Promise.all(pending).then(() => finishNested());
|
|
107
|
+
}
|
|
108
|
+
function applyConstraint(q, constraint) {
|
|
109
|
+
if (constraint)
|
|
110
|
+
constraint(q);
|
|
111
|
+
return q;
|
|
112
|
+
}
|
|
113
|
+
function relationFromModel(model, relation) {
|
|
114
|
+
try {
|
|
115
|
+
return model.related(relation);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Keep `WHERE IN` under common driver bind limits. */
|
|
122
|
+
const EAGER_IN_CHUNK = 900;
|
|
123
|
+
/** Max concurrent IN-chunk queries (bulk eager / sync). Pages ≤900 ids stay single-query. */
|
|
124
|
+
const EAGER_IN_CONCURRENCY = 3;
|
|
125
|
+
async function forEachIdChunk(ids, run) {
|
|
126
|
+
if (ids.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
if (ids.length <= EAGER_IN_CHUNK) {
|
|
129
|
+
await run(ids);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const chunks = [];
|
|
133
|
+
for (let i = 0; i < ids.length; i += EAGER_IN_CHUNK) {
|
|
134
|
+
chunks.push(ids.slice(i, i + EAGER_IN_CHUNK));
|
|
135
|
+
}
|
|
136
|
+
// Bounded pool — avoids pool stampede when stacking on parallel with() relations.
|
|
137
|
+
let next = 0;
|
|
138
|
+
const workers = Array.from({ length: Math.min(EAGER_IN_CONCURRENCY, chunks.length) }, async () => {
|
|
139
|
+
while (next < chunks.length) {
|
|
140
|
+
const index = next++;
|
|
141
|
+
await run(chunks[index]);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
await Promise.all(workers);
|
|
145
|
+
}
|
|
146
|
+
function eagerLoadOneRelation(models, relation, constraint) {
|
|
147
|
+
const sampleRel = relationFromModel(models[0], relation);
|
|
148
|
+
if (!sampleRel)
|
|
149
|
+
return;
|
|
150
|
+
if (sampleRel instanceof HasOne && sampleRel.isOfMany()) {
|
|
151
|
+
return eagerLoadHasOneOfMany(models, relation, sampleRel, constraint);
|
|
152
|
+
}
|
|
153
|
+
if (sampleRel instanceof MorphOne && sampleRel.isOfMany()) {
|
|
154
|
+
return eagerLoadMorphOneOfMany(models, relation, sampleRel, constraint);
|
|
155
|
+
}
|
|
156
|
+
if (sampleRel instanceof BelongsTo) {
|
|
157
|
+
return eagerLoadBelongsTo(models, relation, sampleRel, constraint);
|
|
158
|
+
}
|
|
159
|
+
if (sampleRel instanceof HasMany) {
|
|
160
|
+
return eagerLoadHasMany(models, relation, sampleRel, constraint);
|
|
161
|
+
}
|
|
162
|
+
if (sampleRel instanceof HasOne) {
|
|
163
|
+
return eagerLoadHasOne(models, relation, sampleRel, constraint);
|
|
164
|
+
}
|
|
165
|
+
if (sampleRel instanceof BelongsToMany) {
|
|
166
|
+
return eagerLoadBelongsToMany(models, relation, sampleRel, constraint);
|
|
167
|
+
}
|
|
168
|
+
if (sampleRel instanceof MorphToMany) {
|
|
169
|
+
return eagerLoadMorphToMany(models, relation, sampleRel, constraint);
|
|
170
|
+
}
|
|
171
|
+
if (sampleRel instanceof MorphMany) {
|
|
172
|
+
return eagerLoadMorphMany(models, relation, sampleRel, constraint);
|
|
173
|
+
}
|
|
174
|
+
if (sampleRel instanceof MorphOne) {
|
|
175
|
+
return eagerLoadMorphOne(models, relation, sampleRel, constraint);
|
|
176
|
+
}
|
|
177
|
+
if (sampleRel instanceof MorphTo) {
|
|
178
|
+
return eagerLoadMorphTo(models, relation, sampleRel, constraint);
|
|
179
|
+
}
|
|
180
|
+
if (sampleRel instanceof HasManyThrough) {
|
|
181
|
+
return eagerLoadHasManyThrough(models, relation, sampleRel, constraint);
|
|
182
|
+
}
|
|
183
|
+
if (sampleRel instanceof HasOneThrough) {
|
|
184
|
+
return eagerLoadHasOneThrough(models, relation, sampleRel, constraint);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Single-query ofMany via GROUP BY join (avoids per-row correlated subqueries). */
|
|
188
|
+
async function eagerLoadHasOneOfMany(models, relation, sample, constraint) {
|
|
189
|
+
const foreignKey = sample.getForeignKeyName();
|
|
190
|
+
const localKey = sample.getLocalKeyName();
|
|
191
|
+
const Related = sample.getRelated();
|
|
192
|
+
const ofManyColumn = sample.getOfManyColumn();
|
|
193
|
+
const ofManyAggregate = sample.getOfManyAggregate();
|
|
194
|
+
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
195
|
+
const byParent = new Map();
|
|
196
|
+
if (parentIds.length > 0 && constraint) {
|
|
197
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
198
|
+
let q = Related.whereIn(foreignKey, chunk);
|
|
199
|
+
applyConstraint(q, constraint);
|
|
200
|
+
const related = await q.get();
|
|
201
|
+
const pick = ofManyAggregate === "max"
|
|
202
|
+
? (a, b) => (a > b ? a : b)
|
|
203
|
+
: (a, b) => (a < b ? a : b);
|
|
204
|
+
const best = new Map();
|
|
205
|
+
for (const row of related.all()) {
|
|
206
|
+
const key = String(row[foreignKey]);
|
|
207
|
+
const value = row[ofManyColumn];
|
|
208
|
+
const prev = best.get(key);
|
|
209
|
+
if (!prev || pick(value, prev.value) === value) {
|
|
210
|
+
best.set(key, { value, model: row });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [key, entry] of best) {
|
|
214
|
+
if (!byParent.has(key))
|
|
215
|
+
byParent.set(key, entry.model);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (parentIds.length > 0) {
|
|
220
|
+
const dialect = Related.getConnection().dialect;
|
|
221
|
+
const table = wrapSqlName(dialect, Related.table);
|
|
222
|
+
const col = wrapSqlName(dialect, ofManyColumn);
|
|
223
|
+
const fk = wrapSqlName(dialect, foreignKey);
|
|
224
|
+
const agg = ofManyAggregate.toUpperCase();
|
|
225
|
+
const softOuter = softDeleteAliasSql(Related, table);
|
|
226
|
+
const softSub = softDeleteAliasSql(Related, "bunyad_of_many");
|
|
227
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
228
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
229
|
+
const rows = await Related.getConnection().all(`SELECT ${table}.*
|
|
230
|
+
FROM ${table}
|
|
231
|
+
INNER JOIN (
|
|
232
|
+
SELECT bunyad_of_many.${fk} AS __bunyad_fk, ${agg}(bunyad_of_many.${col}) AS __bunyad_agg
|
|
233
|
+
FROM ${table} AS bunyad_of_many
|
|
234
|
+
WHERE bunyad_of_many.${fk} IN (${placeholders})${softSub}
|
|
235
|
+
GROUP BY bunyad_of_many.${fk}
|
|
236
|
+
) AS bunyad_agg
|
|
237
|
+
ON ${table}.${fk} = bunyad_agg.__bunyad_fk
|
|
238
|
+
AND ${table}.${col} = bunyad_agg.__bunyad_agg
|
|
239
|
+
WHERE ${table}.${fk} IN (${placeholders})${softOuter}`, [...chunk, ...chunk]);
|
|
240
|
+
for (const row of rows) {
|
|
241
|
+
const key = String(row[foreignKey]);
|
|
242
|
+
if (!byParent.has(key))
|
|
243
|
+
byParent.set(key, new Related(row));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
for (const model of models) {
|
|
248
|
+
const id = model[localKey];
|
|
249
|
+
model[relation] =
|
|
250
|
+
id == null ? null : (byParent.get(String(id)) ?? null);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function eagerLoadMorphOneOfMany(models, relation, sample, constraint) {
|
|
254
|
+
const Related = sample.getRelated();
|
|
255
|
+
const typeColumn = sample.getTypeColumn();
|
|
256
|
+
const idColumn = sample.getIdColumn();
|
|
257
|
+
const morphType = sample.getMorphType();
|
|
258
|
+
const localKey = sample.getLocalKeyName();
|
|
259
|
+
const ofManyColumn = sample.getOfManyColumn();
|
|
260
|
+
const ofManyAggregate = sample.getOfManyAggregate();
|
|
261
|
+
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
262
|
+
const byParent = new Map();
|
|
263
|
+
if (parentIds.length > 0 && constraint) {
|
|
264
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
265
|
+
let q = Related.where(typeColumn, morphType).whereIn(idColumn, chunk);
|
|
266
|
+
applyConstraint(q, constraint);
|
|
267
|
+
const related = await q.get();
|
|
268
|
+
const pick = ofManyAggregate === "max"
|
|
269
|
+
? (a, b) => (a > b ? a : b)
|
|
270
|
+
: (a, b) => (a < b ? a : b);
|
|
271
|
+
const best = new Map();
|
|
272
|
+
for (const row of related.all()) {
|
|
273
|
+
const key = String(row[idColumn]);
|
|
274
|
+
const value = row[ofManyColumn];
|
|
275
|
+
const prev = best.get(key);
|
|
276
|
+
if (!prev || pick(value, prev.value) === value) {
|
|
277
|
+
best.set(key, { value, model: row });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const [key, entry] of best) {
|
|
281
|
+
if (!byParent.has(key))
|
|
282
|
+
byParent.set(key, entry.model);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
else if (parentIds.length > 0) {
|
|
287
|
+
const dialect = Related.getConnection().dialect;
|
|
288
|
+
const table = wrapSqlName(dialect, Related.table);
|
|
289
|
+
const col = wrapSqlName(dialect, ofManyColumn);
|
|
290
|
+
const typeCol = wrapSqlName(dialect, typeColumn);
|
|
291
|
+
const idCol = wrapSqlName(dialect, idColumn);
|
|
292
|
+
const agg = ofManyAggregate.toUpperCase();
|
|
293
|
+
const softOuter = softDeleteAliasSql(Related, table);
|
|
294
|
+
const softSub = softDeleteAliasSql(Related, "bunyad_of_many");
|
|
295
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
296
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
297
|
+
const rows = await Related.getConnection().all(`SELECT ${table}.*
|
|
298
|
+
FROM ${table}
|
|
299
|
+
INNER JOIN (
|
|
300
|
+
SELECT bunyad_of_many.${idCol} AS __bunyad_fk, ${agg}(bunyad_of_many.${col}) AS __bunyad_agg
|
|
301
|
+
FROM ${table} AS bunyad_of_many
|
|
302
|
+
WHERE bunyad_of_many.${typeCol} = ?
|
|
303
|
+
AND bunyad_of_many.${idCol} IN (${placeholders})${softSub}
|
|
304
|
+
GROUP BY bunyad_of_many.${idCol}
|
|
305
|
+
) AS bunyad_agg
|
|
306
|
+
ON ${table}.${idCol} = bunyad_agg.__bunyad_fk
|
|
307
|
+
AND ${table}.${col} = bunyad_agg.__bunyad_agg
|
|
308
|
+
WHERE ${table}.${typeCol} = ?
|
|
309
|
+
AND ${table}.${idCol} IN (${placeholders})${softOuter}`, [morphType, ...chunk, morphType, ...chunk]);
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
const key = String(row[idColumn]);
|
|
312
|
+
if (!byParent.has(key))
|
|
313
|
+
byParent.set(key, new Related(row));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
for (const model of models) {
|
|
318
|
+
const id = model[localKey];
|
|
319
|
+
model[relation] =
|
|
320
|
+
id == null ? null : (byParent.get(String(id)) ?? null);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function eagerLoadBelongsTo(models, relation, sample, constraint) {
|
|
324
|
+
const foreignKey = sample.getForeignKeyName();
|
|
325
|
+
const ownerKey = sample.getOwnerKeyName();
|
|
326
|
+
const Related = sample.getRelated();
|
|
327
|
+
const ids = uniqueKeys(models.map((model) => model[foreignKey]));
|
|
328
|
+
if (ids.length === 0) {
|
|
329
|
+
for (const model of models) {
|
|
330
|
+
model[relation] = null;
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const byOwner = new Map();
|
|
335
|
+
return (async () => {
|
|
336
|
+
await forEachIdChunk(ids, async (chunk) => {
|
|
337
|
+
let related;
|
|
338
|
+
if (constraint) {
|
|
339
|
+
let q = Related.whereIn(ownerKey, chunk);
|
|
340
|
+
applyConstraint(q, constraint);
|
|
341
|
+
related = (await q.get()).all();
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
related = await eagerFetchByKeys(Related, ownerKey, chunk);
|
|
345
|
+
}
|
|
346
|
+
for (const row of related) {
|
|
347
|
+
byOwner.set(String(row[ownerKey]), row);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
for (const model of models) {
|
|
351
|
+
const id = model[foreignKey];
|
|
352
|
+
model[relation] =
|
|
353
|
+
id == null ? null : (byOwner.get(String(id)) ?? null);
|
|
354
|
+
}
|
|
355
|
+
})();
|
|
356
|
+
}
|
|
357
|
+
async function eagerLoadHasMany(models, relation, sample, constraint) {
|
|
358
|
+
const foreignKey = sample.getForeignKeyName();
|
|
359
|
+
const localKey = sample.getLocalKeyName();
|
|
360
|
+
const Related = sample.getRelated();
|
|
361
|
+
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
362
|
+
const byParent = new Map();
|
|
363
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
364
|
+
let related;
|
|
365
|
+
if (constraint) {
|
|
366
|
+
let q = Related.whereIn(foreignKey, chunk);
|
|
367
|
+
applyConstraint(q, constraint);
|
|
368
|
+
related = (await q.get()).all();
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
related = await eagerFetchByKeys(Related, foreignKey, chunk);
|
|
372
|
+
}
|
|
373
|
+
for (const row of related) {
|
|
374
|
+
const key = String(row[foreignKey]);
|
|
375
|
+
const list = byParent.get(key) ?? [];
|
|
376
|
+
list.push(row);
|
|
377
|
+
byParent.set(key, list);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
for (const model of models) {
|
|
381
|
+
const id = model[localKey];
|
|
382
|
+
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function eagerLoadHasOne(models, relation, sample, constraint) {
|
|
386
|
+
const foreignKey = sample.getForeignKeyName();
|
|
387
|
+
const localKey = sample.getLocalKeyName();
|
|
388
|
+
const Related = sample.getRelated();
|
|
389
|
+
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
390
|
+
const byParent = new Map();
|
|
391
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
392
|
+
let related;
|
|
393
|
+
if (constraint) {
|
|
394
|
+
let q = Related.whereIn(foreignKey, chunk);
|
|
395
|
+
applyConstraint(q, constraint);
|
|
396
|
+
related = (await q.get()).all();
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
related = await eagerFetchByKeys(Related, foreignKey, chunk);
|
|
400
|
+
}
|
|
401
|
+
for (const row of related) {
|
|
402
|
+
const key = String(row[foreignKey]);
|
|
403
|
+
if (!byParent.has(key))
|
|
404
|
+
byParent.set(key, row);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
for (const model of models) {
|
|
408
|
+
const id = model[localKey];
|
|
409
|
+
model[relation] =
|
|
410
|
+
id == null ? null : (byParent.get(String(id)) ?? null);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function eagerLoadHasManyThrough(models, relation, sample, constraint) {
|
|
414
|
+
const Through = sample.getThrough();
|
|
415
|
+
const Related = sample.getRelated();
|
|
416
|
+
const firstKey = sample.getFirstKeyName();
|
|
417
|
+
const secondKey = sample.getSecondKeyName();
|
|
418
|
+
const localKey = sample.getLocalKeyName();
|
|
419
|
+
const secondLocalKey = sample.getSecondLocalKeyName();
|
|
420
|
+
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
421
|
+
const throughByParent = new Map();
|
|
422
|
+
const throughIdToParent = new Map();
|
|
423
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
424
|
+
const throughRows = await eagerFetchByKeys(Through, firstKey, chunk);
|
|
425
|
+
for (const row of throughRows) {
|
|
426
|
+
const rec = row;
|
|
427
|
+
const parentKey = String(rec[firstKey]);
|
|
428
|
+
const throughId = rec[secondLocalKey];
|
|
429
|
+
if (throughId == null)
|
|
430
|
+
continue;
|
|
431
|
+
const list = throughByParent.get(parentKey) ?? [];
|
|
432
|
+
list.push(throughId);
|
|
433
|
+
throughByParent.set(parentKey, list);
|
|
434
|
+
throughIdToParent.set(String(throughId), parentKey);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
const throughIds = uniqueKeys([...throughIdToParent.keys()]);
|
|
438
|
+
const byParent = new Map();
|
|
439
|
+
await forEachIdChunk(throughIds, async (chunk) => {
|
|
440
|
+
let relatedRows;
|
|
441
|
+
if (constraint) {
|
|
442
|
+
let q = Related.whereIn(secondKey, chunk);
|
|
443
|
+
applyConstraint(q, constraint);
|
|
444
|
+
relatedRows = (await q.get()).all();
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
relatedRows = await eagerFetchByKeys(Related, secondKey, chunk);
|
|
448
|
+
}
|
|
449
|
+
for (const row of relatedRows) {
|
|
450
|
+
const throughFk = row[secondKey];
|
|
451
|
+
if (throughFk == null)
|
|
452
|
+
continue;
|
|
453
|
+
const parentKey = throughIdToParent.get(String(throughFk));
|
|
454
|
+
if (parentKey == null)
|
|
455
|
+
continue;
|
|
456
|
+
const list = byParent.get(parentKey) ?? [];
|
|
457
|
+
list.push(row);
|
|
458
|
+
byParent.set(parentKey, list);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
for (const model of models) {
|
|
462
|
+
const id = model[localKey];
|
|
463
|
+
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function eagerLoadHasOneThrough(models, relation, sample, constraint) {
|
|
467
|
+
await eagerLoadHasManyThrough(models, relation, sample.asHasManyThrough(), constraint);
|
|
468
|
+
for (const model of models) {
|
|
469
|
+
const collection = model[relation];
|
|
470
|
+
if (collection && typeof collection.first === "function") {
|
|
471
|
+
model[relation] =
|
|
472
|
+
collection.first() ?? null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function eagerLoadBelongsToMany(models, relation, sample, constraint) {
|
|
477
|
+
const Related = sample.getRelated();
|
|
478
|
+
const parent = models[0].constructor;
|
|
479
|
+
const parentKey = parent.primaryKey;
|
|
480
|
+
const pivotTable = sample.getPivotTable();
|
|
481
|
+
const foreignPivotKey = sample.getForeignPivotKeyName();
|
|
482
|
+
const relatedPivotKey = sample.getRelatedPivotKeyName();
|
|
483
|
+
const pivotColumns = sample.getPivotColumns();
|
|
484
|
+
const pivotKeys = pivotColumns.length > 0
|
|
485
|
+
? [foreignPivotKey, relatedPivotKey, ...pivotColumns.filter((c) => c !== foreignPivotKey && c !== relatedPivotKey)]
|
|
486
|
+
: [];
|
|
487
|
+
const parentIds = uniqueKeys(models.map((model) => model[parentKey]));
|
|
488
|
+
const byParent = new Map();
|
|
489
|
+
if (parentIds.length > 0) {
|
|
490
|
+
const dialect = parent.getConnection().dialect;
|
|
491
|
+
const qRelated = wrapSqlName(dialect, Related.table);
|
|
492
|
+
const qPivot = wrapSqlName(dialect, pivotTable);
|
|
493
|
+
const qRelatedPk = wrapSqlName(dialect, Related.primaryKey);
|
|
494
|
+
const qForeignPivot = wrapSqlName(dialect, foreignPivotKey);
|
|
495
|
+
const qRelatedPivot = wrapSqlName(dialect, relatedPivotKey);
|
|
496
|
+
const soft = softDeleteAliasSql(Related, qRelated);
|
|
497
|
+
const pivotSelect = pivotKeys.length > 0
|
|
498
|
+
? `, ${pivotKeys
|
|
499
|
+
.map((col) => `${qPivot}.${wrapSqlName(dialect, col)} AS __pivot_${col}`)
|
|
500
|
+
.join(", ")}`
|
|
501
|
+
: "";
|
|
502
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
503
|
+
if (constraint) {
|
|
504
|
+
let q = Related.newQuery()
|
|
505
|
+
.join(pivotTable, `${pivotTable}.${relatedPivotKey}`, "=", `${Related.table}.${Related.primaryKey}`)
|
|
506
|
+
.whereIn(`${pivotTable}.${foreignPivotKey}`, chunk)
|
|
507
|
+
.select(`${Related.table}.*`)
|
|
508
|
+
.selectRaw(`${pivotTable}.${foreignPivotKey} as __bunyad_parent_id`);
|
|
509
|
+
for (const col of pivotKeys) {
|
|
510
|
+
q = q.selectRaw(`${pivotTable}.${col} as __pivot_${col}`);
|
|
511
|
+
}
|
|
512
|
+
applyConstraint(q, constraint);
|
|
513
|
+
const related = await q.get();
|
|
514
|
+
for (const row of related.all()) {
|
|
515
|
+
const rec = row;
|
|
516
|
+
const parentId = rec.__bunyad_parent_id;
|
|
517
|
+
delete rec.__bunyad_parent_id;
|
|
518
|
+
applyPivotAttributes(row, rec, pivotKeys);
|
|
519
|
+
const key = String(parentId);
|
|
520
|
+
const list = byParent.get(key) ?? [];
|
|
521
|
+
list.push(row);
|
|
522
|
+
byParent.set(key, list);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
527
|
+
const rows = await parent.getConnection().all(`SELECT ${qRelated}.*, ${qPivot}.${qForeignPivot} AS __bunyad_parent_id${pivotSelect}
|
|
528
|
+
FROM ${qRelated}
|
|
529
|
+
INNER JOIN ${qPivot}
|
|
530
|
+
ON ${qPivot}.${qRelatedPivot} = ${qRelated}.${qRelatedPk}
|
|
531
|
+
WHERE ${qPivot}.${qForeignPivot} IN (${placeholders})${soft}`, chunk);
|
|
532
|
+
for (const row of rows) {
|
|
533
|
+
const parentId = row.__bunyad_parent_id;
|
|
534
|
+
const { __bunyad_parent_id: _, ...attrs } = row;
|
|
535
|
+
const model = new Related(attrs);
|
|
536
|
+
model.exists = true;
|
|
537
|
+
applyPivotAttributes(model, attrs, pivotKeys);
|
|
538
|
+
const key = String(parentId);
|
|
539
|
+
const list = byParent.get(key) ?? [];
|
|
540
|
+
list.push(model);
|
|
541
|
+
byParent.set(key, list);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
for (const model of models) {
|
|
546
|
+
const id = model[parentKey];
|
|
547
|
+
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function eagerLoadMorphToMany(models, relation, sample, constraint) {
|
|
551
|
+
const Related = sample.getRelated();
|
|
552
|
+
const parent = models[0].constructor;
|
|
553
|
+
const parentKey = parent.primaryKey;
|
|
554
|
+
const pivotTable = sample.getPivotTable();
|
|
555
|
+
const foreignPivotKey = sample.getForeignPivotKeyName();
|
|
556
|
+
const relatedPivotKey = sample.getRelatedPivotKeyName();
|
|
557
|
+
const morphTypeColumn = sample.getMorphTypeColumn();
|
|
558
|
+
const morphTypes = sample.getMorphTypes();
|
|
559
|
+
const pivotTenantKey = sample.getPivotTenantKey();
|
|
560
|
+
const parentTenantKey = sample.getParentTenantKey();
|
|
561
|
+
const parentIds = uniqueKeys(models.map((model) => model[parentKey]));
|
|
562
|
+
const byParent = new Map();
|
|
563
|
+
if (parentIds.length > 0 && morphTypes.length > 0) {
|
|
564
|
+
const dialect = parent.getConnection().dialect;
|
|
565
|
+
const qRelated = wrapSqlName(dialect, Related.table);
|
|
566
|
+
const qPivot = wrapSqlName(dialect, pivotTable);
|
|
567
|
+
const qRelatedPk = wrapSqlName(dialect, Related.primaryKey);
|
|
568
|
+
const qForeignPivot = wrapSqlName(dialect, foreignPivotKey);
|
|
569
|
+
const qRelatedPivot = wrapSqlName(dialect, relatedPivotKey);
|
|
570
|
+
const qMorphType = wrapSqlName(dialect, morphTypeColumn);
|
|
571
|
+
const soft = softDeleteAliasSql(Related, qRelated);
|
|
572
|
+
const morphPlaceholders = morphTypes.map(() => "?").join(", ");
|
|
573
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
574
|
+
if (constraint) {
|
|
575
|
+
let q = Related.newQuery()
|
|
576
|
+
.join(pivotTable, `${pivotTable}.${relatedPivotKey}`, "=", `${Related.table}.${Related.primaryKey}`)
|
|
577
|
+
.whereIn(`${pivotTable}.${foreignPivotKey}`, chunk)
|
|
578
|
+
.whereIn(`${pivotTable}.${morphTypeColumn}`, morphTypes)
|
|
579
|
+
.select(`${Related.table}.*`)
|
|
580
|
+
.selectRaw(`${pivotTable}.${foreignPivotKey} as __bunyad_parent_id`);
|
|
581
|
+
if (pivotTenantKey && parentTenantKey) {
|
|
582
|
+
const tenantIds = uniqueKeys(models.map((model) => model[parentTenantKey]));
|
|
583
|
+
if (tenantIds.length > 0) {
|
|
584
|
+
q = q.whereIn(`${pivotTable}.${pivotTenantKey}`, tenantIds);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
applyConstraint(q, constraint);
|
|
588
|
+
const related = await q.get();
|
|
589
|
+
for (const row of related.all()) {
|
|
590
|
+
const rec = row;
|
|
591
|
+
const parentId = rec.__bunyad_parent_id;
|
|
592
|
+
delete rec.__bunyad_parent_id;
|
|
593
|
+
const key = String(parentId);
|
|
594
|
+
const list = byParent.get(key) ?? [];
|
|
595
|
+
list.push(row);
|
|
596
|
+
byParent.set(key, list);
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const idPlaceholders = chunk.map(() => "?").join(", ");
|
|
601
|
+
const params = [...chunk, ...morphTypes];
|
|
602
|
+
let tenantSql = "";
|
|
603
|
+
if (pivotTenantKey && parentTenantKey) {
|
|
604
|
+
const tenantIds = uniqueKeys(models.map((model) => model[parentTenantKey]));
|
|
605
|
+
if (tenantIds.length > 0) {
|
|
606
|
+
const qTenant = wrapSqlName(dialect, pivotTenantKey);
|
|
607
|
+
tenantSql = ` AND ${qPivot}.${qTenant} IN (${tenantIds.map(() => "?").join(", ")})`;
|
|
608
|
+
params.push(...tenantIds);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const rows = await parent.getConnection().all(`SELECT ${qRelated}.*, ${qPivot}.${qForeignPivot} AS __bunyad_parent_id
|
|
612
|
+
FROM ${qRelated}
|
|
613
|
+
INNER JOIN ${qPivot}
|
|
614
|
+
ON ${qPivot}.${qRelatedPivot} = ${qRelated}.${qRelatedPk}
|
|
615
|
+
WHERE ${qPivot}.${qForeignPivot} IN (${idPlaceholders})
|
|
616
|
+
AND ${qPivot}.${qMorphType} IN (${morphPlaceholders})${tenantSql}${soft}`, params);
|
|
617
|
+
for (const row of rows) {
|
|
618
|
+
const parentId = row.__bunyad_parent_id;
|
|
619
|
+
const { __bunyad_parent_id: _, ...attrs } = row;
|
|
620
|
+
const key = String(parentId);
|
|
621
|
+
const list = byParent.get(key) ?? [];
|
|
622
|
+
list.push(new Related(attrs));
|
|
623
|
+
byParent.set(key, list);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
for (const model of models) {
|
|
628
|
+
const id = model[parentKey];
|
|
629
|
+
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function eagerLoadMorphMany(models, relation, sample, constraint) {
|
|
633
|
+
const Related = sample.getRelated();
|
|
634
|
+
const typeColumn = sample.getTypeColumn();
|
|
635
|
+
const idColumn = sample.getIdColumn();
|
|
636
|
+
const morphType = sample.getMorphType();
|
|
637
|
+
const localKey = sample.getLocalKeyName();
|
|
638
|
+
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
639
|
+
const byParent = new Map();
|
|
640
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
641
|
+
let q = Related.where(typeColumn, morphType).whereIn(idColumn, chunk);
|
|
642
|
+
applyConstraint(q, constraint);
|
|
643
|
+
const related = await q.get();
|
|
644
|
+
for (const row of related.all()) {
|
|
645
|
+
const key = String(row[idColumn]);
|
|
646
|
+
const list = byParent.get(key) ?? [];
|
|
647
|
+
list.push(row);
|
|
648
|
+
byParent.set(key, list);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
for (const model of models) {
|
|
652
|
+
const id = model[localKey];
|
|
653
|
+
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function eagerLoadMorphOne(models, relation, sample, constraint) {
|
|
657
|
+
const Related = sample.getRelated();
|
|
658
|
+
const typeColumn = sample.getTypeColumn();
|
|
659
|
+
const idColumn = sample.getIdColumn();
|
|
660
|
+
const morphType = sample.getMorphType();
|
|
661
|
+
const localKey = sample.getLocalKeyName();
|
|
662
|
+
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
663
|
+
const byParent = new Map();
|
|
664
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
665
|
+
let q = Related.where(typeColumn, morphType).whereIn(idColumn, chunk);
|
|
666
|
+
applyConstraint(q, constraint);
|
|
667
|
+
const related = await q.get();
|
|
668
|
+
for (const row of related.all()) {
|
|
669
|
+
const key = String(row[idColumn]);
|
|
670
|
+
if (!byParent.has(key))
|
|
671
|
+
byParent.set(key, row);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
for (const model of models) {
|
|
675
|
+
const id = model[localKey];
|
|
676
|
+
model[relation] =
|
|
677
|
+
id == null ? null : (byParent.get(String(id)) ?? null);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function eagerLoadMorphTo(models, relation, sample, constraint) {
|
|
681
|
+
const typeColumn = sample.getTypeColumn();
|
|
682
|
+
const idColumn = sample.getIdColumn();
|
|
683
|
+
const ownerKey = sample.getOwnerKeyName();
|
|
684
|
+
const byType = new Map();
|
|
685
|
+
for (const model of models) {
|
|
686
|
+
const row = model;
|
|
687
|
+
const type = row[typeColumn];
|
|
688
|
+
const id = row[idColumn];
|
|
689
|
+
if (type == null || id == null)
|
|
690
|
+
continue;
|
|
691
|
+
const list = byType.get(String(type)) ?? [];
|
|
692
|
+
list.push(id);
|
|
693
|
+
byType.set(String(type), list);
|
|
694
|
+
}
|
|
695
|
+
const loaded = new Map();
|
|
696
|
+
await Promise.all([...byType.entries()].map(async ([type, ids]) => {
|
|
697
|
+
const Related = resolveMorphType(type);
|
|
698
|
+
const key = ownerKey ?? Related.primaryKey;
|
|
699
|
+
await forEachIdChunk(uniqueKeys(ids), async (chunk) => {
|
|
700
|
+
let q = Related.whereIn(key, chunk);
|
|
701
|
+
applyConstraint(q, constraint);
|
|
702
|
+
const related = await q.get();
|
|
703
|
+
for (const row of related.all()) {
|
|
704
|
+
loaded.set(`${type}:${String(row[key])}`, row);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}));
|
|
708
|
+
for (const model of models) {
|
|
709
|
+
const row = model;
|
|
710
|
+
const type = row[typeColumn];
|
|
711
|
+
const id = row[idColumn];
|
|
712
|
+
row[relation] =
|
|
713
|
+
type == null || id == null
|
|
714
|
+
? null
|
|
715
|
+
: (loaded.get(`${String(type)}:${String(id)}`) ?? null);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/** Batch `loadCount` / `loadSum` / … onto many models (one GROUP BY query per relation). */
|
|
719
|
+
export async function eagerLoadAggregates(models, specs) {
|
|
720
|
+
const list = models;
|
|
721
|
+
if (list.length === 0 || specs.length === 0)
|
|
722
|
+
return;
|
|
723
|
+
const Parent = list[0].constructor;
|
|
724
|
+
await Promise.all(specs.map((spec) => eagerLoadOneAggregate(list, Parent, spec)));
|
|
725
|
+
}
|
|
726
|
+
function aggregateDefault(fn) {
|
|
727
|
+
if (fn === "exists" || fn === "count" || fn === "sum")
|
|
728
|
+
return 0;
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
async function eagerLoadOneAggregate(models, Parent, spec) {
|
|
732
|
+
const meta = resolveRelation(Parent, spec.relation);
|
|
733
|
+
if (!meta) {
|
|
734
|
+
for (const model of models) {
|
|
735
|
+
const value = await aggregateRelation(model, spec.relation, spec.fn, spec.column);
|
|
736
|
+
model[spec.alias] = value;
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const defaults = aggregateDefault(spec.fn);
|
|
741
|
+
for (const model of models) {
|
|
742
|
+
model[spec.alias] = defaults;
|
|
743
|
+
}
|
|
744
|
+
if (meta.kind === "has") {
|
|
745
|
+
const parentIds = uniqueKeys(models.map((model) => model[meta.localKey]));
|
|
746
|
+
if (parentIds.length === 0)
|
|
747
|
+
return;
|
|
748
|
+
const dialect = meta.related.getConnection().dialect;
|
|
749
|
+
const fk = wrapSqlName(dialect, meta.foreignKey);
|
|
750
|
+
let q = meta.related.whereIn(meta.foreignKey, parentIds).selectRaw(`${fk} as __bunyad_k`);
|
|
751
|
+
if (spec.fn === "exists") {
|
|
752
|
+
q = q.selectRaw("1 as __bunyad_v");
|
|
753
|
+
}
|
|
754
|
+
else if (spec.fn === "count") {
|
|
755
|
+
q = q.selectRaw("COUNT(*) as __bunyad_v");
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
const col = wrapSqlName(dialect, spec.column);
|
|
759
|
+
q = q.selectRaw(`${spec.fn.toUpperCase()}(${col}) as __bunyad_v`);
|
|
760
|
+
}
|
|
761
|
+
const rows = await q.groupBy(meta.foreignKey).toBase().getRows();
|
|
762
|
+
const byParent = new Map();
|
|
763
|
+
for (const row of rows) {
|
|
764
|
+
byParent.set(String(row.__bunyad_k), row.__bunyad_v);
|
|
765
|
+
}
|
|
766
|
+
for (const model of models) {
|
|
767
|
+
const id = model[meta.localKey];
|
|
768
|
+
if (id == null)
|
|
769
|
+
continue;
|
|
770
|
+
const value = byParent.get(String(id));
|
|
771
|
+
if (value !== undefined) {
|
|
772
|
+
model[spec.alias] = value;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (meta.kind === "belongsTo") {
|
|
778
|
+
const fks = uniqueKeys(models.map((model) => model[meta.foreignKey]));
|
|
779
|
+
if (fks.length === 0)
|
|
780
|
+
return;
|
|
781
|
+
const dialect = meta.related.getConnection().dialect;
|
|
782
|
+
const owner = wrapSqlName(dialect, meta.ownerKey);
|
|
783
|
+
let q = meta.related.whereIn(meta.ownerKey, fks).selectRaw(`${owner} as __bunyad_k`);
|
|
784
|
+
if (spec.fn === "exists") {
|
|
785
|
+
q = q.selectRaw("1 as __bunyad_v");
|
|
786
|
+
}
|
|
787
|
+
else if (spec.fn === "count") {
|
|
788
|
+
q = q.selectRaw("COUNT(*) as __bunyad_v");
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
const col = wrapSqlName(dialect, spec.column);
|
|
792
|
+
q = q.selectRaw(`${spec.fn.toUpperCase()}(${col}) as __bunyad_v`);
|
|
793
|
+
}
|
|
794
|
+
const rows = await q.groupBy(meta.ownerKey).toBase().getRows();
|
|
795
|
+
const byOwner = new Map();
|
|
796
|
+
for (const row of rows) {
|
|
797
|
+
byOwner.set(String(row.__bunyad_k), row.__bunyad_v);
|
|
798
|
+
}
|
|
799
|
+
for (const model of models) {
|
|
800
|
+
const fk = model[meta.foreignKey];
|
|
801
|
+
if (fk == null)
|
|
802
|
+
continue;
|
|
803
|
+
const value = byOwner.get(String(fk));
|
|
804
|
+
if (value !== undefined) {
|
|
805
|
+
model[spec.alias] = value;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (meta.kind === "hasManyThrough" || meta.kind === "hasOneThrough")
|
|
811
|
+
return;
|
|
812
|
+
const parentIds = uniqueKeys(models.map((model) => model[Parent.primaryKey]));
|
|
813
|
+
if (parentIds.length === 0)
|
|
814
|
+
return;
|
|
815
|
+
const dialect = Parent.getConnection().dialect;
|
|
816
|
+
const qRelated = wrapSqlName(dialect, meta.related.table);
|
|
817
|
+
const qPivot = wrapSqlName(dialect, meta.pivotTable);
|
|
818
|
+
const qRelatedPk = wrapSqlName(dialect, meta.related.primaryKey);
|
|
819
|
+
const qForeignPivot = wrapSqlName(dialect, meta.foreignPivotKey);
|
|
820
|
+
const qRelatedPivot = wrapSqlName(dialect, meta.relatedPivotKey);
|
|
821
|
+
const soft = softDeleteAliasSql(meta.related, qRelated);
|
|
822
|
+
const byParent = new Map();
|
|
823
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
824
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
825
|
+
let selectV;
|
|
826
|
+
if (spec.fn === "exists") {
|
|
827
|
+
selectV = "1";
|
|
828
|
+
}
|
|
829
|
+
else if (spec.fn === "count") {
|
|
830
|
+
selectV = "COUNT(*)";
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
selectV = `${spec.fn.toUpperCase()}(${wrapSqlName(dialect, spec.column)})`;
|
|
834
|
+
}
|
|
835
|
+
const rows = await Parent.getConnection().all(`SELECT ${qPivot}.${qForeignPivot} AS __bunyad_k, ${selectV} AS __bunyad_v
|
|
836
|
+
FROM ${qRelated}
|
|
837
|
+
INNER JOIN ${qPivot}
|
|
838
|
+
ON ${qPivot}.${qRelatedPivot} = ${qRelated}.${qRelatedPk}
|
|
839
|
+
WHERE ${qPivot}.${qForeignPivot} IN (${placeholders})${soft}
|
|
840
|
+
GROUP BY ${qPivot}.${qForeignPivot}`, chunk);
|
|
841
|
+
for (const row of rows) {
|
|
842
|
+
byParent.set(String(row.__bunyad_k), row.__bunyad_v);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
for (const model of models) {
|
|
846
|
+
const id = model[Parent.primaryKey];
|
|
847
|
+
if (id == null)
|
|
848
|
+
continue;
|
|
849
|
+
const value = byParent.get(String(id));
|
|
850
|
+
if (value !== undefined) {
|
|
851
|
+
model[spec.alias] = value;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|