@@ -68,26 +68,13 @@ export type RestApiHandlerOptions<Schema extends SchemaDef = SchemaDef> = {
6868 externalIdMapping ?: Record < string , string > ;
6969
7070 /**
71- * Explicit nested route configuration.
71+ * When `true`, enables nested route handling for all to-many relations:
72+ * `/:parentType/:parentId/:relationName` (collection) and
73+ * `/:parentType/:parentId/:relationName/:childId` (single).
7274 *
73- * First-level keys are parent model names, second-level keys are relation field names on the parent model
74- * (e.g., `posts` for `User.posts`). This matches the URL segment used in nested routes:
75- * `/:parentType/:parentId/:relationName` and `/:parentType/:parentId/:relationName/:childId`.
75+ * Defaults to `false`.
7676 */
77- nestedRoutes ?: Record <
78- string ,
79- Record <
80- string ,
81- {
82- /**
83- * When `true`, the constructor throws if the configured relation does not have an `onDelete`
84- * action of `Cascade`, `Restrict`, or `NoAction` in the schema. This ensures the database
85- * prevents orphaned child records when a parent is deleted.
86- */
87- requireOrphanProtection ?: boolean ;
88- }
89- >
90- > ;
77+ nestedRoutes ?: boolean ;
9178} & CommonHandlerOptions < Schema > ;
9279
9380type RelationshipInfo = {
@@ -288,7 +275,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
288275 private modelNameMapping : Record < string , string > ;
289276 private reverseModelNameMapping : Record < string , string > ;
290277 private externalIdMapping : Record < string , string > ;
291- private nestedRoutes : Record < string , Record < string , { requireOrphanProtection ?: boolean } > > ;
278+ private nestedRoutes : boolean ;
292279
293280 constructor ( private readonly options : RestApiHandlerOptions < Schema > ) {
294281 this . validateOptions ( options ) ;
@@ -309,20 +296,11 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
309296 Object . entries ( this . externalIdMapping ) . map ( ( [ k , v ] ) => [ lowerCaseFirst ( k ) , v ] ) ,
310297 ) ;
311298
312- this . nestedRoutes = options . nestedRoutes ?? { } ;
313- this . nestedRoutes = Object . fromEntries (
314- Object . entries ( this . nestedRoutes ) . map ( ( [ parentModel , children ] ) => [
315- lowerCaseFirst ( parentModel ) ,
316- Object . fromEntries (
317- Object . entries ( children ) . map ( ( [ childModel , config ] ) => [ lowerCaseFirst ( childModel ) , config ] ) ,
318- ) ,
319- ] ) ,
320- ) ;
299+ this . nestedRoutes = options . nestedRoutes ?? false ;
321300
322301 this . urlPatternMap = this . buildUrlPatternMap ( segmentCharset ) ;
323302
324303 this . buildTypeMap ( ) ;
325- this . validateNestedRoutes ( ) ;
326304 this . buildSerializers ( ) ;
327305 }
328306
@@ -337,55 +315,14 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
337315 modelNameMapping : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) ,
338316 externalIdMapping : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) ,
339317 queryOptions : queryOptionsSchema . optional ( ) ,
340- nestedRoutes : z
341- . record ( z . string ( ) , z . record ( z . string ( ) , z . object ( { requireOrphanProtection : z . boolean ( ) . optional ( ) } ) ) )
342- . optional ( ) ,
318+ nestedRoutes : z . boolean ( ) . optional ( ) ,
343319 } ) ;
344320 const parseResult = schema . safeParse ( options ) ;
345321 if ( ! parseResult . success ) {
346322 throw new Error ( `Invalid options: ${ fromError ( parseResult . error ) } ` ) ;
347323 }
348324 }
349325
350- private validateNestedRoutes ( ) {
351- for ( const [ parentModel , relations ] of Object . entries ( this . nestedRoutes ) ) {
352- const parentInfo = this . getModelInfo ( parentModel ) ;
353- if ( ! parentInfo ) {
354- throw new Error ( `Invalid nestedRoutes: parent model "${ parentModel } " not found in schema` ) ;
355- }
356- for ( const [ relationName , config ] of Object . entries ( relations ) ) {
357- const parentField : FieldDef | undefined = this . schema . models [ parentInfo . name ] ?. fields [ relationName ] ;
358- if ( ! parentField ?. relation ) {
359- throw new Error (
360- `Invalid nestedRoutes: relation "${ relationName } " not found on parent model "${ parentModel } "` ,
361- ) ;
362- }
363- const reverseRelation = parentField . relation . opposite ;
364- if ( ! reverseRelation ) {
365- throw new Error (
366- `Invalid nestedRoutes: relation "${ parentModel } .${ relationName } " has no opposite relation defined` ,
367- ) ;
368- }
369- if ( ! parentField . array ) {
370- throw new Error (
371- `Invalid nestedRoutes: relation "${ parentModel } .${ relationName } " is a to-one relation — nested routes only support to-many relations` ,
372- ) ;
373- }
374- if ( config . requireOrphanProtection ) {
375- const childModelName = parentField . type ;
376- const onDelete = this . schema . models [ childModelName ] ?. fields [ reverseRelation ] ?. relation ?. onDelete ;
377- const safeActions = [ 'Cascade' , 'Restrict' , 'NoAction' ] ;
378- if ( ! onDelete || ! safeActions . includes ( onDelete ) ) {
379- throw new Error (
380- `Invalid nestedRoutes: requireOrphanProtection is enabled for "${ parentModel } .${ relationName } " ` +
381- `but its onDelete action is "${ onDelete ?? 'not set' } " — must be Cascade, Restrict, or NoAction` ,
382- ) ;
383- }
384- }
385- }
386- }
387- }
388-
389326 get schema ( ) {
390327 return this . options . schema ;
391328 }
@@ -420,10 +357,6 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
420357 return this . modelNameMapping [ modelName ] ?? modelName ;
421358 }
422359
423- private getNestedRouteConfig ( parentType : string , parentRelation : string ) {
424- return this . nestedRoutes [ lowerCaseFirst ( parentType ) ] ?. [ parentRelation ] ;
425- }
426-
427360 /**
428361 * Resolves child model type and reverse relation from a parent relation name.
429362 * e.g. given parentType='user', parentRelation='posts', returns { childType:'post', reverseRelation:'author' }
@@ -551,7 +484,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
551484
552485 // /:type/:id/:relationship/:childId — nested single read
553486 match = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
554- if ( match && this . getNestedRouteConfig ( match . type , match . relationship ) ) {
487+ if ( match && this . nestedRoutes && this . resolveNestedRelation ( match . type , match . relationship ) ?. isCollection ) {
555488 return await this . processNestedSingleRead (
556489 client ,
557490 match . type ,
@@ -576,7 +509,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
576509 }
577510 // /:type/:id/:relationship — nested create
578511 const nestedMatch = this . matchUrlPattern ( path , UrlPatterns . FETCH_RELATIONSHIP ) ;
579- if ( nestedMatch && this . getNestedRouteConfig ( nestedMatch . type , nestedMatch . relationship ) ) {
512+ if ( nestedMatch && this . nestedRoutes && this . resolveNestedRelation ( nestedMatch . type , nestedMatch . relationship ) ?. isCollection ) {
580513 return await this . processNestedCreate (
581514 client ,
582515 nestedMatch . type ,
@@ -639,7 +572,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
639572 const nestedPatchMatch = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
640573 if (
641574 nestedPatchMatch &&
642- this . getNestedRouteConfig ( nestedPatchMatch . type , nestedPatchMatch . relationship )
575+ this . nestedRoutes &&
576+ this . resolveNestedRelation ( nestedPatchMatch . type , nestedPatchMatch . relationship ) ?. isCollection
643577 ) {
644578 return await this . processNestedUpdate (
645579 client ,
@@ -678,7 +612,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
678612 const nestedDeleteMatch = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
679613 if (
680614 nestedDeleteMatch &&
681- this . getNestedRouteConfig ( nestedDeleteMatch . type , nestedDeleteMatch . relationship )
615+ this . nestedRoutes &&
616+ this . resolveNestedRelation ( nestedDeleteMatch . type , nestedDeleteMatch . relationship ) ?. isCollection
682617 ) {
683618 return await this . processNestedDelete (
684619 client ,
0 commit comments