Skip to content

Commit e127e04

Browse files
Make REST API ID divider configurable
1 parent b0de9c3 commit e127e04

File tree

3 files changed

+127
-28
lines changed

3 files changed

+127
-28
lines changed

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

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,6 @@ import { LoggerConfig, Response } from '../../types';
2222
import { APIHandlerBase, RequestContext } from '../base';
2323
import { logWarning, registerCustomSerializers } from '../utils';
2424

25-
const urlPatterns = {
26-
// collection operations
27-
collection: new UrlPattern('/:type'),
28-
// single resource operations
29-
single: new UrlPattern('/:type/:id'),
30-
// related entity fetching
31-
fetchRelationship: new UrlPattern('/:type/:id/:relationship'),
32-
// relationship operations
33-
relationship: new UrlPattern('/:type/:id/relationships/:relationship'),
34-
};
35-
3625
/**
3726
* Request handler options
3827
*/
@@ -215,9 +204,26 @@ class RequestHandler extends APIHandlerBase {
215204
private typeMap: Record<string, ModelInfo>;
216205
public idDivider;
217206

207+
private urlPatterns;
208+
218209
constructor(private readonly options: Options) {
219210
super();
220211
this.idDivider = options.idDivider ?? '_';
212+
this.urlPatterns = this.buildUrlPatterns(this.idDivider);
213+
}
214+
215+
buildUrlPatterns(idDivider: string) {
216+
const options = { segmentNameCharset: `a-zA-Z0-9-_~ %${idDivider}` };
217+
return {
218+
// collection operations
219+
collection: new UrlPattern('/:type', options),
220+
// single resource operations
221+
single: new UrlPattern('/:type/:id', options),
222+
// related entity fetching
223+
fetchRelationship: new UrlPattern('/:type/:id/:relationship', options),
224+
// relationship operations
225+
relationship: new UrlPattern('/:type/:id/relationships/:relationship', options),
226+
};
221227
}
222228

223229
async handleRequest({
@@ -251,19 +257,19 @@ class RequestHandler extends APIHandlerBase {
251257
try {
252258
switch (method) {
253259
case 'GET': {
254-
let match = urlPatterns.single.match(path);
260+
let match = this.urlPatterns.single.match(path);
255261
if (match) {
256262
// single resource read
257263
return await this.processSingleRead(prisma, match.type, match.id, query);
258264
}
259265

260-
match = urlPatterns.fetchRelationship.match(path);
266+
match = this.urlPatterns.fetchRelationship.match(path);
261267
if (match) {
262268
// fetch related resource(s)
263269
return await this.processFetchRelated(prisma, match.type, match.id, match.relationship, query);
264270
}
265271

266-
match = urlPatterns.relationship.match(path);
272+
match = this.urlPatterns.relationship.match(path);
267273
if (match) {
268274
// read relationship
269275
return await this.processReadRelationship(
@@ -275,7 +281,7 @@ class RequestHandler extends APIHandlerBase {
275281
);
276282
}
277283

278-
match = urlPatterns.collection.match(path);
284+
match = this.urlPatterns.collection.match(path);
279285
if (match) {
280286
// collection read
281287
return await this.processCollectionRead(prisma, match.type, query);
@@ -289,13 +295,13 @@ class RequestHandler extends APIHandlerBase {
289295
return this.makeError('invalidPayload');
290296
}
291297

292-
let match = urlPatterns.collection.match(path);
298+
let match = this.urlPatterns.collection.match(path);
293299
if (match) {
294300
// resource creation
295301
return await this.processCreate(prisma, match.type, query, requestBody, modelMeta, zodSchemas);
296302
}
297303

298-
match = urlPatterns.relationship.match(path);
304+
match = this.urlPatterns.relationship.match(path);
299305
if (match) {
300306
// relationship creation (collection relationship only)
301307
return await this.processRelationshipCRUD(
@@ -319,7 +325,7 @@ class RequestHandler extends APIHandlerBase {
319325
return this.makeError('invalidPayload');
320326
}
321327

322-
let match = urlPatterns.single.match(path);
328+
let match = this.urlPatterns.single.match(path);
323329
if (match) {
324330
// resource update
325331
return await this.processUpdate(
@@ -333,7 +339,7 @@ class RequestHandler extends APIHandlerBase {
333339
);
334340
}
335341

336-
match = urlPatterns.relationship.match(path);
342+
match = this.urlPatterns.relationship.match(path);
337343
if (match) {
338344
// relationship update
339345
return await this.processRelationshipCRUD(
@@ -351,13 +357,13 @@ class RequestHandler extends APIHandlerBase {
351357
}
352358

353359
case 'DELETE': {
354-
let match = urlPatterns.single.match(path);
360+
let match = this.urlPatterns.single.match(path);
355361
if (match) {
356362
// resource deletion
357363
return await this.processDelete(prisma, match.type, match.id);
358364
}
359365

360-
match = urlPatterns.relationship.match(path);
366+
match = this.urlPatterns.relationship.match(path);
361367
if (match) {
362368
// relationship deletion (collection relationship only)
363369
return await this.processRelationshipCRUD(

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

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('REST server tests', () => {
8585
zodSchemas = params.zodSchemas;
8686
modelMeta = params.modelMeta;
8787

88-
const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5, idDivider });
88+
const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5 });
8989
handler = (args) =>
9090
_handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) });
9191
});
@@ -2521,4 +2521,97 @@ describe('REST server tests', () => {
25212521
expect(Buffer.isBuffer(included.attributes.bytes)).toBeTruthy();
25222522
});
25232523
});
2524+
2525+
describe('REST server tests - compound id with custom separator', () => {
2526+
const schema = `
2527+
enum Role {
2528+
COMMON_USER
2529+
ADMIN_USER
2530+
}
2531+
2532+
model User {
2533+
email String
2534+
role Role
2535+
2536+
@@id([email, role])
2537+
}
2538+
`;
2539+
const idDivider = ':';
2540+
2541+
beforeAll(async () => {
2542+
const params = await loadSchema(schema);
2543+
2544+
prisma = params.enhanceRaw(params.prisma, params);
2545+
zodSchemas = params.zodSchemas;
2546+
modelMeta = params.modelMeta;
2547+
2548+
const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5, idDivider });
2549+
handler = (args) =>
2550+
_handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) });
2551+
});
2552+
2553+
it('POST', async () => {
2554+
const r = await handler({
2555+
method: 'post',
2556+
path: '/user',
2557+
query: {},
2558+
requestBody: {
2559+
data: {
2560+
type: 'user',
2561+
attributes: { email: '[email protected]', role: 'COMMON_USER' },
2562+
},
2563+
},
2564+
prisma,
2565+
});
2566+
2567+
expect(r.status).toBe(201);
2568+
});
2569+
2570+
it('GET', async () => {
2571+
await prisma.user.create({
2572+
data: { email: '[email protected]', role: 'COMMON_USER' },
2573+
});
2574+
2575+
const r = await handler({
2576+
method: 'get',
2577+
path: '/user',
2578+
query: {},
2579+
prisma,
2580+
});
2581+
2582+
expect(r.status).toBe(200);
2583+
expect(r.body.data).toHaveLength(1);
2584+
});
2585+
2586+
it('GET single', async () => {
2587+
await prisma.user.create({
2588+
data: { email: '[email protected]', role: 'COMMON_USER' },
2589+
});
2590+
2591+
const r = await handler({
2592+
method: 'get',
2593+
path: '/user/[email protected]:COMMON_USER',
2594+
query: {},
2595+
prisma,
2596+
});
2597+
2598+
expect(r.status).toBe(200);
2599+
expect(r.body.data.attributes.email).toBe('[email protected]');
2600+
});
2601+
2602+
it('PUT', async () => {
2603+
await prisma.user.create({
2604+
data: { email: '[email protected]', role: 'COMMON_USER' },
2605+
});
2606+
2607+
const r = await handler({
2608+
method: 'put',
2609+
path: '/user/[email protected]:COMMON_USER',
2610+
query: {},
2611+
prisma,
2612+
});
2613+
2614+
expect(r.status).toBe(200);
2615+
});
2616+
});
25242617
});

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)