@@ -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}
128211function 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+
287412export 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