Skip to content
Merged
5 changes: 5 additions & 0 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@ type CommonPrimitiveFilter<
*/
gte?: DataType;

/**
* Checks if the value is between the specified values (inclusive).
*/
between?: [start: DataType, end: DataType];

/**
* Builds a negated filter.
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,12 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
.with('lte', () => this.eb(lhs, '<=', rhs))
.with('gt', () => this.eb(lhs, '>', rhs))
.with('gte', () => this.eb(lhs, '>=', rhs))
.with('between', () => {
invariant(Array.isArray(rhs), 'right hand side must be an array');
invariant(rhs.length === 2, 'right hand side must have a length of 2');
const [start, end] = rhs;
return this.eb.and([this.eb(lhs, '>=', start), this.eb(lhs, '<=', end)]);
})
.with('not', () => this.eb.not(recurse(value)))
// aggregations
.with(P.union(...AGGREGATE_OPERATORS), (op) => {
Expand Down
1 change: 1 addition & 0 deletions packages/orm/src/client/crud/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ export class InputValidator<Schema extends SchemaDef> {
lte: baseSchema.optional(),
gt: baseSchema.optional(),
gte: baseSchema.optional(),
between: baseSchema.array().length(2).optional(),
not: makeThis().optional(),
...(withAggregations?.includes('_count')
? { _count: this.makeNumberFilterSchema(z.number().int(), false, false).optional() }
Expand Down
10 changes: 10 additions & 0 deletions packages/server/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const FilterOperations = [
'lte',
'gt',
'gte',
'between',
'contains',
'icontains',
'search',
Expand Down Expand Up @@ -1939,6 +1940,15 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
}
}
} else {
if (op === 'between') {
const parts = value
.split(',')
.map((v) => this.coerce(fieldDef, v));
if (parts.length !== 2) {
throw new InvalidValueError(`"between" expects exactly 2 comma-separated values`);
}
return { between: [parts[0]!, parts[1]!] };
}
const coerced = this.coerce(fieldDef, value);
switch (op) {
case 'icontains':
Expand Down
88 changes: 87 additions & 1 deletion packages/server/test/api/rest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ describe('REST server tests', () => {
});

it('toplevel filtering', async () => {
const now = new Date();
const past = new Date(now.getTime() - 1);
await client.user.create({
data: {
myId: 'user1',
Expand All @@ -436,7 +438,7 @@ describe('REST server tests', () => {
myId: 'user2',
email: '[email protected]',
posts: {
create: { id: 2, title: 'Post2', viewCount: 1, published: true },
create: { id: 2, title: 'Post2', viewCount: 1, published: true, publishedAt: now },
},
},
});
Expand Down Expand Up @@ -523,6 +525,38 @@ describe('REST server tests', () => {
});
expect(r.body.data).toHaveLength(0);

r = await handler({
method: 'get',
path: '/user',
query: { ['filter[email$between]']: ',[email protected]' },
client,
});
expect(r.body.data).toHaveLength(1);

r = await handler({
method: 'get',
path: '/user',
query: { ['filter[email$between]']: '[email protected],' },
client,
});
expect(r.body.data).toHaveLength(0);

r = await handler({
method: 'get',
path: '/user',
query: { ['filter[email$between]']: ',[email protected]' },
client,
});
expect(r.body.data).toHaveLength(2);

r = await handler({
method: 'get',
path: '/user',
query: { ['filter[email$between]']: '[email protected],[email protected]' },
client,
});
expect(r.body.data).toHaveLength(2);

// Int filter
r = await handler({
method: 'get',
Expand Down Expand Up @@ -568,6 +602,58 @@ describe('REST server tests', () => {
expect(r.body.data).toHaveLength(1);
expect(r.body.data[0]).toMatchObject({ id: 1 });

r = await handler({
method: 'get',
path: '/post',
query: { ['filter[viewCount$between]']: '1,2' },
client,
});
expect(r.body.data).toHaveLength(1);
expect(r.body.data[0]).toMatchObject({ id: 2 });

r = await handler({
method: 'get',
path: '/post',
query: { ['filter[viewCount$between]']: '2,1' },
client,
});
expect(r.body.data).toHaveLength(0);

r = await handler({
method: 'get',
path: '/post',
query: { ['filter[viewCount$between]']: '0,2' },
client,
});
expect(r.body.data).toHaveLength(2);

// DateTime filter
r = await handler({
method: 'get',
path: '/post',
query: { ['filter[publishedAt$between]']: `${now.toISOString()},${now.toISOString()}` },
client,
});
expect(r.body.data).toHaveLength(1);
expect(r.body.data[0]).toMatchObject({ id: 2 });

r = await handler({
method: 'get',
path: '/post',
query: { ['filter[publishedAt$between]']: `${past.toISOString()},${now.toISOString()}` },
client,
});
expect(r.body.data).toHaveLength(1);
expect(r.body.data[0]).toMatchObject({ id: 2 });

r = await handler({
method: 'get',
path: '/post',
query: { ['filter[publishedAt$between]']: `${now.toISOString()},${past.toISOString()}` },
client,
});
expect(r.body.data).toHaveLength(0);

// Boolean filter
r = await handler({
method: 'get',
Expand Down
134 changes: 132 additions & 2 deletions tests/e2e/orm/client-api/filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,58 @@ describe('Client filter tests ', () => {
}),
).toResolveWithLength(2);

// between
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', '[email protected]'] } },
}),
).toResolveWithLength(0);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', '[email protected]'] } },
}),
).toResolveWithLength(0);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', '[email protected]'] } },
}),
).toResolveWithLength(0);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', '[email protected]'] } },
}),
).toResolveWithLength(1);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', '[email protected]'] } },
}),
).toResolveWithLength(1);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', '[email protected]'] } },
}),
).toResolveWithLength(2);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', 'u3%@test.com'] } },
}),
).toResolveWithLength(2);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', 'u3%@test.com'] } },
}),
).toResolveWithLength(3);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', '[email protected]'] } },
}),
).toResolveWithLength(3);
await expect(
client.user.findMany({
where: { email: { between: ['[email protected]', 'u3%@test.com'] } },
}),
).toResolveWithLength(3);

// contains
await expect(
client.user.findFirst({
Expand Down Expand Up @@ -409,6 +461,14 @@ describe('Client filter tests ', () => {
await expect(client.profile.findMany({ where: { age: { gte: 20 } } })).toResolveWithLength(1);
await expect(client.profile.findMany({ where: { age: { gte: 21 } } })).toResolveWithLength(0);

// between
await expect(client.profile.findMany({ where: { age: { between: [20, 20] } } })).toResolveWithLength(1);
await expect(client.profile.findMany({ where: { age: { between: [19, 20] } } })).toResolveWithLength(1);
await expect(client.profile.findMany({ where: { age: { between: [20, 21] } } })).toResolveWithLength(1);
await expect(client.profile.findMany({ where: { age: { between: [19, 19] } } })).toResolveWithLength(0);
await expect(client.profile.findMany({ where: { age: { between: [21, 21] } } })).toResolveWithLength(0);
await expect(client.profile.findMany({ where: { age: { between: [21, 20] } } })).toResolveWithLength(0);

// not
await expect(
client.profile.findFirst({
Expand Down Expand Up @@ -460,11 +520,14 @@ describe('Client filter tests ', () => {
});

it('supports date filters', async () => {
const now = new Date();
const past = new Date(now.getTime() - 1);
const future = new Date(now.getTime() + 2);
const user1 = await createUser('[email protected]', {
createdAt: new Date(),
createdAt: now,
});
const user2 = await createUser('[email protected]', {
createdAt: new Date(Date.now() + 1000),
createdAt: new Date(now.getTime() + 1),
});

// equals
Expand Down Expand Up @@ -577,6 +640,73 @@ describe('Client filter tests ', () => {
}),
).resolves.toMatchObject(user2);

// between
await expect(
client.user.findMany({
where: { createdAt: { between: [user1.createdAt, user1.createdAt] } },
}),
).toResolveWithLength(1);
await expect(
client.user.findMany({
where: { createdAt: { between: [user1.createdAt, user2.createdAt] } },
}),
).toResolveWithLength(2);
await expect(
client.user.findMany({
where: { createdAt: { between: [user2.createdAt, user2.createdAt] } },
}),
).toResolveWithLength(1);
await expect(
client.user.findMany({
where: { createdAt: { between: [user2.createdAt, user1.createdAt] } },
}),
).toResolveWithLength(0);
await expect(
client.user.findMany({
where: { createdAt: { between: [past, past] } },
}),
).toResolveWithLength(0);
await expect(
client.user.findMany({
where: { createdAt: { between: [past, user1.createdAt] } },
}),
).toResolveWithLength(1);
await expect(
client.user.findMany({
where: { createdAt: { between: [past.toISOString(), user1.createdAt] } },
}),
).toResolveWithLength(1);
await expect(
client.user.findMany({
where: { createdAt: { between: [past, user2.createdAt] } },
}),
).toResolveWithLength(2);
await expect(
client.user.findMany({
where: { createdAt: { between: [past, future] } },
}),
).toResolveWithLength(2);
await expect(
client.user.findMany({
where: { createdAt: { between: [past.toISOString(), future.toISOString()] } },
}),
).toResolveWithLength(2);
await expect(
client.user.findMany({
where: { createdAt: { between: [future, past] } },
}),
).toResolveWithLength(0);
await expect(
client.user.findMany({
where: { createdAt: { between: [future, user1.createdAt] } },
}),
).toResolveWithLength(0);
await expect(
client.user.findMany({
where: { createdAt: { between: [future, future] } },
}),
).toResolveWithLength(0);

// not
await expect(
client.user.findFirst({
Expand Down
Loading