feat(12.2-03): add parent-scoped child show, update, delete and pivot routes

- loadChild finds a child with one query carrying the parent predicate; a foreign child is 404
- GET/PUT .../records/{child} and POST .../delete (all or nothing) per relation kind
- hasMany link adopts NULL-key rows and unlink clears the key; pending created children are never candidates
- link accepts pivot values for one id through the pivot.form whitelist; GET/PUT .../pivot/{child}
- Link and Unlink share linkRelated/unlinkRelated for the deferred commit
This commit is contained in:
Jakub Zych
2026-10-02 18:44:34 +02:00
parent 48a5b8045a
commit afb05b6ee4
13 changed files with 2327 additions and 90 deletions

View File

@@ -275,10 +275,22 @@ func (s *service) mount(r pact.Router) {
constrainRelation(g)
g.Post("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink", requireAjax(s.relationUnlink))
constrainRelation(g)
// Relation child routes (D-11, D-12): create through the relation's
// manage form.
// Relation child routes (D-11, D-12, D-15): create, show, update and
// delete children through the relation's forms, every child scoped
// to the parent record.
g.Post("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/records", requireAjax(s.relationChildCreate))
constrainRelation(g)
g.Get("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/records/{child}", s.relationChildShow)
constrainChild(g)
g.Put("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/records/{child}", requireAjax(s.relationChildUpdate))
constrainChild(g)
g.Post("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/delete", requireAjax(s.relationChildDelete))
constrainRelation(g)
// Pivot form values of one belongsToMany link (D-14).
g.Get("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/pivot/{child}", s.relationPivotShow)
constrainChild(g)
g.Put("/{vendor}/{plugin}/{controller}/{id}/relations/{name}/pivot/{child}", requireAjax(s.relationPivotUpdate))
constrainChild(g)
// File routes of `type: fileupload` fields (D-09). {id} 0 is the
// record being created in the X-Session-Key session.
g.Post("/{vendor}/{plugin}/{controller}/{id}/files/{field}", requireAjax(s.fileUpload))
@@ -318,6 +330,11 @@ func constrainRelation(g pact.Router) {
g.Where("name", "[A-Za-z_][A-Za-z0-9_]*")
}
func constrainChild(g pact.Router) {
constrainRelation(g)
g.Where("child", "[0-9]+")
}
func constrainFile(g pact.Router) {
constrainController(g)
g.Where("field", "[A-Za-z_][A-Za-z0-9_]*")