@@ -567,6 +567,163 @@ describe('REST OpenAPI spec generation - queryOptions', () => {
567567 } ) ;
568568} ) ;
569569
570+ describe ( 'REST OpenAPI spec generation - nestedRoutes' , ( ) => {
571+ let handler : RestApiHandler ;
572+ let spec : any ;
573+
574+ beforeAll ( async ( ) => {
575+ const client = await createTestClient ( schema ) ;
576+ handler = new RestApiHandler ( {
577+ schema : client . $schema ,
578+ endpoint : 'http://localhost/api' ,
579+ nestedRoutes : true ,
580+ } ) ;
581+ spec = await handler . generateSpec ( ) ;
582+ } ) ;
583+
584+ it ( 'does not generate nested single paths when nestedRoutes is false' , async ( ) => {
585+ const client = await createTestClient ( schema ) ;
586+ const plainHandler = new RestApiHandler ( {
587+ schema : client . $schema ,
588+ endpoint : 'http://localhost/api' ,
589+ } ) ;
590+ const plainSpec = await plainHandler . generateSpec ( ) ;
591+ expect ( plainSpec . paths ?. [ '/user/{id}/posts/{childId}' ] ) . toBeUndefined ( ) ;
592+ expect ( plainSpec . paths ?. [ '/post/{id}/comments/{childId}' ] ) . toBeUndefined ( ) ;
593+ // fetch-related path should not have POST on plain handler
594+ expect ( ( plainSpec . paths as any ) [ '/user/{id}/posts' ] ?. post ) . toBeUndefined ( ) ;
595+ // fetch-related path should not have PATCH for to-one on plain handler
596+ expect ( ( plainSpec . paths as any ) [ '/post/{id}/setting' ] ?. patch ) . toBeUndefined ( ) ;
597+ } ) ;
598+
599+ it ( 'generates nested single paths for collection relations' , ( ) => {
600+ // User -> posts (collection)
601+ expect ( spec . paths [ '/user/{id}/posts/{childId}' ] ) . toBeDefined ( ) ;
602+ // Post -> comments (collection)
603+ expect ( spec . paths [ '/post/{id}/comments/{childId}' ] ) . toBeDefined ( ) ;
604+ // User -> likes (collection, compound-ID child: PostLike has @@id([postId, userId]))
605+ expect ( spec . paths [ '/user/{id}/likes/{childId}' ] ) . toBeDefined ( ) ;
606+ } ) ;
607+
608+ it ( 'does not generate nested single paths for to-one relations' , ( ) => {
609+ // Post -> setting (to-one)
610+ expect ( spec . paths [ '/post/{id}/setting/{childId}' ] ) . toBeUndefined ( ) ;
611+ // Post -> author (to-one)
612+ expect ( spec . paths [ '/post/{id}/author/{childId}' ] ) . toBeUndefined ( ) ;
613+ } ) ;
614+
615+ it ( 'nested single path has GET, PATCH, DELETE' , ( ) => {
616+ const path = spec . paths [ '/user/{id}/posts/{childId}' ] ;
617+ expect ( path . get ) . toBeDefined ( ) ;
618+ expect ( path . patch ) . toBeDefined ( ) ;
619+ expect ( path . delete ) . toBeDefined ( ) ;
620+ } ) ;
621+
622+ it ( 'nested single path GET returns single resource response' , ( ) => {
623+ const getOp = spec . paths [ '/user/{id}/posts/{childId}' ] . get ;
624+ const schema = getOp . responses [ '200' ] . content [ 'application/vnd.api+json' ] . schema ;
625+ expect ( schema . $ref ) . toBe ( '#/components/schemas/PostResponse' ) ;
626+ } ) ;
627+
628+ it ( 'nested single path PATCH uses UpdateRequest body' , ( ) => {
629+ const patchOp = spec . paths [ '/user/{id}/posts/{childId}' ] . patch ;
630+ const schema = patchOp . requestBody . content [ 'application/vnd.api+json' ] . schema ;
631+ expect ( schema . $ref ) . toBe ( '#/components/schemas/PostUpdateRequest' ) ;
632+ } ) ;
633+
634+ it ( 'nested single path has childId path parameter' , ( ) => {
635+ const getOp = spec . paths [ '/user/{id}/posts/{childId}' ] . get ;
636+ const params = getOp . parameters ;
637+ const childIdParam = params . find ( ( p : any ) => p . name === 'childId' ) ;
638+ expect ( childIdParam ) . toBeDefined ( ) ;
639+ expect ( childIdParam . in ) . toBe ( 'path' ) ;
640+ expect ( childIdParam . required ) . toBe ( true ) ;
641+ } ) ;
642+
643+ it ( 'fetch-related path has POST for collection relation when nestedRoutes enabled' , ( ) => {
644+ const postsPath = spec . paths [ '/user/{id}/posts' ] ;
645+ expect ( postsPath . get ) . toBeDefined ( ) ;
646+ expect ( postsPath . post ) . toBeDefined ( ) ;
647+ } ) ;
648+
649+ it ( 'fetch-related POST uses CreateRequest body' , ( ) => {
650+ const postOp = spec . paths [ '/user/{id}/posts' ] . post ;
651+ const schema = postOp . requestBody . content [ 'application/vnd.api+json' ] . schema ;
652+ expect ( schema . $ref ) . toBe ( '#/components/schemas/PostCreateRequest' ) ;
653+ } ) ;
654+
655+ it ( 'fetch-related POST returns 201 with resource response' , ( ) => {
656+ const postOp = spec . paths [ '/user/{id}/posts' ] . post ;
657+ const schema = postOp . responses [ '201' ] . content [ 'application/vnd.api+json' ] . schema ;
658+ expect ( schema . $ref ) . toBe ( '#/components/schemas/PostResponse' ) ;
659+ } ) ;
660+
661+ it ( 'fetch-related path has PATCH for to-one relation when nestedRoutes enabled' , ( ) => {
662+ // Post -> setting is to-one
663+ const settingPath = spec . paths [ '/post/{id}/setting' ] ;
664+ expect ( settingPath . get ) . toBeDefined ( ) ;
665+ expect ( settingPath . patch ) . toBeDefined ( ) ;
666+ // to-one should not get POST (no nested create for to-one)
667+ expect ( settingPath . post ) . toBeUndefined ( ) ;
668+ } ) ;
669+
670+ it ( 'fetch-related PATCH for to-one uses UpdateRequest body' , ( ) => {
671+ const patchOp = spec . paths [ '/post/{id}/setting' ] . patch ;
672+ const schema = patchOp . requestBody . content [ 'application/vnd.api+json' ] . schema ;
673+ expect ( schema . $ref ) . toBe ( '#/components/schemas/SettingUpdateRequest' ) ;
674+ } ) ;
675+
676+ it ( 'fetch-related path does not have PATCH for to-many (collection) relation' , ( ) => {
677+ // User -> posts is a to-many relation; PATCH should only be generated for to-one
678+ const postsPath = spec . paths [ '/user/{id}/posts' ] ;
679+ expect ( postsPath . patch ) . toBeUndefined ( ) ;
680+ } ) ;
681+
682+ it ( 'spec passes OpenAPI 3.1 validation' , async ( ) => {
683+ // Deep clone to avoid validate() mutating $ref strings in the shared spec object
684+ await validate ( JSON . parse ( JSON . stringify ( spec ) ) ) ;
685+ } ) ;
686+
687+ it ( 'operationIds are unique for nested paths' , ( ) => {
688+ const allOperationIds : string [ ] = [ ] ;
689+ for ( const pathItem of Object . values ( spec . paths as Record < string , any > ) ) {
690+ for ( const method of [ 'get' , 'post' , 'patch' , 'put' , 'delete' ] ) {
691+ if ( pathItem [ method ] ?. operationId ) {
692+ allOperationIds . push ( pathItem [ method ] . operationId ) ;
693+ }
694+ }
695+ }
696+ const unique = new Set ( allOperationIds ) ;
697+ expect ( unique . size ) . toBe ( allOperationIds . length ) ;
698+ } ) ;
699+
700+ it ( 'nestedRoutes respects queryOptions slicing excludedOperations' , async ( ) => {
701+ const client = await createTestClient ( schema ) ;
702+ const slicedHandler = new RestApiHandler ( {
703+ schema : client . $schema ,
704+ endpoint : 'http://localhost/api' ,
705+ nestedRoutes : true ,
706+ queryOptions : {
707+ slicing : {
708+ models : {
709+ post : { excludedOperations : [ 'create' , 'delete' , 'update' ] } ,
710+ } ,
711+ } as any ,
712+ } ,
713+ } ) ;
714+ const s = await slicedHandler . generateSpec ( ) ;
715+
716+ // Nested create (POST /user/{id}/posts) should be absent
717+ expect ( ( s . paths as any ) [ '/user/{id}/posts' ] ?. post ) . toBeUndefined ( ) ;
718+ // Nested single GET should still exist (findUnique not excluded)
719+ expect ( ( s . paths as any ) [ '/user/{id}/posts/{childId}' ] ?. get ) . toBeDefined ( ) ;
720+ // Nested single DELETE should be absent
721+ expect ( ( s . paths as any ) [ '/user/{id}/posts/{childId}' ] ?. delete ) . toBeUndefined ( ) ;
722+ // Nested single PATCH (update) should be absent
723+ expect ( ( s . paths as any ) [ '/user/{id}/posts/{childId}' ] ?. patch ) . toBeUndefined ( ) ;
724+ } ) ;
725+ } ) ;
726+
570727describe ( 'REST OpenAPI spec generation - @meta description' , ( ) => {
571728 const metaSchema = `
572729model User {
0 commit comments