Skip to content

Commit d01f101

Browse files
committed
addressing PR comments
1 parent 6c836f6 commit d01f101

File tree

3 files changed

+88
-34
lines changed

3 files changed

+88
-34
lines changed

packages/server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@
7272
"express": "^5.0.0",
7373
"next": "^15.0.0",
7474
"supertest": "^7.1.4",
75-
"zod": "catalog:"
75+
"zod": "~3.25.0"
7676
},
7777
"peerDependencies": {
7878
"express": "^5.0.0",
7979
"next": "^15.0.0",
80-
"zod": "~3.25.0"
80+
"zod": "catalog:"
8181
},
8282
"peerDependenciesMeta": {
8383
"express": {

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

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type ClientContract,
99
} from '@zenstackhq/orm';
1010
import type { FieldDef, ModelDef, SchemaDef } from '@zenstackhq/orm/schema';
11+
import { Decimal } from 'decimal.js';
1112
import SuperJSON from 'superjson';
1213
import { Linker, Paginator, Relator, Serializer, type SerializerOptions } from 'ts-japi';
1314
import UrlPattern from 'url-pattern';
@@ -37,7 +38,7 @@ export type RestApiHandlerOptions<Schema extends SchemaDef = SchemaDef> = {
3738
/**
3839
* The default page size for limiting the number of results returned
3940
* from collection queries, including resource collection, related data
40-
* of collection types, and relashionship of collection types.
41+
* of collection types, and relationship of collection types.
4142
*
4243
* Defaults to 100. Set to Infinity to disable pagination.
4344
*/
@@ -200,7 +201,7 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
200201
title: 'Error occurred while executing the query',
201202
},
202203
unknownError: {
203-
status: 400,
204+
status: 500,
204205
title: 'Unknown error',
205206
},
206207
};
@@ -851,10 +852,20 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
851852
body = SuperJSON.deserialize({ json: body, meta: body.meta.serialization });
852853
}
853854

854-
const parsed = this.createUpdatePayloadSchema.parse(body);
855-
const attributes: any = parsed.data.attributes;
855+
const parseResult = this.createUpdatePayloadSchema.safeParse(body);
856+
if (!parseResult.success) {
857+
return {
858+
attributes: undefined,
859+
relationships: undefined,
860+
error: this.makeError('invalidPayload', getZodErrorMessage(parseResult.error)),
861+
};
862+
}
856863

857-
return { attributes, relationships: parsed.data.relationships };
864+
return {
865+
attributes: parseResult.data.data.attributes,
866+
relationships: parseResult.data.data.relationships,
867+
error: undefined,
868+
};
858869
}
859870

860871
private async processCreate(
@@ -868,7 +879,10 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
868879
return this.makeUnsupportedModelError(type);
869880
}
870881

871-
const { attributes, relationships } = this.processRequestBody(requestBody);
882+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
883+
if (error) {
884+
return error;
885+
}
872886

873887
const createPayload: any = { data: { ...attributes } };
874888

@@ -929,9 +943,16 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
929943
}
930944

931945
const modelName = typeInfo.name;
932-
const { attributes, relationships } = this.processRequestBody(requestBody);
946+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
947+
if (error) {
948+
return error;
949+
}
933950

934-
const matchFields = this.upsertMetaSchema.parse(requestBody).meta.matchFields;
951+
const parseResult = this.upsertMetaSchema.safeParse(requestBody);
952+
if (parseResult.error) {
953+
return this.makeError('invalidPayload', getZodErrorMessage(parseResult.error));
954+
}
955+
const matchFields = parseResult.data.meta.matchFields;
935956
const uniqueFieldSets = this.getUniqueFieldSets(modelName);
936957

937958
if (!uniqueFieldSets.some((set) => set.every((field) => matchFields.includes(field)))) {
@@ -942,7 +963,7 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
942963
where: this.makeUpsertWhere(matchFields, attributes, typeInfo),
943964
create: { ...attributes },
944965
update: {
945-
...Object.fromEntries(Object.entries(attributes).filter((e) => !matchFields.includes(e[0]))),
966+
...Object.fromEntries(Object.entries(attributes ?? {}).filter((e) => !matchFields.includes(e[0]))),
946967
},
947968
};
948969

@@ -1110,7 +1131,10 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
11101131
return this.makeUnsupportedModelError(type);
11111132
}
11121133

