@@ -568,7 +568,26 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
568568 requestBody ,
569569 ) ;
570570 }
571- // /:type/:id/:relationship/:childId — nested update
571+ // /:type/:id/:relationship — nested update (to-one)
572+ const nestedToOnePatchMatch = this . matchUrlPattern ( path , UrlPatterns . FETCH_RELATIONSHIP ) ;
573+ if (
574+ nestedToOnePatchMatch &&
575+ this . nestedRoutes &&
576+ this . resolveNestedRelation ( nestedToOnePatchMatch . type , nestedToOnePatchMatch . relationship ) &&
577+ ! this . resolveNestedRelation ( nestedToOnePatchMatch . type , nestedToOnePatchMatch . relationship )
578+ ?. isCollection
579+ ) {
580+ return await this . processNestedUpdate (
581+ client ,
582+ nestedToOnePatchMatch . type ,
583+ nestedToOnePatchMatch . id ,
584+ nestedToOnePatchMatch . relationship ,
585+ undefined ,
586+ query ,
587+ requestBody ,
588+ ) ;
589+ }
590+ // /:type/:id/:relationship/:childId — nested update (to-many)
572591 const nestedPatchMatch = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
573592 if (
574593 nestedPatchMatch &&
@@ -1228,48 +1247,39 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
12281247 } ;
12291248 }
12301249
1231- private async processNestedUpdate (
1232- client : ClientContract < Schema > ,
1233- parentType : string ,
1234- parentId : string ,
1235- parentRelation : string ,
1236- childId : string ,
1237- _query : Record < string , string | string [ ] > | undefined ,
1250+ /**
1251+ * Builds the ORM `data` payload for a nested update, shared by both to-many (childId present)
1252+ * and to-one (childId absent) variants. Returns either `{ updateData }` or `{ error }`.
1253+ */
1254+ private buildNestedUpdatePayload (
1255+ childType : string ,
1256+ typeInfo : ReturnType < RestApiHandler < Schema > [ 'getModelInfo' ] > ,
1257+ rev : string ,
12381258 requestBody : unknown ,
1239- ) : Promise < Response > {
1240- const resolved = this . resolveNestedRelation ( parentType , parentRelation ) ;
1241- if ( ! resolved ) {
1242- return this . makeError ( 'invalidPath' ) ;
1243- }
1244-
1245- const parentInfo = this . getModelInfo ( parentType ) ! ;
1246- const childType = resolved . childType ;
1247- const typeInfo = this . getModelInfo ( childType ) ! ;
1248- const rev = resolved . reverseRelation ;
1249-
1259+ ) : { updateData : any ; error ?: never } | { updateData ?: never ; error : Response } {
12501260 const { attributes, relationships, error } = this . processRequestBody ( requestBody ) ;
1251- if ( error ) return error ;
1261+ if ( error ) return { error } ;
12521262
12531263 const updateData : any = { ...attributes } ;
12541264
12551265 // Reject attempts to change the parent relation via the nested endpoint
12561266 if ( relationships && Object . prototype . hasOwnProperty . call ( relationships , rev ) ) {
1257- return this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) ;
1267+ return { error : this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) } ;
12581268 }
1259- const fkFields = Object . values ( typeInfo . fields ) . filter ( ( f ) => f . foreignKeyFor ?. includes ( rev ) ) ;
1269+ const fkFields = Object . values ( typeInfo ! . fields ) . filter ( ( f ) => f . foreignKeyFor ?. includes ( rev ) ) ;
12601270 if ( fkFields . some ( ( f ) => Object . prototype . hasOwnProperty . call ( updateData , f . name ) ) ) {
1261- return this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) ;
1271+ return { error : this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) } ;
12621272 }
12631273
12641274 // Turn relationship payload into connect/set objects
12651275 if ( relationships ) {
12661276 for ( const [ key , data ] of Object . entries < any > ( relationships ) ) {
12671277 if ( ! data ?. data ) {
1268- return this . makeError ( 'invalidRelationData' ) ;
1278+ return { error : this . makeError ( 'invalidRelationData' ) } ;
12691279 }
1270- const relationInfo = typeInfo . relationships [ key ] ;
1280+ const relationInfo = typeInfo ! . relationships [ key ] ;
12711281 if ( ! relationInfo ) {
1272- return this . makeUnsupportedRelationshipError ( childType , key , 400 ) ;
1282+ return { error : this . makeUnsupportedRelationshipError ( childType , key , 400 ) } ;
12731283 }
12741284 if ( relationInfo . isCollection ) {
12751285 updateData [ key ] = {
@@ -1279,7 +1289,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
12791289 } ;
12801290 } else {
12811291 if ( typeof data . data !== 'object' ) {
1282- return this . makeError ( 'invalidRelationData' ) ;
1292+ return { error : this . makeError ( 'invalidRelationData' ) } ;
12831293 }
12841294 updateData [ key ] = {
12851295 connect : { [ this . makeDefaultIdKey ( relationInfo . idFields ) ] : data . data . id } ,
@@ -1288,30 +1298,66 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
12881298 }
12891299 }
12901300
1291- // Atomically update child scoped to parent; ORM throws NOT_FOUND if parent or child-belongs-to-parent check fails
1292- await ( client as any ) [ parentType ] . update ( {
1293- where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1294- data : {
1295- [ parentRelation ] : {
1296- update : { where : this . makeIdFilter ( typeInfo . idFields , childId ) , data : updateData } ,
1297- } ,
1298- } ,
1299- } ) ;
1301+ return { updateData } ;
1302+ }
13001303
1301- // Fetch the updated entity for the response
1302- const fetchArgs : any = { where : this . makeIdFilter ( typeInfo . idFields , childId ) } ;
1303- this . includeRelationshipIds ( childType , fetchArgs , 'include' ) ;
1304- const entity = await ( client as any ) [ childType ] . findUnique ( fetchArgs ) ;
1305- if ( ! entity ) return this . makeError ( 'notFound' ) ;
1304+ /**
1305+ * Handles PATCH /:type/:id/:relationship/:childId (to-many) and
1306+ * PATCH /:type/:id/:relationship (to-one, childId undefined).
1307+ */
1308+ private async processNestedUpdate (
1309+ client : ClientContract < Schema > ,
1310+ parentType : string ,
1311+ parentId : string ,
1312+ parentRelation : string ,
1313+ childId : string | undefined ,
1314+ _query : Record < string , string | string [ ] > | undefined ,
1315+ requestBody : unknown ,
1316+ ) : Promise < Response > {
1317+ const resolved = this . resolveNestedRelation ( parentType , parentRelation ) ;
1318+ if ( ! resolved ) {
1319+ return this . makeError ( 'invalidPath' ) ;
1320+ }
13061321
1307- const linkUrl = this . makeLinkUrl ( this . makeNestedLinkUrl ( parentType , parentId , parentRelation , childId ) ) ;
1308- const nestedLinker = new tsjapi . Linker ( ( ) => linkUrl ) ;
1309- return {
1310- status : 200 ,
1311- body : await this . serializeItems ( childType , entity , {
1312- linkers : { document : nestedLinker , resource : nestedLinker } ,
1313- } ) ,
1314- } ;
1322+ const parentInfo = this . getModelInfo ( parentType ) ! ;
1323+ const childType = resolved . childType ;
1324+ const typeInfo = this . getModelInfo ( childType ) ! ;
1325+
1326+ const { updateData, error } = this . buildNestedUpdatePayload ( childType , typeInfo , resolved . reverseRelation , requestBody ) ;
1327+ if ( error ) return error ;
1328+
1329+ if ( childId ) {
1330+ // to-many: ORM requires a where filter to identify the child within the collection
1331+ await ( client as any ) [ parentType ] . update ( {
1332+ where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1333+ data : { [ parentRelation ] : { update : { where : this . makeIdFilter ( typeInfo . idFields , childId ) , data : updateData } } } ,
1334+ } ) ;
1335+ const fetchArgs : any = { where : this . makeIdFilter ( typeInfo . idFields , childId ) } ;
1336+ this . includeRelationshipIds ( childType , fetchArgs , 'include' ) ;
1337+ const entity = await ( client as any ) [ childType ] . findUnique ( fetchArgs ) ;
1338+ if ( ! entity ) return this . makeError ( 'notFound' ) ;
1339+ const linkUrl = this . makeLinkUrl ( this . makeNestedLinkUrl ( parentType , parentId , parentRelation , childId ) ) ;
1340+ const nestedLinker = new tsjapi . Linker ( ( ) => linkUrl ) ;
1341+ return { status : 200 , body : await this . serializeItems ( childType , entity , { linkers : { document : nestedLinker , resource : nestedLinker } } ) } ;
1342+ } else {
1343+ // to-one: no where filter needed; fetch via parent select
1344+ await ( client as any ) [ parentType ] . update ( {
1345+ where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1346+ data : { [ parentRelation ] : { update : updateData } } ,
1347+ } ) ;
1348+ const childIncludeArgs : any = { } ;
1349+ this . includeRelationshipIds ( childType , childIncludeArgs , 'include' ) ;
1350+ const fetchArgs : any = {
1351+ where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1352+ select : { [ parentRelation ] : childIncludeArgs . include ? { include : childIncludeArgs . include } : true } ,
1353+ } ;
1354+ const parent = await ( client as any ) [ parentType ] . findUnique ( fetchArgs ) ;
1355+ const entity = parent ?. [ parentRelation ] ;
1356+ if ( ! entity ) return this . makeError ( 'notFound' ) ;
1357+ const linkUrl = this . makeLinkUrl ( this . makeNestedLinkUrl ( parentType , parentId , parentRelation ) ) ;
1358+ const nestedLinker = new tsjapi . Linker ( ( ) => linkUrl ) ;
1359+ return { status : 200 , body : await this . serializeItems ( childType , entity , { linkers : { document : nestedLinker , resource : nestedLinker } } ) } ;
1360+ }
13151361 }
13161362
13171363 private async processNestedDelete (
0 commit comments