Skip to content

Commit

Permalink
feat(query-builder): allow partial loading via `qb.(left/inner)JoinAn…
Browse files Browse the repository at this point in the history
…dSelect()`

Closes #4364
  • Loading branch information
B4nan committed May 19, 2023
1 parent e3cd184 commit 22c8c84
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 7 deletions.
16 changes: 10 additions & 6 deletions packages/knex/src/query/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,13 @@ export class QueryBuilder<T extends object = AnyEntity> {
return this.join(field, alias, cond, 'leftJoin');
}

joinAndSelect(field: string, alias: string, cond: QBFilterQuery = {}, type: 'leftJoin' | 'innerJoin' | 'pivotJoin' = 'innerJoin', path?: string): SelectQueryBuilder<T> {
joinAndSelect(field: string, alias: string, cond: QBFilterQuery = {}, type: 'leftJoin' | 'innerJoin' | 'pivotJoin' = 'innerJoin', path?: string, fields?: string[]): SelectQueryBuilder<T> {
if (!this.type) {
this.select('*');
}

const prop = this.joinReference(field, alias, cond, type, path);
this.addSelect(this.getFieldsForJoinedLoad<T>(prop, alias));
this.addSelect(this.getFieldsForJoinedLoad<T>(prop, alias, fields));
const [fromAlias] = this.helper.splitField(field);
const populate = this._joinedProps.get(fromAlias);
const item = { field: prop.name, strategy: LoadStrategy.JOINED, children: [] };
Expand All @@ -230,14 +230,18 @@ export class QueryBuilder<T extends object = AnyEntity> {
return this as SelectQueryBuilder<T>;
}

leftJoinAndSelect(field: string, alias: string, cond: QBFilterQuery = {}): SelectQueryBuilder<T> {
return this.joinAndSelect(field, alias, cond, 'leftJoin');
leftJoinAndSelect(field: string, alias: string, cond: QBFilterQuery = {}, fields: string[]): SelectQueryBuilder<T> {
return this.joinAndSelect(field, alias, cond, 'leftJoin', undefined, fields);
}

protected getFieldsForJoinedLoad<U extends object>(prop: EntityProperty<U>, alias: string): Field<U>[] {
innerJoinAndSelect(field: string, alias: string, cond: QBFilterQuery = {}, fields: string[]): SelectQueryBuilder<T> {
return this.joinAndSelect(field, alias, cond, 'innerJoin', undefined, fields);
}

protected getFieldsForJoinedLoad<U extends object>(prop: EntityProperty<U>, alias: string, explicitFields?: string[]): Field<U>[] {
const fields: Field<U>[] = [];
prop.targetMeta!.props
.filter(prop => this.platform.shouldHaveColumn(prop, this._populate))
.filter(prop => explicitFields ? explicitFields.includes(prop.name) || prop.primary : this.platform.shouldHaveColumn(prop, this._populate))
.forEach(prop => fields.push(...this.driver.mapPropToFieldNames<U>(this as unknown as QueryBuilder<U>, prop, alias)));

return fields;
Expand Down
3 changes: 2 additions & 1 deletion packages/knex/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ export interface IQueryBuilder<T> {
join(field: string, alias: string, cond?: QBFilterQuery, type?: 'leftJoin' | 'innerJoin' | 'pivotJoin', path?: string): this;
leftJoin(field: string, alias: string, cond?: QBFilterQuery): this;
joinAndSelect(field: string, alias: string, cond?: QBFilterQuery): this;
leftJoinAndSelect(field: string, alias: string, cond?: QBFilterQuery): this;
leftJoinAndSelect(field: string, alias: string, cond?: QBFilterQuery, fields?: string[]): this;
innerJoinAndSelect(field: string, alias: string, cond?: QBFilterQuery, fields?: string[]): this;
withSubQuery(subQuery: Knex.QueryBuilder, alias: string): this;
where(cond: QBFilterQuery<T>, operator?: keyof typeof GroupOperator): this;
where(cond: string, params?: any[], operator?: keyof typeof GroupOperator): this;
Expand Down
17 changes: 17 additions & 0 deletions tests/features/partial-loading/partial-loading.mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ describe('partial loading (mysql)', () => {
orm.em.clear();
mock.mock.calls.length = 0;

// partial loading with query builder
const r3 = await orm.em.qb(Author2, 'a')
.select('id')
.innerJoinAndSelect('a.books', 'b', {}, ['author', 'title'])
.where({ id: god.id })
.orderBy({ 'b.title': 1 });
expect(r3).toHaveLength(1);
expect(r3[0].id).toBe(god.id);
expect(r3[0].name).toBeUndefined();
expect(r3[0].books[0].uuid).toBe(god.books[0].uuid);
expect(r3[0].books[0].title).toBe('Bible 1');
expect(r3[0].books[0].price).toBeUndefined();
expect(r3[0].books[0].author).toBeDefined();
expect(mock.mock.calls[0][0]).toMatch('select `a`.`id`, `b`.`uuid_pk` as `b__uuid_pk`, `b`.`title` as `b__title`, `b`.`author_id` as `b__author_id` from `author2` as `a` inner join `book2` as `b` on `a`.`id` = `b`.`author_id` where `a`.`id` = ? order by `b`.`title` asc');
orm.em.clear();
mock.mock.calls.length = 0;

// when populating collections, the owner is selected automatically (here book.author)
const r00 = await orm.em.find(Author2, god, { fields: ['id', 'books.title'] });
expect(r00).toHaveLength(1);
Expand Down

0 comments on commit 22c8c84

Please sign in to comment.