1113-
const { attributes, relationships } = this.processRequestBody(requestBody);
1134+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
1135+
if (error) {
1136+
return error;
1137+
}
11141138

11151139
const updatePayload: any = {
11161140
where: this.makeIdFilter(typeInfo.idFields, resourceId),
@@ -1198,9 +1222,11 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
11981222
const externalIdName = this.externalIdMapping[modelLower];
11991223
for (const [name, info] of Object.entries(modelDef.uniqueFields)) {
12001224
if (name === externalIdName) {
1201-
if (typeof info === 'string') {
1202-
return [this.requireField(model, info)];
1225+
if (typeof info.type === 'string') {
1226+
// single unique field
1227+
return [this.requireField(model, info.type)];
12031228
} else {
1229+
// compound unique fields
12041230
return Object.keys(info).map((f) => this.requireField(model, f));
12051231
}
12061232
}
@@ -1561,18 +1587,30 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
15611587
}
15621588

15631589
const type = fieldDef.type;
1564-
if (type === 'Int' || type === 'BigInt') {
1590+
if (type === 'Int') {
15651591
const parsed = parseInt(value);
15661592
if (isNaN(parsed)) {
15671593
throw new InvalidValueError(`invalid ${type} value: ${value}`);
15681594
}
15691595
return parsed;
1570-
} else if (type === 'Float' || type === 'Decimal') {
1596+
} else if (type === 'BigInt') {
1597+
try {
1598+
return BigInt(value);
1599+
} catch {
1600+
throw new InvalidValueError(`invalid ${type} value: ${value}`);
1601+
}
1602+
} else if (type === 'Float') {
15711603
const parsed = parseFloat(value);
15721604
if (isNaN(parsed)) {
15731605
throw new InvalidValueError(`invalid ${type} value: ${value}`);
15741606
}
15751607
return parsed;
1608+
} else if (type === 'Decimal') {
1609+
try {
1610+
return new Decimal(value);
1611+
} catch {
1612+
throw new InvalidValueError(`invalid ${type} value: ${value}`);
1613+
}
15761614
} else if (type === 'Boolean') {
15771615
if (value === 'true') {
15781616
return true;
@@ -1592,6 +1630,7 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
15921630
if (
15931631
key.startsWith('filter[') ||
15941632
key.startsWith('sort[') ||
1633+
key === 'include' ||
15951634
key.startsWith('include[') ||
15961635
key.startsWith('fields[')
15971636
) {
@@ -1678,7 +1717,6 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
16781717
let curr = item;
16791718
let currType = typeInfo;
16801719

1681-
const idFields = this.getIdFields(typeInfo.name);
16821720
for (const filterValue of enumerate(value)) {
16831721
for (let i = 0; i < filterKeys.length; i++) {
16841722
// extract filter operation from (optional) trailing $op
@@ -1697,6 +1735,7 @@ export class RestApiHandler<Schema extends SchemaDef> implements ApiHandler<Sche
16971735
};
16981736
}
16991737

1738+
const idFields = this.getIdFields(currType.name);
17001739
const fieldDef =
17011740
filterKey === 'id'
17021741
? Object.values(currType.fields).find((f) => idFields.some((idf) => idf.name === f.name))

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

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import { createPolicyTestClient, createTestClient } from '@zenstackhq/testtools'
44
import { Decimal } from 'decimal.js';
55
import SuperJSON from 'superjson';
66
import { beforeEach, describe, expect, it } from 'vitest';
7-
import makeHandler from '../../src/api/rest';
7+
import { RestApiHandler } from '../../src/api/rest';
88

99
const idDivider = '_';
1010

1111
describe('REST server tests', () => {
1212
let client: ClientContract<SchemaDef>;
1313
let handler: (any: any) => Promise<{ status: number; body: any }>;
1414

15-
describe('REST server tests - regular prisma', () => {
15+
describe('REST server tests - regular client', () => {
1616
const schema = `
1717
type Address {
1818
city String
@@ -87,8 +87,12 @@ describe('REST server tests', () => {
8787

8888
beforeEach(async () => {
8989
client = await createTestClient(schema);
90-
const _handler = makeHandler({ schema: client.$schema, endpoint: 'http://localhost/api', pageSize: 5 });
91-
handler = (args) => _handler({ ...args, url: new URL(`http://localhost/${args.path}`) });
90+
const _handler = new RestApiHandler({
91+
schema: client.$schema,
92+
endpoint: 'http://localhost/api',
93+
pageSize: 5,
94+
});
95+
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
9296
});
9397

9498
describe('CRUD', () => {
@@ -2567,8 +2571,12 @@ describe('REST server tests', () => {
25672571
beforeEach(async () => {
25682572
client = await createPolicyTestClient(schema);
25692573

2570-
const _handler = makeHandler({ schema: client.$schema, endpoint: 'http://localhost/api', pageSize: 5 });
2571-
handler = (args) => _handler({ ...args, url: new URL(`http://localhost/${args.path}`) });
2574+
const _handler = new RestApiHandler({
2575+
schema: client.$schema,
2576+
endpoint: 'http://localhost/api',
2577+
pageSize: 5,
2578+
});
2579+
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
25722580
});
25732581

25742582
it('update policy rejection test', async () => {
@@ -2673,8 +2681,12 @@ describe('REST server tests', () => {
26732681
beforeEach(async () => {
26742682
client = await createPolicyTestClient(schema);
26752683

2676-
const _handler = makeHandler({ schema: client.$schema, endpoint: 'http://localhost/api', pageSize: 5 });
2677-
handler = (args) => _handler({ ...args, url: new URL(`http://localhost/${args.path}`) });
2684+
const _handler = new RestApiHandler({
2685+
schema: client.$schema,
2686+
endpoint: 'http://localhost/api',
2687+
pageSize: 5,
2688+
});
2689+
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
26782690
});
26792691

26802692
it('crud test', async () => {
@@ -2745,8 +2757,12 @@ describe('REST server tests', () => {
27452757
it('field types', async () => {
27462758
const client = await createTestClient(schema, { provider: 'postgresql' });
27472759

2748-
const _handler = makeHandler({ schema: client.$schema, endpoint: 'http://localhost/api', pageSize: 5 });
2749-
handler = (args) => _handler({ ...args, url: new URL(`http://localhost/${args.path}`) });
2760+
const _handler = new RestApiHandler({
2761+
schema: client.$schema,
2762+
endpoint: 'http://localhost/api',
2763+
pageSize: 5,
2764+
});
2765+
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
27502766

27512767
await client.bar.create({ data: { id: 1, bytes: Buffer.from([7, 8, 9]) } });
27522768

@@ -2887,19 +2903,18 @@ describe('REST server tests', () => {
28872903
}
28882904
`;
28892905
const idDivider = ':';
2890-
const dbName = 'restful-compound-id-custom-separator';
28912906

28922907
beforeEach(async () => {
28932908
client = await createTestClient(schema);
28942909

2895-
const _handler = makeHandler({
2910+
const _handler = new RestApiHandler({
28962911
schema: client.$schema,
28972912
endpoint: 'http://localhost/api',
28982913
pageSize: 5,
28992914
idDivider,
29002915
urlSegmentCharset: 'a-zA-Z0-9-_~ %@.:',
29012916
});
2902-
handler = (args) => _handler({ ...args, url: new URL(`http://localhost/${args.path}`) });
2917+
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
29032918
});
29042919

29052920
it('POST', async () => {
@@ -2992,14 +3007,14 @@ describe('REST server tests', () => {
29923007
beforeEach(async () => {
29933008
client = await createTestClient(schema);
29943009

2995-
const _handler = makeHandler({
3010+
const _handler = new RestApiHandler({
29963011
schema: client.$schema,
29973012
endpoint: 'http://localhost/api',
29983013
modelNameMapping: {
29993014
User: 'myUser',
30003015
},
30013016
});
3002-
handler = (args) => _handler({ ...args, url: new URL(`http://localhost/${args.path}`) });
3017+
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
30033018
});
30043019

30053020
it('works with name mapping', async () => {
@@ -3090,14 +3105,14 @@ describe('REST server tests', () => {
30903105
beforeEach(async () => {
30913106
client = await createTestClient(schema);
30923107

3093-
const _handler = makeHandler({
3108+
const _handler = new RestApiHandler({
30943109
schema: client.$schema,
30953110
endpoint: 'http://localhost/api',
30963111
externalIdMapping: {
30973112
User: 'name_source',
30983113
},
30993114
});
3100-
handler = (args) => _handler({ ...args, url: new URL(`http://localhost/${args.path}`) });
3115+
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
31013116
});
31023117

31033118
it('works with id mapping', async () => {

0 commit comments

Comments
 (0)