Skip to content

Commit 401f45d

Browse files
authored
fix(rest): support json equality filtering (#2088)
1 parent 002e132 commit 401f45d

File tree

2 files changed

+81
-9
lines changed

2 files changed

+81
-9
lines changed

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

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,14 +1349,14 @@ class RequestHandler extends APIHandlerBase {
13491349
private makePrismaIdFilter(idFields: FieldInfo[], resourceId: string, nested: boolean = true) {
13501350
const decodedId = decodeURIComponent(resourceId);
13511351
if (idFields.length === 1) {
1352-
return { [idFields[0].name]: this.coerce(idFields[0].type, decodedId) };
1352+
return { [idFields[0].name]: this.coerce(idFields[0], decodedId) };
13531353
} else if (nested) {
13541354
return {
13551355
// TODO: support `@@id` with custom name
13561356
[idFields.map((idf) => idf.name).join(prismaIdDivider)]: idFields.reduce(
13571357
(acc, curr, idx) => ({
13581358
...acc,
1359-
[curr.name]: this.coerce(curr.type, decodedId.split(this.idDivider)[idx]),
1359+
[curr.name]: this.coerce(curr, decodedId.split(this.idDivider)[idx]),
13601360
}),
13611361
{}
13621362
),
@@ -1365,7 +1365,7 @@ class RequestHandler extends APIHandlerBase {
13651365
return idFields.reduce(
13661366
(acc, curr, idx) => ({
13671367
...acc,
1368-
[curr.name]: this.coerce(curr.type, decodedId.split(this.idDivider)[idx]),
1368+
[curr.name]: this.coerce(curr, decodedId.split(this.idDivider)[idx]),
13691369
}),
13701370
{}
13711371
);
@@ -1381,13 +1381,13 @@ class RequestHandler extends APIHandlerBase {
13811381

13821382
private makeIdConnect(idFields: FieldInfo[], id: string | number) {
13831383
if (idFields.length === 1) {
1384-
return { [idFields[0].name]: this.coerce(idFields[0].type, id) };
1384+
return { [idFields[0].name]: this.coerce(idFields[0], id) };
13851385
} else {
13861386
return {
13871387
[this.makePrismaIdKey(idFields)]: idFields.reduce(
13881388
(acc, curr, idx) => ({
13891389
...acc,
1390-
[curr.name]: this.coerce(curr.type, `${id}`.split(this.idDivider)[idx]),
1390+
[curr.name]: this.coerce(curr, `${id}`.split(this.idDivider)[idx]),
13911391
}),
13921392
{}
13931393
),
@@ -1436,8 +1436,17 @@ class RequestHandler extends APIHandlerBase {
14361436
}
14371437
}
14381438

1439-
private coerce(type: string, value: any) {
1439+
private coerce(fieldInfo: FieldInfo, value: any) {
14401440
if (typeof value === 'string') {
1441+
if (fieldInfo.isTypeDef || fieldInfo.type === 'Json') {
1442+
try {
1443+
return JSON.parse(value);
1444+
} catch {
1445+
throw new InvalidValueError(`invalid JSON value: ${value}`);
1446+
}
1447+
}
1448+
1449+
const type = fieldInfo.type;
14411450
if (type === 'Int' || type === 'BigInt') {
14421451
const parsed = parseInt(value);
14431452
if (isNaN(parsed)) {
@@ -1738,6 +1747,7 @@ class RequestHandler extends APIHandlerBase {
17381747
}
17391748

17401749
private makeFilterValue(fieldInfo: FieldInfo, value: string, op: FilterOperationType): any {
1750+
// TODO: inequality filters?
17411751
if (fieldInfo.isDataModel) {
17421752
// relation filter is converted to an ID filter
17431753
const info = this.typeMap[lowerCaseFirst(fieldInfo.type)];
@@ -1758,7 +1768,7 @@ class RequestHandler extends APIHandlerBase {
17581768
}
17591769
}
17601770
} else {
1761-
const coerced = this.coerce(fieldInfo.type, value);
1771+
const coerced = this.coerce(fieldInfo, value);
17621772
switch (op) {
17631773
case 'icontains':
17641774
return { contains: coerced, mode: 'insensitive' };
@@ -1767,7 +1777,7 @@ class RequestHandler extends APIHandlerBase {
17671777
const values = value
17681778
.split(',')
17691779
.filter((i) => i)
1770-
.map((v) => this.coerce(fieldInfo.type, v));
1780+
.map((v) => this.coerce(fieldInfo, v));
17711781
return { [op]: values };
17721782
}
17731783
case 'isEmpty':
@@ -1777,11 +1787,16 @@ class RequestHandler extends APIHandlerBase {
17771787
return { isEmpty: value === 'true' ? true : false };
17781788
default:
17791789
if (op === undefined) {
1790+
if (fieldInfo.isTypeDef || fieldInfo.type === 'Json') {
1791+
// handle JSON value equality filter
1792+
return { equals: coerced };
1793+
}
1794+
17801795
// regular filter, split value by comma
17811796
const values = value
17821797
.split(',')
17831798
.filter((i) => i)
1784-
.map((v) => this.coerce(fieldInfo.type, v));
1799+
.map((v) => this.coerce(fieldInfo, v));
17851800
return values.length > 1 ? { in: values } : { equals: values[0] };
17861801
} else {
17871802
return { [op]: coerced };

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ describe('REST server tests', () => {
2222

2323
describe('REST server tests - regular prisma', () => {
2424
const schema = `
25+
type Address {
26+
city String
27+
}
28+
2529
model User {
2630
myId String @id @default(cuid())
2731
createdAt DateTime @default (now())
@@ -30,6 +34,8 @@ describe('REST server tests', () => {
3034
posts Post[]
3135
likes PostLike[]
3236
profile Profile?
37+
address Address? @json
38+
someJson Json?
3339
}
3440
3541
model Profile {
@@ -428,6 +434,8 @@ describe('REST server tests', () => {
428434
data: {
429435
myId: 'user1',
430436
email: 'user1@abc.com',
437+
address: { city: 'Seattle' },
438+
someJson: 'foo',
431439
posts: {
432440
create: { id: 1, title: 'Post1' },
433441
},
@@ -723,6 +731,55 @@ describe('REST server tests', () => {
723731
},
724732
],
725733
});
734+
735+
// typedef equality filter
736+
r = await handler({
737+
method: 'get',
738+
path: '/user',
739+
query: { ['filter[address]']: JSON.stringify({ city: 'Seattle' }) },
740+
prisma,
741+
});
742+
expect(r.body.data).toHaveLength(1);
743+
r = await handler({
744+
method: 'get',
745+
path: '/user',
746+
query: { ['filter[address]']: JSON.stringify({ city: 'Tokyo' }) },
747+
prisma,
748+
});
749+
expect(r.body.data).toHaveLength(0);
750+
751+
// plain json equality filter
752+
r = await handler({
753+
method: 'get',
754+
path: '/user',
755+
query: { ['filter[someJson]']: JSON.stringify('foo') },
756+
prisma,
757+
});
758+
expect(r.body.data).toHaveLength(1);
759+
r = await handler({
760+
method: 'get',
761+
path: '/user',
762+
query: { ['filter[someJson]']: JSON.stringify('bar') },
763+
prisma,
764+
});
765+
expect(r.body.data).toHaveLength(0);
766+
767+
// invalid json
768+
r = await handler({
769+
method: 'get',
770+
path: '/user',
771+
query: { ['filter[someJson]']: '{ hello: world }' },
772+
prisma,
773+
});
774+
expect(r.body).toMatchObject({
775+
errors: [
776+
{
777+
status: 400,
778+
code: 'invalid-value',
779+
title: 'Invalid value for type',
780+
},
781+
],
782+
});
726783
});
727784

728785
it('related data filtering', async () => {

0 commit comments

Comments
 (0)