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 6bc1f887c..9e6cd020f 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 @@ -1,5 +1,5 @@ import { invariant } from '@zenstackhq/common-helpers'; -import { type AliasableExpression, type Expression, type ExpressionBuilder, type SelectQueryBuilder } from 'kysely'; +import { type AliasableExpression, type Expression, type ExpressionBuilder, type SelectQueryBuilder, sql } from 'kysely'; import type { FieldDef, GetModels, SchemaDef } from '../../../schema'; import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants'; import type { FindArgs } from '../../crud-types'; @@ -170,9 +170,20 @@ export abstract class LateralJoinDialectBase extends B parentResultName, ); + // Check if objArgs is empty (all select fields are false) + const hasFields = Object.keys(objArgs).length > 0; + if (relationFieldDef.array) { + if (!hasFields) { + // Return JSON-formatted empty array literal for array relations when all select fields are false + return sql`CAST('[]' AS JSON)`.as('$data'); + } return this.buildArrayAgg(this.buildJsonObject(objArgs)).as('$data'); } else { + if (!hasFields) { + // Return null for single relations when all select fields are false + return sql`NULL`.as('$data'); + } return this.buildJsonObject(objArgs).as('$data'); } }); @@ -217,6 +228,14 @@ export abstract class LateralJoinDialectBase extends B })), ); } else if (payload.select) { + // check if all select fields are false + const hasAnyTrueField = Object.values(payload.select).some((value) => !!value); + if (!hasAnyTrueField) { + // when all fields are explicitly set to false, return empty objArgs + // (filtered out in ResultProcessor.processRelation for array relations) + return objArgs; + } + // select specific fields Object.assign( objArgs, diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index fc75cac9d..ebb3dee49 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -283,6 +283,12 @@ export abstract class BaseOperationHandler { // select if (args && 'select' in args && args.select) { + // check if all fields are false - if so, return empty array + const hasAnyTrueField = Object.values(args.select).some((value) => !!value); + if (!hasAnyTrueField) { + // when all fields are explicitly set to false, return empty array + return []; + } // select is mutually exclusive with omit query = this.buildFieldSelection(model, query, args.select, model); } else { diff --git a/packages/orm/src/client/result-processor.ts b/packages/orm/src/client/result-processor.ts index fc8ae1938..1682e2d12 100644 --- a/packages/orm/src/client/result-processor.ts +++ b/packages/orm/src/client/result-processor.ts @@ -110,6 +110,17 @@ export class ResultProcessor { return value; } } + + // Filter out objects with no properties from array relations (occurs when all select fields are false) + if (Array.isArray(relationData) && fieldDef.array) { + relationData = relationData.filter((item) => { + if (item && typeof item === 'object') { + return Object.keys(item).length > 0; + } + return true; + }); + } + return this.doProcessResult(relationData, fieldDef.type as GetModels); } diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 5b6ab5639..8384e46cf 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -835,6 +835,45 @@ describe('Client find tests ', () => { }); }); + it('returns empty array when all select fields are false', async () => { + const user = await createUser(client); + await createPosts(client, user.id); + + // findMany with only false fields in select should return empty array + const r1 = await client.user.findMany({ + select: { id: false } as any, + }); + expect(r1).toEqual([]); + + // findFirst with only false fields in select should return null + const r2 = await client.user.findFirst({ + select: { id: false, email: false } as any, + }); + expect(r2).toBeNull(); + + // findUnique with only false fields in select should return null + const r3 = await client.user.findUnique({ + where: { id: user.id }, + select: { id: false } as any, + }); + expect(r3).toBeNull(); + + // nested query with only false fields in select should return empty array for relations + const r4 = await client.user.findUnique({ + where: { id: user.id }, + select: { + id: true, + email: true, + posts: { select: { id: false } as any }, + }, + }); + expect(r4).toMatchObject({ + id: user.id, + email: user.email, + posts: [], + }); + }); + it('allows field omission', async () => { const user = await createUser(client); await createPosts(client, user.id);