Skip to content

Commit 4e35bc1

Browse files
authored
fix(orm): resolve delegate-inherited fields in cursor pagination (#2591)
1 parent 09b96e4 commit 4e35bc1

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

packages/orm/src/client/crud/dialects/base-dialect.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,17 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
252252
const [field, order] = orderByItems[j]!;
253253
const _order = negateOrderBy ? (order === 'asc' ? 'desc' : 'asc') : order;
254254
const op = j === i ? (_order === 'asc' ? '>=' : '<=') : '=';
255+
// Fields inherited from a delegate base live on the base table, which is
256+
// joined with its model name as alias. See buildSelectModel/buildDelegateJoin.
257+
const fieldDef = requireField(this.schema, model, field);
258+
const outerAlias = fieldDef.originModel ?? modelAlias;
259+
const subSelectAlias = fieldDef.originModel ?? subQueryAlias;
255260
andFilters.push(
256261
this.eb(
257-
this.eb.ref(`${modelAlias}.${field}`),
262+
this.eb.ref(`${outerAlias}.${field}`),
258263
op,
259264
this.buildSelectModel(model, subQueryAlias)
260-
.select(`${subQueryAlias}.${field}`)
265+
.select(`${subSelectAlias}.${field}`)
261266
.where(cursorFilter),
262267
),
263268
);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
// https://github.com/zenstackhq/zenstack/issues/2588
5+
describe('Regression for issue 2588', () => {
6+
const schema = `
7+
model Asset {
8+
id String @id @default(uuid())
9+
createdAt DateTime @default(now())
10+
assetType String
11+
@@delegate(assetType)
12+
}
13+
14+
model Notification extends Asset {
15+
title String
16+
}
17+
`;
18+
19+
async function setup() {
20+
const db = await createTestClient(schema);
21+
const a = await db.notification.create({
22+
data: { title: 'A', createdAt: new Date('2025-01-01T00:00:00Z') },
23+
});
24+
const b = await db.notification.create({
25+
data: { title: 'B', createdAt: new Date('2025-01-02T00:00:00Z') },
26+
});
27+
const c = await db.notification.create({
28+
data: { title: 'C', createdAt: new Date('2025-01-03T00:00:00Z') },
29+
});
30+
return { db, a, b, c };
31+
}
32+
33+
it('cursor + orderBy on delegate parent field does not error', async () => {
34+
const { db, b } = await setup();
35+
36+
const result = await db.notification.findMany({
37+
cursor: { id: b.id },
38+
orderBy: { createdAt: 'asc' },
39+
});
40+
41+
expect(result.map((n: any) => n.title)).toEqual(['B', 'C']);
42+
});
43+
44+
it('cursor + skip + orderBy on delegate parent field works', async () => {
45+
const { db, a } = await setup();
46+
47+
const result = await db.notification.findMany({
48+
cursor: { id: a.id },
49+
skip: 1,
50+
orderBy: { createdAt: 'asc' },
51+
take: 25,
52+
});
53+
54+
expect(result.map((n: any) => n.title)).toEqual(['B', 'C']);
55+
});
56+
57+
it('cursor + multiple orderBy mixing child and delegate fields', async () => {
58+
const { db, a } = await setup();
59+
60+
const result = await db.notification.findMany({
61+
cursor: { id: a.id },
62+
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
63+
});
64+
65+
expect(result.map((n: any) => n.title)).toEqual(['A', 'B', 'C']);
66+
});
67+
68+
it('cursor + orderBy on child field still works', async () => {
69+
const { db, b } = await setup();
70+
71+
const result = await db.notification.findMany({
72+
cursor: { id: b.id },
73+
orderBy: { title: 'asc' },
74+
});
75+
76+
expect(result.map((n: any) => n.title)).toEqual(['B', 'C']);
77+
});
78+
});

0 commit comments

Comments
 (0)