Skip to content

Commit 0a07c1a

Browse files
authored
Merge pull request #11 from darkbasic/mandatory-populate
Compute mandatory populate and filter collections when reassigning results
2 parents 0f369f5 + e69b274 commit 0a07c1a

6 files changed

+160
-5
lines changed

.changeset/afraid-cars-attend.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mikro-orm-find-dataloader": minor
3+
---
4+
5+
perf: run mandatory populate logic once per querymap

.changeset/curvy-geese-pay.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mikro-orm-find-dataloader": patch
3+
---
4+
5+
Fix vscode test runner

.changeset/fluffy-ducks-learn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mikro-orm-find-dataloader": minor
3+
---
4+
5+
fix: filter collections when reassigning results

.changeset/twelve-lions-vanish.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mikro-orm-find-dataloader": minor
3+
---
4+
5+
fix: compute mandatory populates even if not efficiently

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@
4747
"ts-node": "^10.9.1",
4848
"typescript": "^5.3.2"
4949
},
50+
"jest": {
51+
"projects": [
52+
"<rootDir>/packages/*"
53+
]
54+
},
5055
"lint-staged": {
5156
"*.{js,jsx}": [
5257
"prettier --write",

packages/find/src/findDataloader.ts

+135-5
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,90 @@ export function groupInversedOrMappedKeysByEntity<T extends AnyEntity<T>>(
124124
return entitiesMap;
125125
}
126126

127+
function allKeysArePK<K extends object>(
128+
keys: Array<EntityKey<K>> | undefined,
129+
primaryKeys: Array<EntityKey<K>>,
130+
): boolean {
131+
if (keys == null) {
132+
return false;
133+
}
134+
if (keys.length !== primaryKeys.length) {
135+
return false;
136+
}
137+
for (const key of keys) {
138+
if (!primaryKeys.includes(key)) {
139+
return false;
140+
}
141+
}
142+
return true;
143+
}
144+
145+
// {id: 5, name: "a"} returns false because contains additional fields
146+
// Returns true for all PK formats including {id: 1} or {owner: 1, recipient: 2}
147+
function isPK<K extends object>(filter: FilterQueryDataloader<K>, meta: EntityMetadata<K>): boolean {
148+
if (meta == null) {
149+
return false;
150+
}
151+
if (meta.compositePK) {
152+
// COMPOSITE
153+
if (Array.isArray(filter)) {
154+
// PK or PK[] or object[]
155+
// [1, 2]
156+
// [[1, 2], [3, 4]]
157+
// [{owner: 1, recipient: 2}, {owner: 3, recipient: 4}]
158+
// [{owner: 1, recipient: 2, sex: 0}, {owner: 3, recipient: 4, sex: 1}]
159+
if (Utils.isPrimaryKey(filter, meta.compositePK)) {
160+
// PK
161+
return true;
162+
}
163+
if (Utils.isPrimaryKey(filter[0], meta.compositePK)) {
164+
// PK[]
165+
return true;
166+
}
167+
const keys = typeof filter[0] === "object" ? (Object.keys(filter[0]) as Array<EntityKey<K>>) : undefined;
168+
if (allKeysArePK(keys, meta.primaryKeys)) {
169+
// object is PK or PK[]
170+
return true;
171+
}
172+
} else {
173+
// object
174+
// {owner: 1, recipient: 2, sex: 0}
175+
const keys = typeof filter === "object" ? (Object.keys(filter) as Array<EntityKey<K>>) : undefined;
176+
if (allKeysArePK(keys, meta.primaryKeys)) {
177+
// object is PK
178+
return true;
179+
}
180+
}
181+
} else {
182+
// NOT COMPOSITE
183+
if (Array.isArray(filter)) {
184+
// PK[]
185+
// [1, 2]
186+
// [{id: 1}, {id: 2}] NOT POSSIBLE FOR NON COMPOSITE
187+
if (Utils.isPrimaryKey(filter[0])) {
188+
return true;
189+
}
190+
} else {
191+
// PK or object
192+
// 1
193+
// {id: [1, 2], sex: 0} or {id: 1, sex: 0}
194+
if (Utils.isPrimaryKey(filter)) {
195+
// PK
196+
return true;
197+
}
198+
const keys =
199+
typeof filter === "object" ? (Object.keys(filter) as [EntityKey<K>, ...Array<EntityKey<K>>]) : undefined;
200+
if (keys?.length === 1 && keys[0] === meta.primaryKeys[0]) {
201+
// object is PK
202+
return true;
203+
}
204+
}
205+
}
206+
return false;
207+
}
208+
127209
// Call this fn only if keyProp.targetMeta != null otherwise you will get false positives
210+
// Returns only PKs in short-hand format like 1 or [1, 1] not {id: 1} or {owner: 1, recipient: 2}
128211
function getPKs<K extends object>(
129212
filter: FilterQueryDataloader<K>,
130213
meta: EntityMetadata<K>,
@@ -259,7 +342,7 @@ function updateQueryFilter<K extends object, P extends string = never>(
259342
newQueryMap?: boolean,
260343
): void {
261344
if (options?.populate != null && accOptions != null && accOptions.populate !== true) {
262-
if (Array.isArray(options.populate) && options.populate[0] === "*") {
345+
if (Array.isArray(options.populate) && options.populate.includes("*")) {
263346
accOptions.populate = true;
264347
} else if (Array.isArray(options.populate)) {
265348
if (accOptions.populate == null) {
@@ -276,14 +359,56 @@ function updateQueryFilter<K extends object, P extends string = never>(
276359
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
277360
const curValue = (cur as Record<string, any[]>)[key]!;
278361
if (Array.isArray(value)) {
279-
value.push(...curValue.reduce<any[]>((acc, cur) => acc.concat(cur), []));
362+
// value.push(...curValue.reduce<any[]>((acc, cur) => acc.concat(cur), []));
363+
value.push(...structuredClone(curValue));
280364
} else {
281365
updateQueryFilter([value], curValue);
282366
}
283367
}
284368
}
285369
}
286370

371+
// The least amount of populate necessary to map the dataloader results to their original queries
372+
function getMandatoryPopulate<K extends object>(
373+
cur: FilterQueryDataloader<K>,
374+
meta: EntityMetadata<K>,
375+
): string | undefined;
376+
function getMandatoryPopulate<K extends object>(
377+
cur: FilterQueryDataloader<K>,
378+
meta: EntityMetadata<K>,
379+
options: { populate?: Set<any> },
380+
): void;
381+
function getMandatoryPopulate<K extends object>(
382+
cur: FilterQueryDataloader<K>,
383+
meta: EntityMetadata<K>,
384+
options?: { populate?: Set<any> },
385+
): any {
386+
for (const [key, value] of Object.entries(cur)) {
387+
const keyProp = meta.properties[key as EntityKey<K>];
388+
if (keyProp == null) {
389+
throw new Error(`Cannot find properties for ${key}`);
390+
}
391+
// If our current key leads to scalar we don't need to populate anything
392+
if (keyProp.targetMeta != null) {
393+
// Our current key points to either a Reference or a Collection
394+
// We need to populate all Collections
395+
// We also need to populate References whenever we have to further match non-PKs properties
396+
if (keyProp.ref !== true || !isPK(value, keyProp.targetMeta)) {
397+
const furtherPop = getMandatoryPopulate(value, keyProp.targetMeta);
398+
const computedPopulate = furtherPop == null ? `${key}` : `${key}.${furtherPop}`;
399+
if (options != null) {
400+
if (options.populate == null) {
401+
options.populate = new Set();
402+
}
403+
options.populate.add(computedPopulate);
404+
} else {
405+
return computedPopulate;
406+
}
407+
}
408+
}
409+
}
410+
}
411+
287412
export interface DataloaderFind<K extends object, Hint extends string = never, Fields extends string = never> {
288413
entityName: string;
289414
meta: EntityMetadata<K>;
@@ -305,7 +430,9 @@ export function groupFindQueriesByOpts(
305430
dataloaderFind.filtersAndKeys?.push({ key, newFilter });
306431
let queryMap = queriesMap.get(key);
307432
if (queryMap == null) {
308-
queryMap = [structuredClone(newFilter), {}];
433+
const queryMapOpts = {};
434+
queryMap = [structuredClone(newFilter), queryMapOpts];
435+
getMandatoryPopulate(newFilter, meta, queryMapOpts);
309436
updateQueryFilter(queryMap, newFilter, options, true);
310437
queriesMap.set(key, queryMap);
311438
} else {
@@ -348,6 +475,7 @@ export function getFindBatchLoadFn<Entity extends object>(
348475
for (const [key, value] of Object.entries(filter)) {
349476
const entityValue = entity[key as keyof K];
350477
if (Array.isArray(value)) {
478+
// Our current filter is an array
351479
if (Array.isArray(entityValue)) {
352480
// Collection
353481
if (!value.every((el) => entityValue.includes(el))) {
@@ -360,8 +488,10 @@ export function getFindBatchLoadFn<Entity extends object>(
360488
}
361489
}
362490
} else {
363-
// Object: recursion
364-
if (!filterResult(entityValue as object, value)) {
491+
// Our current filter is an object
492+
if (entityValue instanceof Collection) {
493+
entityValue.find((entity) => filterResult(entity, value));
494+
} else if (!filterResult(entityValue as object, value)) {
365495
return false;
366496
}
367497
}

0 commit comments

Comments
 (0)