diff --git a/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts index e4c13fe58..cb5035d40 100644 --- a/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts +++ b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts @@ -301,9 +301,19 @@ export abstract class LateralJoinDialectBase extends B objArgs, ...Object.entries((payload as any).include) .filter(([, value]) => value) - .map(([field]) => ({ - [field]: eb.ref(`${parentResultName}$${field}.$data`), - })), + .map(([field, value]) => { + if (field === '_count') { + return { + [field]: this.buildCountJson( + relationModel as GetModels, + eb, + relationModelAlias, + value, + ), + }; + } + return { [field]: eb.ref(`${parentResultName}$${field}.$data`) }; + }), ); } diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 48418072c..52a0a315d 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -317,6 +317,15 @@ export class SqliteCrudDialect extends BaseCrudDialect ...Object.entries((payload as any).include) .filter(([, value]) => value) .map(([field, value]) => { + if (field === '_count') { + const subJson = this.buildCountJson( + relationModel, + eb, + tmpAlias(`${parentAlias}$${relationField}`), + value, + ); + return [sql.lit(field), subJson]; + } const subJson = this.buildRelationJSON( relationModel, eb, diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 2f52eb1fa..69d9b1c0d 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -1189,6 +1189,57 @@ describe('Client find tests ', () => { expect(result3?._count.posts).toBe(1); }); + it('supports _count nested inside an include', async () => { + // regression for https://github.com/zenstackhq/zenstack/issues/2669 + const user = await createUser(client, 'u1@test.com'); + const [post1] = await createPosts(client, user.id); + await client.comment.createMany({ + data: [ + { content: 'c1', postId: post1.id }, + { content: 'c2', postId: post1.id }, + ], + }); + + // _count nested inside a relation's `include` (not `select`) + const result = await client.user.findFirst({ + include: { + posts: { + include: { + _count: { select: { comments: true } }, + }, + }, + }, + }); + + expect(result?.posts).toHaveLength(2); + const p1 = result?.posts.find((p) => p.id === post1.id); + const p2 = result?.posts.find((p) => p.id !== post1.id); + expect(p1?._count).toEqual({ comments: 2 }); + expect(p2?._count).toEqual({ comments: 0 }); + + // `_count: true` nested inside an include + const result2 = await client.user.findFirst({ + include: { + posts: { + include: { _count: true }, + }, + }, + }); + expect(result2?.posts.find((p) => p.id === post1.id)?._count).toEqual({ comments: 2 }); + + // _count nested inside an include, with a filter on the counted relation + const result3 = await client.user.findFirst({ + include: { + posts: { + include: { + _count: { select: { comments: { where: { content: 'c1' } } } }, + }, + }, + }, + }); + expect(result3?.posts.find((p) => p.id === post1.id)?._count).toEqual({ comments: 1 }); + }); + it('rejects orderBy array elements with multiple keys', async () => { await createUser(client, 'u1@test.com'); diff --git a/tests/regression/test/issue-2669.test.ts b/tests/regression/test/issue-2669.test.ts new file mode 100644 index 000000000..49db66458 --- /dev/null +++ b/tests/regression/test/issue-2669.test.ts @@ -0,0 +1,72 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// https://github.com/zenstackhq/zenstack/issues/2669 +describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as const }])( + 'Regression for issue 2669 ($provider)', + ({ provider }) => { + const schema = ` +datasource db { + provider = '${provider}' + url = '${provider === 'sqlite' ? 'file:./dev.db' : '$DB_URL'}' +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + comments Comment[] +} + +model Comment { + id Int @id @default(autoincrement()) + content String + post Post @relation(fields: [postId], references: [id]) + postId Int +} +`; + + it('supports _count inside a nested include', async () => { + const db = await createTestClient(schema, { + provider, + }); + + await db.user.create({ + data: { + email: 'user1@test.com', + posts: { + create: { + title: 'Post1', + comments: { + create: [{ content: 'c1' }, { content: 'c2' }], + }, + }, + }, + }, + }); + + const result = await db.user.findMany({ + include: { + posts: { + include: { + _count: { + select: { comments: true }, + }, + }, + }, + }, + }); + + expect(result).toHaveLength(1); + expect(result[0]!.posts).toHaveLength(1); + expect(result[0]!.posts[0]!._count).toEqual({ comments: 2 }); + }); + }, +);