@@ -565,7 +565,26 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
565565 requestBody ,
566566 ) ;
567567 }
568- // /:type/:id/:relationship/:childId — nested update
568+ // /:type/:id/:relationship — nested update (to-one)
569+ const nestedToOnePatchMatch = this . matchUrlPattern ( path , UrlPatterns . FETCH_RELATIONSHIP ) ;
570+ if (
571+ nestedToOnePatchMatch &&
572+ this . nestedRoutes &&
573+ this . resolveNestedRelation ( nestedToOnePatchMatch . type , nestedToOnePatchMatch . relationship ) &&
574+ ! this . resolveNestedRelation ( nestedToOnePatchMatch . type , nestedToOnePatchMatch . relationship )
575+ ?. isCollection
576+ ) {
577+ return await this . processNestedUpdate (
578+ client ,
579+ nestedToOnePatchMatch . type ,
580+ nestedToOnePatchMatch . id ,
581+ nestedToOnePatchMatch . relationship ,
582+ undefined ,
583+ query ,
584+ requestBody ,
585+ ) ;
586+ }
587+ // /:type/:id/:relationship/:childId — nested update (to-many)
569588 const nestedPatchMatch = this . matchUrlPattern ( path , UrlPatterns . NESTED_SINGLE ) ;
570589 if (
571590 nestedPatchMatch &&
@@ -1215,48 +1234,39 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
12151234 } ;
12161235 }
12171236
1218- private async processNestedUpdate (
1219- client : ClientContract < Schema > ,
1220- parentType : string ,
1221- parentId : string ,
1222- parentRelation : string ,
1223- childId : string ,
1224- _query : Record < string , string | string [ ] > | undefined ,
1237+ /**
1238+ * Builds the ORM `data` payload for a nested update, shared by both to-many (childId present)
1239+ * and to-one (childId absent) variants. Returns either `{ updateData }` or `{ error }`.
1240+ */
1241+ private buildNestedUpdatePayload (
1242+ childType : string ,
1243+ typeInfo : ReturnType < RestApiHandler < Schema > [ 'getModelInfo' ] > ,
1244+ rev : string ,
12251245 requestBody : unknown ,
1226- ) : Promise < Response > {
1227- const resolved = this . resolveNestedRelation ( parentType , parentRelation ) ;
1228- if ( ! resolved ) {
1229- return this . makeError ( 'invalidPath' ) ;
1230- }
1231-
1232- const parentInfo = this . getModelInfo ( parentType ) ! ;
1233- const childType = resolved . childType ;
1234- const typeInfo = this . getModelInfo ( childType ) ! ;
1235- const rev = resolved . reverseRelation ;
1236-
1246+ ) : { updateData : any ; error ?: never } | { updateData ?: never ; error : Response } {
12371247 const { attributes, relationships, error } = this . processRequestBody ( requestBody ) ;
1238- if ( error ) return error ;
1248+ if ( error ) return { error } ;
12391249
12401250 const updateData : any = { ...attributes } ;
12411251
12421252 // Reject attempts to change the parent relation via the nested endpoint
12431253 if ( relationships && Object . prototype . hasOwnProperty . call ( relationships , rev ) ) {
1244- return this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) ;
1254+ return { error : this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) } ;
12451255 }
1246- const fkFields = Object . values ( typeInfo . fields ) . filter ( ( f ) => f . foreignKeyFor ?. includes ( rev ) ) ;
1256+ const fkFields = Object . values ( typeInfo ! . fields ) . filter ( ( f ) => f . foreignKeyFor ?. includes ( rev ) ) ;
12471257 if ( fkFields . some ( ( f ) => Object . prototype . hasOwnProperty . call ( updateData , f . name ) ) ) {
1248- return this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) ;
1258+ return { error : this . makeError ( 'invalidPayload' , `Relation "${ rev } " cannot be changed via a nested route` ) } ;
12491259 }
12501260
12511261 // Turn relationship payload into connect/set objects
12521262 if ( relationships ) {
12531263 for ( const [ key , data ] of Object . entries < any > ( relationships ) ) {
12541264 if ( ! data ?. data ) {
1255- return this . makeError ( 'invalidRelationData' ) ;
1265+ return { error : this . makeError ( 'invalidRelationData' ) } ;
12561266 }
1257- const relationInfo = typeInfo . relationships [ key ] ;
1267+ const relationInfo = typeInfo ! . relationships [ key ] ;
12581268 if ( ! relationInfo ) {
1259- return this . makeUnsupportedRelationshipError ( childType , key , 400 ) ;
1269+ return { error : this . makeUnsupportedRelationshipError ( childType , key , 400 ) } ;
12601270 }
12611271 if ( relationInfo . isCollection ) {
12621272 updateData [ key ] = {
@@ -1266,7 +1276,7 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
12661276 } ;
12671277 } else {
12681278 if ( typeof data . data !== 'object' ) {
1269- return this . makeError ( 'invalidRelationData' ) ;
1279+ return { error : this . makeError ( 'invalidRelationData' ) } ;
12701280 }
12711281 updateData [ key ] = {
12721282 connect : { [ this . makeDefaultIdKey ( relationInfo . idFields ) ] : data . data . id } ,
@@ -1275,30 +1285,66 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
12751285 }
12761286 }
12771287
1278- // Atomically update child scoped to parent; ORM throws NOT_FOUND if parent or child-belongs-to-parent check fails
1279- await ( client as any ) [ parentType ] . update ( {
1280- where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1281- data : {
1282- [ parentRelation ] : {
1283- update : { where : this . makeIdFilter ( typeInfo . idFields , childId ) , data : updateData } ,
1284- } ,
1285- } ,
1286- } ) ;
1288+ return { updateData } ;
1289+ }
12871290
1288- // Fetch the updated entity for the response
1289- const fetchArgs : any = { where : this . makeIdFilter ( typeInfo . idFields , childId ) } ;
1290- this . includeRelationshipIds ( childType , fetchArgs , 'include' ) ;
1291- const entity = await ( client as any ) [ childType ] . findUnique ( fetchArgs ) ;
1292- if ( ! entity ) return this . makeError ( 'notFound' ) ;
1291+ /**
1292+ * Handles PATCH /:type/:id/:relationship/:childId (to-many) and
1293+ * PATCH /:type/:id/:relationship (to-one, childId undefined).
1294+ */
1295+ private async processNestedUpdate (
1296+ client : ClientContract < Schema > ,
1297+ parentType : string ,
1298+ parentId : string ,
1299+ parentRelation : string ,
1300+ childId : string | undefined ,
1301+ _query : Record < string , string | string [ ] > | undefined ,
1302+ requestBody : unknown ,
1303+ ) : Promise < Response > {
1304+ const resolved = this . resolveNestedRelation ( parentType , parentRelation ) ;
1305+ if ( ! resolved ) {
1306+ return this . makeError ( 'invalidPath' ) ;
1307+ }
12931308
1294- const linkUrl = this . makeLinkUrl ( this . makeNestedLinkUrl ( parentType , parentId , parentRelation , childId ) ) ;
1295- const nestedLinker = new tsjapi . Linker ( ( ) => linkUrl ) ;
1296- return {
1297- status : 200 ,
1298- body : await this . serializeItems ( childType , entity , {
1299- linkers : { document : nestedLinker , resource : nestedLinker } ,
1300- } ) ,
1301- } ;
1309+ const parentInfo = this . getModelInfo ( parentType ) ! ;
1310+ const childType = resolved . childType ;
1311+ const typeInfo = this . getModelInfo ( childType ) ! ;
1312+
1313+ const { updateData, error } = this . buildNestedUpdatePayload ( childType , typeInfo , resolved . reverseRelation , requestBody ) ;
1314+ if ( error ) return error ;
1315+
1316+ if ( childId ) {
1317+ // to-many: ORM requires a where filter to identify the child within the collection
1318+ await ( client as any ) [ parentType ] . update ( {
1319+ where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1320+ data : { [ parentRelation ] : { update : { where : this . makeIdFilter ( typeInfo . idFields , childId ) , data : updateData } } } ,
1321+ } ) ;
1322+ const fetchArgs : any = { where : this . makeIdFilter ( typeInfo . idFields , childId ) } ;
1323+ this . includeRelationshipIds ( childType , fetchArgs , 'include' ) ;
1324+ const entity = await ( client as any ) [ childType ] . findUnique ( fetchArgs ) ;
1325+ if ( ! entity ) return this . makeError ( 'notFound' ) ;
1326+ const linkUrl = this . makeLinkUrl ( this . makeNestedLinkUrl ( parentType , parentId , parentRelation , childId ) ) ;
1327+ const nestedLinker = new tsjapi . Linker ( ( ) => linkUrl ) ;
1328+ return { status : 200 , body : await this . serializeItems ( childType , entity , { linkers : { document : nestedLinker , resource : nestedLinker } } ) } ;
1329+ } else {
1330+ // to-one: no where filter needed; fetch via parent select
1331+ await ( client as any ) [ parentType ] . update ( {
1332+ where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1333+ data : { [ parentRelation ] : { update : updateData } } ,
1334+ } ) ;
1335+ const childIncludeArgs : any = { } ;
1336+ this . includeRelationshipIds ( childType , childIncludeArgs , 'include' ) ;
1337+ const fetchArgs : any = {
1338+ where : this . makeIdFilter ( parentInfo . idFields , parentId ) ,
1339+ select : { [ parentRelation ] : childIncludeArgs . include ? { include : childIncludeArgs . include } : true } ,
1340+ } ;
1341+ const parent = await ( client as any ) [ parentType ] . findUnique ( fetchArgs ) ;
1342+ const entity = parent ?. [ parentRelation ] ;
1343+ if ( ! entity ) return this . makeError ( 'notFound' ) ;
1344+ const linkUrl = this . makeLinkUrl ( this . makeNestedLinkUrl ( parentType , parentId , parentRelation ) ) ;
1345+ const nestedLinker = new tsjapi . Linker ( ( ) => linkUrl ) ;
1346+ return { status : 200 , body : await this . serializeItems ( childType , entity , { linkers : { document : nestedLinker , resource : nestedLinker } } ) } ;
1347+ }
13021348 }
13031349
13041350 private async processNestedDelete (
0 commit comments