Skip to content
This repository was archived by the owner on Feb 10, 2026. It is now read-only.
Draft
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
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -170,9 +170,20 @@ export abstract class LateralJoinDialectBase<Schema extends SchemaDef> 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');
}
});
Expand Down Expand Up @@ -217,6 +228,14 @@ export abstract class LateralJoinDialectBase<Schema extends SchemaDef> 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,
Expand Down
6 changes: 6 additions & 0 deletions packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {

// 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 {
Expand Down
11 changes: 11 additions & 0 deletions packages/orm/src/client/result-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ export class ResultProcessor<Schema extends SchemaDef> {
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<Schema>);
}

Expand Down
39 changes: 39 additions & 0 deletions tests/e2e/orm/client-api/find.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot
Add the case that this false select is in the nested query

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test case for nested queries with only false fields in commit 3adea0a. The fix now handles both top-level and nested relation queries properly - empty objects are filtered out in the result processor for array relations.


it('allows field omission', async () => {
const user = await createUser(client);
await createPosts(client, user.id);
Expand Down