Skip to content
31 changes: 26 additions & 5 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,12 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
.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 branches = enumerate(payload)
.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)))
.exhaustive();
}
Expand Down Expand Up @@ -800,6 +803,9 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
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)))
Expand Down Expand Up @@ -881,6 +887,10 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
continue;
}

if (value === undefined) {
continue;
}

invariant(typeof value === 'string', `${key} value must be a string`);

const escapedValue = this.escapeLikePattern(value);
Expand Down Expand Up @@ -1162,7 +1172,8 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {

// client-level: check both uncapitalized (current) and original (backward compat) model name
const uncapModel = lowerCaseFirst(model);
const omitConfig = (this.options.omit as Record<string, any> | undefined)?.[uncapModel] ??
const omitConfig =
(this.options.omit as Record<string, any> | undefined)?.[uncapModel] ??
(this.options.omit as Record<string, any> | undefined)?.[model];
if (omitConfig && typeof omitConfig === 'object' && typeof omitConfig[field] === 'boolean') {
return omitConfig[field];
Expand Down Expand Up @@ -1299,6 +1310,14 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
return this.eb.lit<SqlBool>(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<SqlBool>) {
const node = expression.toOperationNode();
if (node.kind !== 'ValueNode') {
Expand Down Expand Up @@ -1357,7 +1376,9 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
const computedFields = this.options.computedFields as Record<string, any>;
// 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}"`);
Expand Down
52 changes: 52 additions & 0 deletions tests/e2e/orm/client-api/filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,5 +801,57 @@ describe('Client filter tests ', () => {
await expect(client.user.findMany({ where: { id: undefined } })).toResolveWithLength(1);
});

it('ignores undefined branch inside OR filters', 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' },
});

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();
});

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
});
Loading