@@ -66,26 +66,13 @@ export type RestApiHandlerOptions<Schema extends SchemaDef = SchemaDef> = {
6666 externalIdMapping ?: Record < string , string > ;
6767
6868 /**
69- * Explicit nested route configuration.
69+ * When `true`, enables nested route handling for all to-many relations:
70+ * `/:parentType/:parentId/:relationName` (collection) and
71+ * `/:parentType/:parentId/:relationName/:childId` (single).
7072 *
71- * First-level keys are parent model names, second-level keys are relation field names on the parent model
72- * (e.g., `posts` for `User.posts`). This matches the URL segment used in nested routes:
73- * `/:parentType/:parentId/:relationName` and `/:parentType/:parentId/:relationName/:childId`.
73+ * Defaults to `false`.
7474 */
75- nestedRoutes ?: Record <
76- string ,
77- Record <
78- string ,
79- {
80- /**
81- * When `true`, the constructor throws if the configured relation does not have an `onDelete`
82- * action of `Cascade`, `Restrict`, or `NoAction` in the schema. This ensures the database
83- * prevents orphaned child records when a parent is deleted.
84- */
85- requireOrphanProtection ?: boolean ;
86- }
87- >
88- > ;
75+ nestedRoutes ?: boolean ;
8976} ;
9077
9178type RelationshipInfo = {
@@ -286,7 +273,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
286273 private modelNameMapping : Record < string , string > ;
287274 private reverseModelNameMapping : Record < string , string > ;
288275 private externalIdMapping : Record < string , string > ;
289- private nestedRoutes : Record < string , Record < string , { requireOrphanProtection ?: boolean } > > ;
276+ private nestedRoutes : boolean ;
290277
291278 constructor ( private readonly options : RestApiHandlerOptions < Schema > ) {
292279 this . validateOptions ( options ) ;
@@ -307,20 +294,11 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
307294 Object . entries ( this . externalIdMapping ) . map ( ( [ k , v ] ) => [ lowerCaseFirst ( k ) , v ] ) ,
308295 ) ;
309296
310- this . nestedRoutes = options . nestedRoutes ?? { } ;
311- this . nestedRoutes = Object . fromEntries (
312- Object . entries ( this . nestedRoutes ) . map ( ( [ parentModel , children ] ) => [
313- lowerCaseFirst ( parentModel ) ,
314- Object . fromEntries (
315- Object . entries ( children ) . map ( ( [ childModel , config ] ) => [ lowerCaseFirst ( childModel ) , config ] ) ,
316- ) ,
317- ] ) ,
318- ) ;
297+ this . nestedRoutes = options . nestedRoutes ?? false ;
319298
320299 this . urlPatternMap = this . buildUrlPatternMap ( segmentCharset ) ;
321300
322301 this . buildTypeMap ( ) ;
323- this . validateNestedRoutes ( ) ;
324302 this . buildSerializers ( ) ;
325303 }
326304
@@ -334,55 +312,14 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
334312 urlSegmentCharset : z . string ( ) . min ( 1 ) . optional ( ) ,
335313 modelNameMapping : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) ,
336314 externalIdMapping : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) ,
337- nestedRoutes : z
338- . record ( z . string ( ) , z . record ( z . string ( ) , z . object ( { requireOrphanProtection : z . boolean ( ) . optional ( ) } ) ) )
339- . optional ( ) ,
315+ nestedRoutes : z . boolean ( ) . optional ( ) ,
340316 } ) ;
341317 const parseResult = schema . safeParse ( options ) ;
342318 if ( ! parseResult . success ) {
343319 throw new Error ( `Invalid options: ${ fromError ( parseResult . error ) } ` ) ;
344320 }
345321 }
346322
347- private validateNestedRoutes ( ) {
348- for ( const [ parentModel , relations ] of Object . entries ( this . nestedRoutes ) ) {
349- const parentInfo = this . getModelInfo ( parentModel ) ;
350- if ( ! parentInfo ) {
351- throw new Error ( `Invalid nestedRoutes: parent model "${ parentModel } " not found in schema` ) ;
352- }
353- for ( const [ relationName , config ] of Object . entries ( relations ) ) {
354- const parentField : FieldDef | undefined = this . schema . models [ parentInfo . name ] ?. fields [ relationName ] ;
355- if ( ! parentField ?. relation ) {
356- throw new Error (
357- `Invalid nestedRoutes: relation "${ relationName } " not found on parent model "${ parentModel } "` ,
358- ) ;
359- }
360- const reverseRelation = parentField . relation . opposite ;
361- if ( ! reverseRelation ) {
362- throw new Error (
363- `Invalid nestedRoutes: relation "${ parentModel } .${ relationName } " has no opposite relation defined` ,
364- ) ;
365- }
366- if ( ! parentField . array ) {
367- throw new Error (
368- `Invalid nestedRoutes: relation "${ parentModel } .${ relationName } " is a to-one relation — nested routes only support to-many relations` ,
369- ) ;
370- }
371- if ( config . requireOrphanProtection ) {
372- const childModelName = parentField . type ;
373- const onDelete = this . schema . models [ childModelName ] ?. fields [ reverseRelation ] ?. relation ?. onDelete ;
374- const safeActions = [ 'Cascade' , 'Restrict' , 'NoAction' ] ;
375- if ( ! onDelete || ! safeActions . includes ( onDelete ) ) {
376- throw new Error (
377- `Invalid nestedRoutes: requireOrphanProtection is enabled for "${ parentModel } .${ relationName } " ` +
378- `but its onDelete action is "${ onDelete ?? 'not set' } " — must be Cascade, Restrict, or NoAction` ,
379- ) ;
380- }
381- }
382- }
383- }
384- }
385-
386323 get schema ( ) {
387324 return this . options . schema ;
388325 }
@@ -417,10 +354,6 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
417354 return this . modelNameMapping [ modelName ] ?? modelName ;
418355 }
419356
420- private getNestedRouteConfig ( parentType : string , parentRelation : string ) {
421- return this . nestedRoutes [ lowerCaseFirst ( parentType ) ] ?. [ parentRelation ] ;
422- }
423-
424357 /**
425358 * Resolves child model type and reverse relation from a parent relation name.
426359 * e.g. given parentType='user', parentRelation='posts', returns { childType:'post', reverseRelation:'author' }
@@ -548,7 +481,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
548481
549482 // /:type/:id/:relationship/:childId — nested single read
550483 match = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
551- if ( match && this . getNestedRouteConfig ( match . type , match . relationship ) ) {
484+ if ( match && this . nestedRoutes && this . resolveNestedRelation ( match . type , match . relationship ) ?. isCollection ) {
552485 return await this . processNestedSingleRead (
553486 client ,
554487 match . type ,
@@ -573,7 +506,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
573506 }
574507 // /:type/:id/:relationship — nested create
575508 const nestedMatch = this . matchUrlPattern ( path , UrlPatterns . FETCH_RELATIONSHIP ) ;
576- if ( nestedMatch && this . getNestedRouteConfig ( nestedMatch . type , nestedMatch . relationship ) ) {
509+ if ( nestedMatch && this . nestedRoutes && this . resolveNestedRelation ( nestedMatch . type , nestedMatch . relationship ) ?. isCollection ) {
577510 return await this . processNestedCreate (
578511 client ,
579512 nestedMatch . type ,
@@ -636,7 +569,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
636569 const nestedPatchMatch = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
637570 if (
638571 nestedPatchMatch &&
639- this . getNestedRouteConfig ( nestedPatchMatch . type , nestedPatchMatch . relationship )
572+ this . nestedRoutes &&
573+ this . resolveNestedRelation ( nestedPatchMatch . type , nestedPatchMatch . relationship ) ?. isCollection
640574 ) {
641575 return await this . processNestedUpdate (
642576 client ,
@@ -675,7 +609,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
675609 const nestedDeleteMatch = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
676610 if (
677611 nestedDeleteMatch &&
678- this . getNestedRouteConfig ( nestedDeleteMatch . type , nestedDeleteMatch . relationship )
612+ this . nestedRoutes &&
613+ this . resolveNestedRelation ( nestedDeleteMatch . type , nestedDeleteMatch . relationship ) ?. isCollection
679614 ) {
680615 return await this . processNestedDelete (
681616 client ,
0 commit comments