Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@athenna/database",
"version": "5.48.0",
"version": "5.49.0",
"description": "The Athenna database handler for SQL/NoSQL.",
"license": "MIT",
"author": "João Lenon <lenon@athenna.io>",
Expand Down
2 changes: 1 addition & 1 deletion src/models/builders/ModelQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ export class ModelQueryBuilder<
>
) => any
) {
const options = this.schema.includeRelation(relation, closure)
const options = this.schema.includeWhereHasRelation(relation, closure)

super.whereExists(query => {
switch (options.type) {
Expand Down
13 changes: 8 additions & 5 deletions src/models/schemas/ModelSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,11 +318,13 @@ export class ModelSchema<M extends BaseModel = any> extends Macroable {
}

/**
* Return the relation options only from relations
* that are included.
* Relation options used only when eager-loading related rows (`with()` /
* {@link ModelSchema.includeRelation includeRelation}). Constraints from
* `whereHas()` are not included here; use `with()` when the response must
* contain related models.
*/
public getIncludedRelations(): RelationOptions[] {
return this.relations.filter(r => r.isIncluded || r.isWhereHasIncluded)
return this.relations.filter(r => r.isIncluded)
}

/**
Expand Down Expand Up @@ -370,8 +372,9 @@ export class ModelSchema<M extends BaseModel = any> extends Macroable {
}

/**
* Include a relation by setting the isWhereHasIncluded
* option to true.
* Marks relation metadata for a `whereHas()` constraint (stores closure).
* Does not eager-load related rows; only {@link ModelSchema.includeRelation}
* participates in {@link ModelSchema.getIncludedRelations eager loading}.
*/
public includeWhereHasRelation(
property: string | ModelRelations<M>,
Expand Down
6 changes: 2 additions & 4 deletions src/types/relations/BelongsToManyOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,8 @@ export type BelongsToManyOptions<
isIncluded?: boolean

/**
* Set if the model will be included when fetching
* data.
* If this option is true, you don't need to call
* methods like `whereHas()` to eager load your relation.
* Internal flag when `whereHas()` applies a constraint on this relation.
* Does not eager-load; use {@link isIncluded} / `with()` to load related rows.
*
* @default false
*/
Expand Down
6 changes: 2 additions & 4 deletions src/types/relations/BelongsToOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,8 @@ export type BelongsToOptions<
isIncluded?: boolean

/**
* Set if the model will be included when fetching
* data.
* If this option is true, you don't need to call
* methods like `whereHas()` to eager load your relation.
* Internal flag when `whereHas()` applies a constraint on this relation.
* Does not eager-load; use {@link isIncluded} / `with()` to load related rows.
*
* @default false
*/
Expand Down
6 changes: 2 additions & 4 deletions src/types/relations/HasManyOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,8 @@ export type HasManyOptions<
isIncluded?: boolean

/**
* Set if the model will be included when fetching
* data.
* If this option is true, you don't need to call
* methods like `whereHas()` to eager load your relation.
* Internal flag when `whereHas()` applies a constraint on this relation.
* Does not eager-load; use {@link isIncluded} / `with()` to load related rows.
*
* @default false
*/
Expand Down
6 changes: 2 additions & 4 deletions src/types/relations/HasOneOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,8 @@ export type HasOneOptions<
isIncluded?: boolean

/**
* Set if the model will be included when fetching
* data.
* If this option is true, you don't need to call
* methods like `whereHas()` to eager load your relation.
* Internal flag when `whereHas()` applies a constraint on this relation.
* Does not eager-load; use {@link isIncluded} / `with()` to load related rows.
*
* @default false
*/
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/models/Product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ export class Product extends BaseModel {

@Column()
public id: string

/** FK for User `HasMany` products (defaults to userId). */
@Column()
public userId: string
}
58 changes: 58 additions & 0 deletions tests/unit/models/builders/ModelQueryBuilderTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2307,4 +2307,62 @@ export default class ModelQueryBuilderTest {

assert.calledOnceWith(Database.driver.limit, limitValue)
}

@Test()
public async whereHasShouldNotEagerLoadRelations({ assert }: Context) {
let findManyCalls = 0

Mock.stub(Database.driver, 'findMany').callsFake(async () => {
findManyCalls++

return [{ id: '1', name: 'John Doe' }]
})

await User.query()
.whereHas('products', qb => qb.where('id', 'p1'))
.findMany()

assert.equal(findManyCalls, 1)
}

@Test()
public async withShouldEagerLoadRelations({ assert }: Context) {
let findManyCalls = 0

Mock.stub(Database.driver, 'findMany').callsFake(async () => {
findManyCalls++

if (findManyCalls === 1) {
return [{ id: '1', name: 'John Doe' }]
}

return [{ id: 'p1', userId: '1' }]
})

await User.query().with('products').findMany()

assert.equal(findManyCalls, 2)
}

@Test()
public async whereHasWithSameRelationShouldStillEagerLoad({ assert }: Context) {
let findManyCalls = 0

Mock.stub(Database.driver, 'findMany').callsFake(async () => {
findManyCalls++

if (findManyCalls === 1) {
return [{ id: '1', name: 'John Doe' }]
}

return [{ id: 'p1', userId: '1' }]
})

await User.query()
.whereHas('products', qb => qb.where('id', 'p1'))
.with('products')
.findMany()

assert.equal(findManyCalls, 2)
}
}
Loading