Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 27e8910

Browse files
authored
feat: between operator (#569)
* feat: `between` operator * chore: add tests * Document inclusivity. * Add reversed order test. * chore: add REST server test * Fix test typo. * Verify correct object returned. * Additional test cases. * Make AI suggested changes. * Remove filter. * Fix styling. * Add more REST tests. * Add more int tests for REST.
1 parent 33f6485 commit 27e8910

6 files changed

Lines changed: 241 additions & 3 deletions

File tree

packages/orm/src/client/crud-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,11 @@ type CommonPrimitiveFilter<
450450
*/
451451
gte?: DataType;
452452

453+
/**
454+
* Checks if the value is between the specified values (inclusive).
455+
*/
456+
between?: [start: DataType, end: DataType];
457+
453458
/**
454459
* Builds a negated filter.
455460
*/

packages/orm/src/client/crud/dialects/base-dialect.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,12 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
765765
.with('lte', () => this.eb(lhs, '<=', rhs))
766766
.with('gt', () => this.eb(lhs, '>', rhs))
767767
.with('gte', () => this.eb(lhs, '>=', rhs))
768+
.with('between', () => {
769+
invariant(Array.isArray(rhs), 'right hand side must be an array');
770+
invariant(rhs.length === 2, 'right hand side must have a length of 2');
771+
const [start, end] = rhs;
772+
return this.eb.and([this.eb(lhs, '>=', start), this.eb(lhs, '<=', end)]);
773+
})
768774
.with('not', () => this.eb.not(recurse(value)))
769775
// aggregations
770776
.with(P.union(...AGGREGATE_OPERATORS), (op) => {

packages/orm/src/client/crud/validator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,7 @@ export class InputValidator<Schema extends SchemaDef> {
890890
lte: baseSchema.optional(),
891891
gt: baseSchema.optional(),
892892
gte: baseSchema.optional(),
893+
between: baseSchema.array().length(2).optional(),
893894
not: makeThis().optional(),
894895
...(withAggregations?.includes('_count')
895896
? { _count: this.makeNumberFilterSchema(z.number().int(), false, false).optional() }

packages/server/src/api/rest/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const FilterOperations = [
106106
'lte',
107107
'gt',
108108
'gte',
109+
'between',
109110
'contains',
110111
'icontains',
111112
'search',
@@ -2058,6 +2059,15 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
20582059
}
20592060
}
20602061
} else {
2062+
if (op === 'between') {
2063+
const parts = value
2064+
.split(',')
2065+
.map((v) => this.coerce(fieldDef, v));
2066+
if (parts.length !== 2) {
2067+
throw new InvalidValueError(`"between" expects exactly 2 comma-separated values`);
2068+
}
2069+
return { between: [parts[0]!, parts[1]!] };
2070+
}
20612071
const coerced = this.coerce(fieldDef, value);
20622072
switch (op) {
20632073
case 'icontains':

packages/server/test/api/rest.test.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,8 @@ describe('REST server tests', () => {
420420
});
421421

422422
it('toplevel filtering', async () => {
423+
const now = new Date();
424+
const past = new Date(now.getTime() - 1);
423425
await client.user.create({
424426
data: {
425427
myId: 'user1',
@@ -436,7 +438,7 @@ describe('REST server tests', () => {
436438
myId: 'user2',
437439
email: 'user2@abc.com',
438440
posts: {
439-
create: { id: 2, title: 'Post2', viewCount: 1, published: true },
441+
create: { id: 2, title: 'Post2', viewCount: 1, published: true, publishedAt: now },
440442
},
441443
},
442444
});
@@ -523,6 +525,38 @@ describe('REST server tests', () => {
523525
});
524526
expect(r.body.data).toHaveLength(0);
525527

528+
r = await handler({
529+
method: 'get',
530+
path: '/user',
531+
query: { ['filter[email$between]']: ',user1@abc.com' },
532+
client,
533+
});
534+
expect(r.body.data).toHaveLength(1);
535+
536+
r = await handler({
537+
method: 'get',
538+
path: '/user',
539+
query: { ['filter[email$between]']: 'user1@abc.com,' },
540+
client,
541+
});
542+
expect(r.body.data).toHaveLength(0);
543+
544+
r = await handler({
545+
method: 'get',
546+
path: '/user',
547+
query: { ['filter[email$between]']: ',user2@abc.com' },
548+
client,
549+
});
550+
expect(r.body.data).toHaveLength(2);
551+
552+
r = await handler({
553+
method: 'get',
554+
path: '/user',
555+
query: { ['filter[email$between]']: 'user1@abc.com,user2@abc.com' },
556+
client,
557+
});
558+
expect(r.body.data).toHaveLength(2);
559+
526560
// Int filter
527561
r = await handler({
528562
method: 'get',
@@ -568,6 +602,58 @@ describe('REST server tests', () => {
568602
expect(r.body.data).toHaveLength(1);
569603
expect(r.body.data[0]).toMatchObject({ id: 1 });
570604

605+
r = await handler({
606+
method: 'get',
607+
path: '/post',
608+
query: { ['filter[viewCount$between]']: '1,2' },
609+
client,
610+
});
611+
expect(r.body.data).toHaveLength(1);
612+
expect(r.body.data[0]).toMatchObject({ id: 2 });
613+
614+
r = await handler({
615+
method: 'get',
616+
path: '/post',
617+
query: { ['filter[viewCount$between]']: '2,1' },
618+
client,
619+
});
620+
expect(r.body.data).toHaveLength(0);
621+
622+
r = await handler({
623+
method: 'get',
624+
path: '/post',
625+
query: { ['filter[viewCount$between]']: '0,2' },
626+
client,
627+
});
628+
expect(r.body.data).toHaveLength(2);
629+
630+
// DateTime filter
631+
r = await handler({
632+
method: 'get',
633+
path: '/post',
634+
query: { ['filter[publishedAt$between]']: `${now.toISOString()},${now.toISOString()}` },
635+
client,
636+
});
637+
expect(r.body.data).toHaveLength(1);
638+
expect(r.body.data[0]).toMatchObject({ id: 2 });
639+
640+
r = await handler({
641+
method: 'get',
642+
path: '/post',
643+
query: { ['filter[publishedAt$between]']: `${past.toISOString()},${now.toISOString()}` },
644+
client,
645+
});
646+
expect(r.body.data).toHaveLength(1);
647+
expect(r.body.data[0]).toMatchObject({ id: 2 });
648+
649+
r = await handler({
650+
method: 'get',
651+
path: '/post',
652+
query: { ['filter[publishedAt$between]']: `${now.toISOString()},${past.toISOString()}` },
653+
client,
654+
});
655+
expect(r.body.data).toHaveLength(0);
656+
571657
// Boolean filter
572658
r = await handler({
573659
method: 'get',

tests/e2e/orm/client-api/filter.test.ts

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,58 @@ describe('Client filter tests ', () => {
315315
}),
316316
).toResolveWithLength(2);
317317

318+
// between
319+
await expect(
320+
client.user.findMany({
321+
where: { email: { between: ['a@test.com', 'a@test.com'] } },
322+
}),
323+
).toResolveWithLength(0);
324+
await expect(
325+
client.user.findMany({
326+
where: { email: { between: ['a@test.com', 'b@test.com'] } },
327+
}),
328+
).toResolveWithLength(0);
329+
await expect(
330+
client.user.findMany({
331+
where: { email: { between: ['z@test.com', 'a@test.com'] } },
332+
}),
333+
).toResolveWithLength(0);
334+
await expect(
335+
client.user.findMany({
336+
where: { email: { between: ['u1@test.com', 'u1@test.com'] } },
337+
}),
338+
).toResolveWithLength(1);
339+
await expect(
340+
client.user.findMany({
341+
where: { email: { between: ['u2@test.com', 'u2@test.com'] } },
342+
}),
343+
).toResolveWithLength(1);
344+
await expect(
345+
client.user.findMany({
346+
where: { email: { between: ['u1@test.com', 'u2@test.com'] } },
347+
}),
348+
).toResolveWithLength(2);
349+
await expect(
350+
client.user.findMany({
351+
where: { email: { between: ['u2@test.com', 'u3%@test.com'] } },
352+
}),
353+
).toResolveWithLength(2);
354+
await expect(
355+
client.user.findMany({
356+
where: { email: { between: ['a@test.com', 'u3%@test.com'] } },
357+
}),
358+
).toResolveWithLength(3);
359+
await expect(
360+
client.user.findMany({
361+
where: { email: { between: ['a@test.com', 'z@test.com'] } },
362+
}),
363+
).toResolveWithLength(3);
364+
await expect(
365+
client.user.findMany({
366+
where: { email: { between: ['u1@test.com', 'u3%@test.com'] } },
367+
}),
368+
).toResolveWithLength(3);
369+
318370
// contains
319371
await expect(
320372
client.user.findFirst({
@@ -409,6 +461,14 @@ describe('Client filter tests ', () => {
409461
await expect(client.profile.findMany({ where: { age: { gte: 20 } } })).toResolveWithLength(1);
410462
await expect(client.profile.findMany({ where: { age: { gte: 21 } } })).toResolveWithLength(0);
411463

464+
// between
465+
await expect(client.profile.findMany({ where: { age: { between: [20, 20] } } })).toResolveWithLength(1);
466+
await expect(client.profile.findMany({ where: { age: { between: [19, 20] } } })).toResolveWithLength(1);
467+
await expect(client.profile.findMany({ where: { age: { between: [20, 21] } } })).toResolveWithLength(1);
468+
await expect(client.profile.findMany({ where: { age: { between: [19, 19] } } })).toResolveWithLength(0);
469+
await expect(client.profile.findMany({ where: { age: { between: [21, 21] } } })).toResolveWithLength(0);
470+
await expect(client.profile.findMany({ where: { age: { between: [21, 20] } } })).toResolveWithLength(0);
471+
412472
// not
413473
await expect(
414474
client.profile.findFirst({
@@ -460,11 +520,14 @@ describe('Client filter tests ', () => {
460520
});
461521

462522
it('supports date filters', async () => {
523+
const now = new Date();
524+
const past = new Date(now.getTime() - 1);
525+
const future = new Date(now.getTime() + 2);
463526
const user1 = await createUser('u1@test.com', {
464-
createdAt: new Date(),
527+
createdAt: now,
465528
});
466529
const user2 = await createUser('u2@test.com', {
467-
createdAt: new Date(Date.now() + 1000),
530+
createdAt: new Date(now.getTime() + 1),
468531
});
469532

470533
// equals
@@ -577,6 +640,73 @@ describe('Client filter tests ', () => {
577640
}),
578641
).resolves.toMatchObject(user2);
579642

643+
// between
644+
await expect(
645+
client.user.findMany({
646+
where: { createdAt: { between: [user1.createdAt, user1.createdAt] } },
647+
}),
648+
).toResolveWithLength(1);
649+
await expect(
650+
client.user.findMany({
651+
where: { createdAt: { between: [user1.createdAt, user2.createdAt] } },
652+
}),
653+
).toResolveWithLength(2);
654+
await expect(
655+
client.user.findMany({
656+
where: { createdAt: { between: [user2.createdAt, user2.createdAt] } },
657+
}),
658+
).toResolveWithLength(1);
659+
await expect(
660+
client.user.findMany({
661+
where: { createdAt: { between: [user2.createdAt, user1.createdAt] } },
662+
}),
663+
).toResolveWithLength(0);
664+
await expect(
665+
client.user.findMany({
666+
where: { createdAt: { between: [past, past] } },
667+
}),
668+
).toResolveWithLength(0);
669+
await expect(
670+
client.user.findMany({
671+
where: { createdAt: { between: [past, user1.createdAt] } },
672+
}),
673+
).toResolveWithLength(1);
674+
await expect(
675+
client.user.findMany({
676+
where: { createdAt: { between: [past.toISOString(), user1.createdAt] } },
677+
}),
678+
).toResolveWithLength(1);
679+
await expect(
680+
client.user.findMany({
681+
where: { createdAt: { between: [past, user2.createdAt] } },
682+
}),
683+
).toResolveWithLength(2);
684+
await expect(
685+
client.user.findMany({
686+
where: { createdAt: { between: [past, future] } },
687+
}),
688+
).toResolveWithLength(2);
689+
await expect(
690+
client.user.findMany({
691+
where: { createdAt: { between: [past.toISOString(), future.toISOString()] } },
692+
}),
693+
).toResolveWithLength(2);
694+
await expect(
695+
client.user.findMany({
696+
where: { createdAt: { between: [future, past] } },
697+
}),
698+
).toResolveWithLength(0);
699+
await expect(
700+
client.user.findMany({
701+
where: { createdAt: { between: [future, user1.createdAt] } },
702+
}),
703+
).toResolveWithLength(0);
704+
await expect(
705+
client.user.findMany({
706+
where: { createdAt: { between: [future, future] } },
707+
}),
708+
).toResolveWithLength(0);
709+
580710
// not
581711
await expect(
582712
client.user.findFirst({

0 commit comments

Comments
 (0)