Skip to content
Open
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
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