@@ -22,19 +22,6 @@ import { LoggerConfig, Response } from '../../types';
22
22
import { APIHandlerBase , RequestContext } from '../base' ;
23
23
import { logWarning , registerCustomSerializers } from '../utils' ;
24
24
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
-
36
- export const idDivider = '_' ;
37
-
38
25
/**
39
26
* Request handler options
40
27
*/
@@ -52,6 +39,19 @@ export type Options = {
52
39
* Defaults to 100. Set to Infinity to disable pagination.
53
40
*/
54
41
pageSize ?: number ;
42
+
43
+ /**
44
+ * The divider used to separate compound ID fields in the URL.
45
+ * Defaults to '_'.
46
+ */
47
+ idDivider ?: string ;
48
+
49
+ /**
50
+ * The charset used for URL segment values. Defaults to `a-zA-Z0-9-_~ %`. You can change it if your entity's ID values
51
+ * allow different characters. Specifically, if your models use compound IDs and the idDivider is set to a different value,
52
+ * it should be included in the charset.
53
+ */
54
+ urlSegmentCharset ?: string ;
55
55
} ;
56
56
57
57
type RelationshipInfo = {
@@ -93,6 +93,8 @@ const FilterOperations = [
93
93
94
94
type FilterOperationType = ( typeof FilterOperations ) [ number ] | undefined ;
95
95
96
+ const prismaIdDivider = '_' ;
97
+
96
98
registerCustomSerializers ( ) ;
97
99
98
100
/**
@@ -210,8 +212,30 @@ class RequestHandler extends APIHandlerBase {
210
212
// all known types and their metadata
211
213
private typeMap : Record < string , ModelInfo > ;
212
214
215
+ // divider used to separate compound ID fields
216
+ private idDivider ;
217
+
218
+ private urlPatterns ;
219
+
213
220
constructor ( private readonly options : Options ) {
214
221
super ( ) ;
222
+ this . idDivider = options . idDivider ?? prismaIdDivider ;
223
+ const segmentCharset = options . urlSegmentCharset ?? 'a-zA-Z0-9-_~ %' ;
224
+ this . urlPatterns = this . buildUrlPatterns ( this . idDivider , segmentCharset ) ;
225
+ }
226
+
227
+ buildUrlPatterns ( idDivider : string , urlSegmentNameCharset : string ) {
228
+ const options = { segmentValueCharset : urlSegmentNameCharset } ;
229
+ return {
230
+ // collection operations
231
+ collection : new UrlPattern ( '/:type' , options ) ,
232
+ // single resource operations
233
+ single : new UrlPattern ( '/:type/:id' , options ) ,
234
+ // related entity fetching
235
+ fetchRelationship : new UrlPattern ( '/:type/:id/:relationship' , options ) ,
236
+ // relationship operations
237
+ relationship : new UrlPattern ( '/:type/:id/relationships/:relationship' , options ) ,
238
+ } ;
215
239
}
216
240
217
241
async handleRequest ( {
@@ -245,19 +269,19 @@ class RequestHandler extends APIHandlerBase {
245
269
try {
246
270
switch ( method ) {
247
271
case 'GET' : {
248
- let match = urlPatterns . single . match ( path ) ;
272
+ let match = this . urlPatterns . single . match ( path ) ;
249
273
if ( match ) {
250
274
// single resource read
251
275
return await this . processSingleRead ( prisma , match . type , match . id , query ) ;
252
276
}
253
277
254
- match = urlPatterns . fetchRelationship . match ( path ) ;
278
+ match = this . urlPatterns . fetchRelationship . match ( path ) ;
255
279
if ( match ) {
256
280
// fetch related resource(s)
257
281
return await this . processFetchRelated ( prisma , match . type , match . id , match . relationship , query ) ;
258
282
}
259
283
260
- match = urlPatterns . relationship . match ( path ) ;
284
+ match = this . urlPatterns . relationship . match ( path ) ;
261
285
if ( match ) {
262
286
// read relationship
263
287
return await this . processReadRelationship (
@@ -269,7 +293,7 @@ class RequestHandler extends APIHandlerBase {
269
293
) ;
270
294
}
271
295
272
- match = urlPatterns . collection . match ( path ) ;
296
+ match = this . urlPatterns . collection . match ( path ) ;
273
297
if ( match ) {
274
298
// collection read
275
299
return await this . processCollectionRead ( prisma , match . type , query ) ;
@@ -283,13 +307,13 @@ class RequestHandler extends APIHandlerBase {
283
307
return this . makeError ( 'invalidPayload' ) ;
284
308
}
285
309
286
- let match = urlPatterns . collection . match ( path ) ;
310
+ let match = this . urlPatterns . collection . match ( path ) ;
287
311
if ( match ) {
288
312
// resource creation
289
313
return await this . processCreate ( prisma , match . type , query , requestBody , modelMeta , zodSchemas ) ;
290
314
}
291
315
292
- match = urlPatterns . relationship . match ( path ) ;
316
+ match = this . urlPatterns . relationship . match ( path ) ;
293
317
if ( match ) {
294
318
// relationship creation (collection relationship only)
295
319
return await this . processRelationshipCRUD (
@@ -313,7 +337,7 @@ class RequestHandler extends APIHandlerBase {
313
337
return this . makeError ( 'invalidPayload' ) ;
314
338
}
315
339
316
- let match = urlPatterns . single . match ( path ) ;
340
+ let match = this . urlPatterns . single . match ( path ) ;
317
341
if ( match ) {
318
342
// resource update
319
343
return await this . processUpdate (
@@ -327,7 +351,7 @@ class RequestHandler extends APIHandlerBase {
327
351
) ;
328
352
}
329
353
330
- match = urlPatterns . relationship . match ( path ) ;
354
+ match = this . urlPatterns . relationship . match ( path ) ;
331
355
if ( match ) {
332
356
// relationship update
333
357
return await this . processRelationshipCRUD (
@@ -345,13 +369,13 @@ class RequestHandler extends APIHandlerBase {
345
369
}
346
370
347
371
case 'DELETE' : {
348
- let match = urlPatterns . single . match ( path ) ;
372
+ let match = this . urlPatterns . single . match ( path ) ;
349
373
if ( match ) {
350
374
// resource deletion
351
375
return await this . processDelete ( prisma , match . type , match . id ) ;
352
376
}
353
377
354
- match = urlPatterns . relationship . match ( path ) ;
378
+ match = this . urlPatterns . relationship . match ( path ) ;
355
379
if ( match ) {
356
380
// relationship deletion (collection relationship only)
357
381
return await this . processRelationshipCRUD (
@@ -391,7 +415,7 @@ class RequestHandler extends APIHandlerBase {
391
415
return this . makeUnsupportedModelError ( type ) ;
392
416
}
393
417
394
- const args : any = { where : this . makeIdFilter ( typeInfo . idFields , resourceId ) } ;
418
+ const args : any = { where : this . makePrismaIdFilter ( typeInfo . idFields , resourceId ) } ;
395
419
396
420
// include IDs of relation fields so that they can be serialized
397
421
this . includeRelationshipIds ( type , args , 'include' ) ;
@@ -456,7 +480,7 @@ class RequestHandler extends APIHandlerBase {
456
480
457
481
select = select ?? { [ relationship ] : true } ;
458
482
const args : any = {
459
- where : this . makeIdFilter ( typeInfo . idFields , resourceId ) ,
483
+ where : this . makePrismaIdFilter ( typeInfo . idFields , resourceId ) ,
460
484
select,
461
485
} ;
462
486
@@ -514,7 +538,7 @@ class RequestHandler extends APIHandlerBase {
514
538
}
515
539
516
540
const args : any = {
517
- where : this . makeIdFilter ( typeInfo . idFields , resourceId ) ,
541
+ where : this . makePrismaIdFilter ( typeInfo . idFields , resourceId ) ,
518
542
select : this . makeIdSelect ( typeInfo . idFields ) ,
519
543
} ;
520
544
@@ -753,7 +777,7 @@ class RequestHandler extends APIHandlerBase {
753
777
if ( relationInfo . isCollection ) {
754
778
createPayload . data [ key ] = {
755
779
connect : enumerate ( data . data ) . map ( ( item : any ) => ( {
756
- [ this . makeIdKey ( relationInfo . idFields ) ] : item . id ,
780
+ [ this . makePrismaIdKey ( relationInfo . idFields ) ] : item . id ,
757
781
} ) ) ,
758
782
} ;
759
783
} else {
@@ -762,15 +786,15 @@ class RequestHandler extends APIHandlerBase {
762
786
}
763
787
createPayload . data [ key ] = {
764
788
connect : {
765
- [ this . makeIdKey ( relationInfo . idFields ) ] : data . data . id ,
789
+ [ this . makePrismaIdKey ( relationInfo . idFields ) ] : data . data . id ,
766
790
} ,
767
791
} ;
768
792
}
769
793
770
794
// make sure ID fields are included for result serialization
771
795
createPayload . include = {
772
796
...createPayload . include ,
773
- [ key ] : { select : { [ this . makeIdKey ( relationInfo . idFields ) ] : true } } ,
797
+ [ key ] : { select : { [ this . makePrismaIdKey ( relationInfo . idFields ) ] : true } } ,
774
798
} ;
775
799
}
776
800
}
@@ -807,7 +831,7 @@ class RequestHandler extends APIHandlerBase {
807
831
}
808
832
809
833
const updateArgs : any = {
810
- where : this . makeIdFilter ( typeInfo . idFields , resourceId ) ,
834
+ where : this . makePrismaIdFilter ( typeInfo . idFields , resourceId ) ,
811
835
select : {
812
836
...typeInfo . idFields . reduce ( ( acc , field ) => ( { ...acc , [ field . name ] : true } ) , { } ) ,
813
837
[ relationship ] : { select : this . makeIdSelect ( relationInfo . idFields ) } ,
@@ -842,7 +866,7 @@ class RequestHandler extends APIHandlerBase {
842
866
updateArgs . data = {
843
867
[ relationship ] : {
844
868
connect : {
845
- [ this . makeIdKey ( relationInfo . idFields ) ] : parsed . data . data . id ,
869
+ [ this . makePrismaIdKey ( relationInfo . idFields ) ] : parsed . data . data . id ,
846
870
} ,
847
871
} ,
848
872
} ;
@@ -866,7 +890,7 @@ class RequestHandler extends APIHandlerBase {
866
890
updateArgs . data = {
867
891
[ relationship ] : {
868
892
[ relationVerb ] : enumerate ( parsed . data . data ) . map ( ( item : any ) =>
869
- this . makeIdFilter ( relationInfo . idFields , item . id )
893
+ this . makePrismaIdFilter ( relationInfo . idFields , item . id )
870
894
) ,
871
895
} ,
872
896
} ;
@@ -907,7 +931,7 @@ class RequestHandler extends APIHandlerBase {
907
931
}
908
932
909
933
const updatePayload : any = {
910
- where : this . makeIdFilter ( typeInfo . idFields , resourceId ) ,
934
+ where : this . makePrismaIdFilter ( typeInfo . idFields , resourceId ) ,
911
935
data : { ...attributes } ,
912
936
} ;
913
937
@@ -926,7 +950,7 @@ class RequestHandler extends APIHandlerBase {
926
950
if ( relationInfo . isCollection ) {
927
951
updatePayload . data [ key ] = {
928
952
set : enumerate ( data . data ) . map ( ( item : any ) => ( {
929
- [ this . makeIdKey ( relationInfo . idFields ) ] : item . id ,
953
+ [ this . makePrismaIdKey ( relationInfo . idFields ) ] : item . id ,
930
954
} ) ) ,
931
955
} ;
932
956
} else {
@@ -935,13 +959,13 @@ class RequestHandler extends APIHandlerBase {
935
959
}
936
960
updatePayload . data [ key ] = {
937
961
set : {
938
- [ this . makeIdKey ( relationInfo . idFields ) ] : data . data . id ,
962
+ [ this . makePrismaIdKey ( relationInfo . idFields ) ] : data . data . id ,
939
963
} ,
940
964
} ;
941
965
}
942
966
updatePayload . include = {
943
967
...updatePayload . include ,
944
- [ key ] : { select : { [ this . makeIdKey ( relationInfo . idFields ) ] : true } } ,
968
+ [ key ] : { select : { [ this . makePrismaIdKey ( relationInfo . idFields ) ] : true } } ,
945
969
} ;
946
970
}
947
971
}
@@ -960,7 +984,7 @@ class RequestHandler extends APIHandlerBase {
960
984
}
961
985
962
986
await prisma [ type ] . delete ( {
963
- where : this . makeIdFilter ( typeInfo . idFields , resourceId ) ,
987
+ where : this . makePrismaIdFilter ( typeInfo . idFields , resourceId ) ,
964
988
} ) ;
965
989
return {
966
990
status : 204 ,
@@ -1110,7 +1134,7 @@ class RequestHandler extends APIHandlerBase {
1110
1134
if ( ids . length === 0 ) {
1111
1135
return undefined ;
1112
1136
} else {
1113
- return data [ ids . map ( ( id ) => id . name ) . join ( idDivider ) ] ;
1137
+ return data [ this . makeIdKey ( ids ) ] ;
1114
1138
}
1115
1139
}
1116
1140
@@ -1206,15 +1230,16 @@ class RequestHandler extends APIHandlerBase {
1206
1230
return r . toString ( ) ;
1207
1231
}
1208
1232
1209
- private makeIdFilter ( idFields : FieldInfo [ ] , resourceId : string ) {
1233
+ private makePrismaIdFilter ( idFields : FieldInfo [ ] , resourceId : string ) {
1210
1234
if ( idFields . length === 1 ) {
1211
1235
return { [ idFields [ 0 ] . name ] : this . coerce ( idFields [ 0 ] . type , resourceId ) } ;
1212
1236
} else {
1213
1237
return {
1214
- [ idFields . map ( ( idf ) => idf . name ) . join ( idDivider ) ] : idFields . reduce (
1238
+ // TODO: support `@@id` with custom name
1239
+ [ idFields . map ( ( idf ) => idf . name ) . join ( prismaIdDivider ) ] : idFields . reduce (
1215
1240
( acc , curr , idx ) => ( {
1216
1241
...acc ,
1217
- [ curr . name ] : this . coerce ( curr . type , resourceId . split ( idDivider ) [ idx ] ) ,
1242
+ [ curr . name ] : this . coerce ( curr . type , resourceId . split ( this . idDivider ) [ idx ] ) ,
1218
1243
} ) ,
1219
1244
{ }
1220
1245
) ,
@@ -1230,11 +1255,16 @@ class RequestHandler extends APIHandlerBase {
1230
1255
}
1231
1256
1232
1257
private makeIdKey ( idFields : FieldInfo [ ] ) {
1233
- return idFields . map ( ( idf ) => idf . name ) . join ( idDivider ) ;
1258
+ return idFields . map ( ( idf ) => idf . name ) . join ( this . idDivider ) ;
1259
+ }
1260
+
1261
+ private makePrismaIdKey ( idFields : FieldInfo [ ] ) {
1262
+ // TODO: support `@@id` with custom name
1263
+ return idFields . map ( ( idf ) => idf . name ) . join ( prismaIdDivider ) ;
1234
1264
}
1235
1265
1236
1266
private makeCompoundId ( idFields : FieldInfo [ ] , item : any ) {
1237
- return idFields . map ( ( idf ) => item [ idf . name ] ) . join ( idDivider ) ;
1267
+ return idFields . map ( ( idf ) => item [ idf . name ] ) . join ( this . idDivider ) ;
1238
1268
}
1239
1269
1240
1270
private includeRelationshipIds ( model : string , args : any , mode : 'select' | 'include' ) {
@@ -1557,11 +1587,11 @@ class RequestHandler extends APIHandlerBase {
1557
1587
const values = value . split ( ',' ) . filter ( ( i ) => i ) ;
1558
1588
const filterValue =
1559
1589
values . length > 1
1560
- ? { OR : values . map ( ( v ) => this . makeIdFilter ( info . idFields , v ) ) }
1561
- : this . makeIdFilter ( info . idFields , value ) ;
1590
+ ? { OR : values . map ( ( v ) => this . makePrismaIdFilter ( info . idFields , v ) ) }
1591
+ : this . makePrismaIdFilter ( info . idFields , value ) ;
1562
1592
return { some : filterValue } ;
1563
1593
} else {
1564
- return { is : this . makeIdFilter ( info . idFields , value ) } ;
1594
+ return { is : this . makePrismaIdFilter ( info . idFields , value ) } ;
1565
1595
}
1566
1596
} else {
1567
1597
const coerced = this . coerce ( fieldInfo . type , value ) ;
0 commit comments