Skip to content

Commit

Permalink
feat(sql): rework joined strategy to support the default `populateWhe…
Browse files Browse the repository at this point in the history
…re: 'all'` (#4957)

The joined strategy now supports `populateWhere: 'all'`, which is the
default behavior, and means "populate the full relations regardless of
the where condition". This was previouly not working with the joined
strategy, as it was reusing the same join clauses as the where clause.
With this PR, the joined strategy will use a separate join branch for
the populated relations.

**This aligns the behavior between the strategies.**

The `order by` clause is shared for both the join branches and a new
`populateOrderBy` option is added to allow control of the order of
populated relations separately.
  • Loading branch information
B4nan committed Nov 24, 2023
1 parent 49206a6 commit e5dbc24
Show file tree
Hide file tree
Showing 25 changed files with 751 additions and 299 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

const meta = this.metadata.get<Entity>(entityName);
options = { ...options };
// save the original hint value so we know it was infer/all
(options as Dictionary)._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
options.populateWhere = await this.applyJoinedFilters(meta, { ...where } as ObjectQuery<Entity>, options);
const results = await em.driver.find<Entity, Hint, Fields>(entityName, where, { ctx: em.transactionContext, ...options });

Expand Down Expand Up @@ -658,6 +660,8 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
}

options = { ...options };
// save the original hint value so we know it was infer/all
(options as Dictionary)._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
options.populateWhere = await this.applyJoinedFilters(meta, { ...where } as ObjectQuery<Entity>, options);
const data = await em.driver.findOne<Entity, Hint, Fields>(entityName, where, {
ctx: em.transactionContext,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/drivers/IDatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export interface FindOptions<T, P extends string = never, F extends string = nev
where?: FilterQuery<T>;
populate?: Populate<T, P>;
populateWhere?: ObjectQuery<T> | PopulateHint | `${PopulateHint}`;
populateOrderBy?: OrderDefinition<T>;
fields?: readonly AutoPath<T, F, '*'>[];
orderBy?: OrderDefinition<T>;
cache?: boolean | number | [string, number];
Expand Down
24 changes: 20 additions & 4 deletions packages/core/src/entity/EntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class EntityLoader {
return [];
}

const filtered = this.filterCollections<Entity>(entities, field, options);
const filtered = this.filterCollections<Entity>(entities, field, options, ref);
const innerOrderBy = Utils.asArray(options.orderBy)
.filter(orderBy => (Array.isArray(orderBy[prop.name]) && (orderBy[prop.name] as unknown[]).length > 0) || Utils.isObject(orderBy[prop.name]))
.flatMap(orderBy => orderBy[prop.name]);
Expand Down Expand Up @@ -316,7 +316,23 @@ export class EntityLoader {
const ids = Utils.unique(children.map(e => e.__helper.getPrimaryKey()));
const where = this.mergePrimaryCondition<Entity>(ids, fk as FilterKey<Entity>, options, meta, this.metadata, this.driver.getPlatform());
const fields = this.buildFields(options.fields, prop, ref) as any;
const { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere, connectionType, logging } = options;

/* eslint-disable prefer-const */
let {
refresh,
filters,
convertCustomTypes,
lockMode,
strategy,
populateWhere,
connectionType,
logging,
} = options;
/* eslint-enable prefer-const */

if (typeof populateWhere === 'object') {
populateWhere = await this.extractChildCondition({ where: populateWhere } as any, prop);
}

const items = await this.em.find(prop.type, where, {
refresh, filters, convertCustomTypes, lockMode, populateWhere, logging,
Expand Down Expand Up @@ -545,12 +561,12 @@ export class EntityLoader {
return children;
}

private filterCollections<Entity extends object>(entities: Entity[], field: keyof Entity, options: Required<EntityLoaderOptions<Entity>>): Entity[] {
private filterCollections<Entity extends object>(entities: Entity[], field: keyof Entity, options: Required<EntityLoaderOptions<Entity>>, ref?: string): Entity[] {
if (options.refresh) {
return entities.filter(e => e[field]);
}

return entities.filter(e => Utils.isCollection(e[field]) && !(e[field] as unknown as Collection<AnyEntity>).isInitialized(true));
return entities.filter(e => Utils.isCollection(e[field]) && !(e[field] as unknown as Collection<AnyEntity>).isInitialized(!ref));
}

private filterReferences<Entity extends object>(entities: Entity[], field: keyof Entity & string, options: Required<EntityLoaderOptions<Entity>>): Entity[keyof Entity][] {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/Cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class Cursor<Entity extends object, Hint extends string = never, Fields e
return Utils.asArray(orderBy).flatMap(order => {
return Utils.keys(order)
.map(key => meta.properties[key as EntityKey<Entity>])
.filter(prop => [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind) || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))
.filter(prop => prop && ([ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind) || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner)))
.map(prop => [prop.name, order[prop.name] as QueryOrder] as const);
});
}
Expand Down
Loading

0 comments on commit e5dbc24

Please sign in to comment.