From 2b2aef240b25fead4ff46be3227eaada3184b824 Mon Sep 17 00:00:00 2001 From: Emil Redzik Date: Mon, 30 Mar 2026 11:11:17 +0200 Subject: [PATCH 1/4] fix(orm): enhance OR filter to ignore undefined branches and improve argument normalization --- .../src/client/crud/dialects/base-dialect.ts | 13 ++++++-- .../orm/src/client/crud/operations/base.ts | 16 ++++++---- tests/e2e/orm/client-api/filter.test.ts | 30 +++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 5fae51ef5..a698e53d1 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -264,9 +264,16 @@ export abstract class BaseCrudDialect { .with('AND', () => this.and(...enumerate(payload).map((subPayload) => this.buildFilter(model, modelAlias, subPayload))), ) - .with('OR', () => - this.or(...enumerate(payload).map((subPayload) => this.buildFilter(model, modelAlias, subPayload))), - ) + .with('OR', () => { + const allBranches = enumerate(payload).map((subPayload) => + this.buildFilter(model, modelAlias, subPayload) + ); + const meaningfulBranches = allBranches.filter((expr) => !this.isTrue(expr)); + if (meaningfulBranches.length === 0) { + return allBranches.length > 0 ? this.true() : this.false(); + } + return this.or(...meaningfulBranches); + }) .with('NOT', () => this.eb.not(this.buildCompositeFilter(model, modelAlias, 'AND', payload))) .exhaustive(); } diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 46306c034..30630893a 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -2531,11 +2531,17 @@ export abstract class BaseOperationHandler { private doNormalizeArgs(args: unknown) { if (args && typeof args === 'object') { - for (const [key, value] of Object.entries(args)) { - if (value === undefined) { - delete args[key as keyof typeof args]; - } else if (value && isPlainObject(value)) { - this.doNormalizeArgs(value); + if (Array.isArray(args)) { + for (const element of args) { + this.doNormalizeArgs(element); + } + } else { + for (const [key, value] of Object.entries(args)) { + if (value === undefined) { + delete args[key as keyof typeof args]; + } else if (value && isPlainObject(value)) { + this.doNormalizeArgs(value); + } } } } diff --git a/tests/e2e/orm/client-api/filter.test.ts b/tests/e2e/orm/client-api/filter.test.ts index 36173ca2d..d3ca14cd9 100644 --- a/tests/e2e/orm/client-api/filter.test.ts +++ b/tests/e2e/orm/client-api/filter.test.ts @@ -801,5 +801,35 @@ describe('Client filter tests ', () => { await expect(client.user.findMany({ where: { id: undefined } })).toResolveWithLength(1); }); + it('ignores undefined branch inside OR filter', async () => { + await createUser('u1@test.com', { + name: 'First', + role: 'ADMIN', + profile: { create: { id: 'p1', bio: 'bio1' } }, + }); + const user2 = await createUser('u2@test.com', { + name: 'Second', + role: 'USER', + profile: { create: { id: 'p2', bio: 'bio2' } }, + }); + + const baseline = await client.user.findFirst({ + where: { + OR: [{ id: user2.id }], + } as any, + orderBy: { createdAt: 'asc' }, + }); + + const withUndefinedBranch = await client.user.findFirst({ + where: { + OR: [{ id: undefined }, { id: user2.id }], + } as any, + orderBy: { createdAt: 'asc' }, + }); + + expect(baseline?.email).toBe(user2.email); + expect(withUndefinedBranch?.email).toBe(baseline?.email); + }); + // TODO: filter for bigint, decimal, bytes }); From 26a18eaaa731c9c1c9c375a659c4d4fd9a4af143 Mon Sep 17 00:00:00 2001 From: Emil Redzik Date: Mon, 30 Mar 2026 11:57:07 +0200 Subject: [PATCH 2/4] fix(orm): streamline OR filter logic to ignore undefined branches and enhance test coverage --- .../orm/src/client/crud/dialects/base-dialect.ts | 12 ++++-------- tests/e2e/orm/client-api/filter.test.ts | 11 +++++++++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index a698e53d1..f3a4c92d1 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -265,14 +265,10 @@ export abstract class BaseCrudDialect { this.and(...enumerate(payload).map((subPayload) => this.buildFilter(model, modelAlias, subPayload))), ) .with('OR', () => { - const allBranches = enumerate(payload).map((subPayload) => - this.buildFilter(model, modelAlias, subPayload) - ); - const meaningfulBranches = allBranches.filter((expr) => !this.isTrue(expr)); - if (meaningfulBranches.length === 0) { - return allBranches.length > 0 ? this.true() : this.false(); - } - return this.or(...meaningfulBranches); + const branches = enumerate(payload) + .map((subPayload) => this.buildFilter(model, modelAlias, subPayload)) + .filter((expr) => !this.isTrue(expr)); + return this.or(...branches); }) .with('NOT', () => this.eb.not(this.buildCompositeFilter(model, modelAlias, 'AND', payload))) .exhaustive(); diff --git a/tests/e2e/orm/client-api/filter.test.ts b/tests/e2e/orm/client-api/filter.test.ts index d3ca14cd9..2c4129d00 100644 --- a/tests/e2e/orm/client-api/filter.test.ts +++ b/tests/e2e/orm/client-api/filter.test.ts @@ -801,7 +801,7 @@ describe('Client filter tests ', () => { await expect(client.user.findMany({ where: { id: undefined } })).toResolveWithLength(1); }); - it('ignores undefined branch inside OR filter', async () => { + it('ignores undefined branch inside OR filters', async () => { await createUser('u1@test.com', { name: 'First', role: 'ADMIN', @@ -827,9 +827,16 @@ describe('Client filter tests ', () => { orderBy: { createdAt: 'asc' }, }); + const onlyUndefinedBranch = await client.user.findFirst({ + where: { + OR: [{ id: undefined }], + } as any, + orderBy: { createdAt: 'asc' }, + }); + expect(baseline?.email).toBe(user2.email); expect(withUndefinedBranch?.email).toBe(baseline?.email); + expect(onlyUndefinedBranch).toBeNull(); }); - // TODO: filter for bigint, decimal, bytes }); From fab090139a71f39c52b79d596d717139a4c4d942 Mon Sep 17 00:00:00 2001 From: Emil Redzik Date: Mon, 30 Mar 2026 12:14:59 +0200 Subject: [PATCH 3/4] fix: coderabbit issue with nested filters and add test for that --- packages/orm/src/client/crud/operations/base.ts | 2 +- tests/e2e/orm/client-api/filter.test.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 30630893a..ce726878a 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -2539,7 +2539,7 @@ export abstract class BaseOperationHandler { for (const [key, value] of Object.entries(args)) { if (value === undefined) { delete args[key as keyof typeof args]; - } else if (value && isPlainObject(value)) { + } else if (value && (isPlainObject(value) || Array.isArray(value))) { this.doNormalizeArgs(value); } } diff --git a/tests/e2e/orm/client-api/filter.test.ts b/tests/e2e/orm/client-api/filter.test.ts index 2c4129d00..ff9574a41 100644 --- a/tests/e2e/orm/client-api/filter.test.ts +++ b/tests/e2e/orm/client-api/filter.test.ts @@ -838,5 +838,20 @@ describe('Client filter tests ', () => { expect(withUndefinedBranch?.email).toBe(baseline?.email); expect(onlyUndefinedBranch).toBeNull(); }); + + it('strips undefined filter operators inside OR branches', async () => { + await createUser('alice@test.com', { name: 'Alice', role: 'ADMIN' }); + await createUser('bob@test.com', { name: 'Bob', role: 'USER' }); + + const result = await client.user.findMany({ + where: { + OR: [{ name: { startsWith: 'A', contains: undefined } }], + } as any, + }); + + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('Alice'); + }); + // TODO: filter for bigint, decimal, bytes }); From 97df5defc9e5e147dffc335fc1722ec23904e4fe Mon Sep 17 00:00:00 2001 From: Emil Redzik Date: Mon, 30 Mar 2026 14:47:30 +0200 Subject: [PATCH 4/4] fix(orm): improve filter handling by adding isAllUndefinedFilter and refining argument normalization --- .../src/client/crud/dialects/base-dialect.ts | 26 ++++++++++++++++--- .../orm/src/client/crud/operations/base.ts | 16 ++++-------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index f3a4c92d1..7a75acf44 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -266,8 +266,8 @@ export abstract class BaseCrudDialect { ) .with('OR', () => { const branches = enumerate(payload) - .map((subPayload) => this.buildFilter(model, modelAlias, subPayload)) - .filter((expr) => !this.isTrue(expr)); + .filter((subPayload) => !this.isAllUndefinedFilter(subPayload)) + .map((subPayload) => this.buildFilter(model, modelAlias, subPayload)); return this.or(...branches); }) .with('NOT', () => this.eb.not(this.buildCompositeFilter(model, modelAlias, 'AND', payload))) @@ -803,6 +803,9 @@ export abstract class BaseCrudDialect { if (excludeKeys.includes(op)) { continue; } + if (value === undefined) { + continue; + } const rhs = Array.isArray(value) ? value.map(getRhs) : getRhs(value); const condition = match(op) .with('equals', () => (rhs === null ? this.eb(lhs, 'is', null) : this.eb(lhs, '=', rhs))) @@ -884,6 +887,10 @@ export abstract class BaseCrudDialect { continue; } + if (value === undefined) { + continue; + } + invariant(typeof value === 'string', `${key} value must be a string`); const escapedValue = this.escapeLikePattern(value); @@ -1165,7 +1172,8 @@ export abstract class BaseCrudDialect { // client-level: check both uncapitalized (current) and original (backward compat) model name const uncapModel = lowerCaseFirst(model); - const omitConfig = (this.options.omit as Record | undefined)?.[uncapModel] ?? + const omitConfig = + (this.options.omit as Record | undefined)?.[uncapModel] ?? (this.options.omit as Record | undefined)?.[model]; if (omitConfig && typeof omitConfig === 'object' && typeof omitConfig[field] === 'boolean') { return omitConfig[field]; @@ -1302,6 +1310,14 @@ export abstract class BaseCrudDialect { return this.eb.lit(this.transformInput(false, 'Boolean', false) as boolean); } + private isAllUndefinedFilter(payload: unknown): boolean { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return false; + } + const entries = Object.entries(payload); + return entries.length > 0 && entries.every(([, v]) => v === undefined); + } + public isTrue(expression: Expression) { const node = expression.toOperationNode(); if (node.kind !== 'ValueNode') { @@ -1360,7 +1376,9 @@ export abstract class BaseCrudDialect { const computedFields = this.options.computedFields as Record; // check both uncapitalized (current) and original (backward compat) model name const computedModel = fieldDef.originModel ?? model; - computer = computedFields?.[lowerCaseFirst(computedModel)]?.[field] ?? computedFields?.[computedModel]?.[field]; + computer = + computedFields?.[lowerCaseFirst(computedModel)]?.[field] ?? + computedFields?.[computedModel]?.[field]; } if (!computer) { throw createConfigError(`Computed field "${field}" implementation not provided for model "${model}"`); diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index ce726878a..46306c034 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -2531,17 +2531,11 @@ export abstract class BaseOperationHandler { private doNormalizeArgs(args: unknown) { if (args && typeof args === 'object') { - if (Array.isArray(args)) { - for (const element of args) { - this.doNormalizeArgs(element); - } - } else { - for (const [key, value] of Object.entries(args)) { - if (value === undefined) { - delete args[key as keyof typeof args]; - } else if (value && (isPlainObject(value) || Array.isArray(value))) { - this.doNormalizeArgs(value); - } + for (const [key, value] of Object.entries(args)) { + if (value === undefined) { + delete args[key as keyof typeof args]; + } else if (value && isPlainObject(value)) { + this.doNormalizeArgs(value); } } }