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
6 changes: 4 additions & 2 deletions lib/queryBuilder/operations/FirstOperation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { QueryBuilderOperation } from './QueryBuilderOperation.js';
import { LimitPerParentOperation } from './LimitPerParentOperation.js';

export class FirstOperation extends QueryBuilderOperation {
shouldLimit(builder) {
Expand All @@ -13,8 +14,9 @@ export class FirstOperation extends QueryBuilderOperation {
return knexBuilder;
}

onAfter3(_, result) {
if (Array.isArray(result)) {
onAfter3(builder, result) {
// Limited per parent, the query still returns the models of all parents:
if (Array.isArray(result) && !builder.has(LimitPerParentOperation)) {
return result[0];
} else {
return result;
Expand Down
7 changes: 5 additions & 2 deletions lib/queryBuilder/operations/LimitPerParentOperation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { QueryBuilderOperation } from './QueryBuilderOperation.js';
import { RangeOperation } from './RangeOperation.js';
import { FirstOperation } from './FirstOperation.js';
import { isNumber } from '../../utils/objectUtils.js';
import { withoutSchema } from './select/Selection.js';
import { getWindowOrder, rowNumberColumn } from './windowOrder.js';
Expand All @@ -22,9 +23,11 @@ export class LimitPerParentOperation extends QueryBuilderOperation {
// `page()` and `range()` keep limiting all parents, as their total does.
if (!builder.has(RangeOperation)) {
const { limit = null, offset = null } = knexBuilder._single;
// `first()` only limits the query with `useLimitInFirst`:
const isFirst = builder.has(FirstOperation);

if (limit !== null || offset !== null) {
return { limit, offset: offset ?? 0 };
if (limit !== null || offset !== null || isFirst) {
return { limit: isFirst ? 1 : limit, offset: offset ?? 0 };
}
}

Expand Down
7 changes: 0 additions & 7 deletions lib/queryBuilder/operations/eager/WhereInEagerOperation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as promiseUtils from '../../../utils/promiseUtils/index.js';

import { EagerOperation } from './EagerOperation.js';
import { LimitPerParentOperation } from '../LimitPerParentOperation.js';
import { FirstOperation } from '../FirstOperation.js';
import { isMsSql, isOracle, isSqlite } from '../../../utils/knexUtils.js';
import { isObject, asArray, flatten, chunk } from '../../../utils/objectUtils.js';
import { ValidationErrorType } from '../../../model/ValidationError.js';
Expand Down Expand Up @@ -133,12 +132,6 @@ export class WhereInEagerOperation extends EagerOperation {
}

if (this.graphOptions.limitPerParent !== false) {
// `first()` limits the related models of each owner to one, and the query
// still returns the batch of all owners:
if (queryBuilder.has(FirstOperation)) {
queryBuilder.clear(FirstOperation).limit(1);
}

queryBuilder.addOperation(
new LimitPerParentOperation('limitPerParent', { findOperation }),
[],
Expand Down
39 changes: 39 additions & 0 deletions tests/integration/withGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,45 @@ export default (session) => {
]);
});

it('should apply first() of a relation mapping modify per parent', async () => {
class Model1Latest extends Model1 {
static get relationMappings() {
return {
...super.relationMappings,
latestModel2: {
relation: Model1.HasOneRelation,
modelClass: Model2,
join: { from: 'Model1.id', to: 'model2.model1_id' },
modify: (query) => query.orderBy('id_col', 'desc').first(),
},
};
}
}

const models = await Model1Latest.query()
.withGraphFetched('latestModel2')
.whereExists(Model1Latest.relatedQuery('model1Relation2'))
.orderBy('id');

expect(relatedIds(models, 'latestModel2')).toEqual([
[1, 2],
[4, 4],
[6, 3],
]);
});

it('should apply first() of an onBuild() hook per parent', async () => {
const models = await fetchModel1s('model1Relation2', (query) =>
query.orderBy('id_col', 'desc').onBuild((query) => query.first()),
);

expect(relatedIds(models, 'model1Relation2')).toEqual([
[1, [2]],
[4, [4]],
[6, [3]],
]);
});

it('should support limitPerParent: false in defaultGraphOptions', async () => {
class Model1Defaults extends Model1 {
static get defaultGraphOptions() {
Expand Down
Loading