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:
@@ -6,11 +6,14 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// relationParent is the parent record of a relation route, loaded through
|
||||
@@ -138,23 +141,306 @@ func (s RelationService) CreateChild(ctx context.Context, cc *CompiledController
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// loadChild finds one child of the parent with a single query that carries
|
||||
// the parent predicate (D-15): on a hasMany the child's ForeignKey must be
|
||||
// the parent's key, on a belongsToMany a pivot row must link the child to
|
||||
// the parent. A child of another parent is recordNotFound (404, never 403).
|
||||
// lock adds FOR UPDATE.
|
||||
func loadChild(ctx context.Context, tx *gorm.DB, cr *CompiledRelation, parent *relationParent, childID uint, lock bool) (any, error) {
|
||||
if parent == nil || childID == 0 {
|
||||
return nil, recordNotFound{}
|
||||
}
|
||||
child := cr.Contract.NewRelated()
|
||||
table := tableName(child)
|
||||
pk := clause.Column{Table: table, Name: primaryColumn(child)}
|
||||
q := tx.Session(&gorm.Session{NewDB: true, Context: ctx}).Model(child).
|
||||
Where(clause.Eq{Column: pk, Value: castPK(child, childID)})
|
||||
if cr.hasMany() {
|
||||
q = q.Where(clause.Eq{Column: clause.Column{Table: table, Name: cr.Contract.ForeignKey}, Value: parent.id})
|
||||
} else {
|
||||
linked := "EXISTS (SELECT 1 FROM " + quotedIdent(tx, tableName(cr.Contract.NewPivot())) + " p WHERE p." +
|
||||
quotedIdent(tx, cr.Contract.ParentForeignKey) + " = ? AND p." + quotedIdent(tx, cr.Contract.RelatedForeignKey) +
|
||||
" = " + quotedIdent(tx, table) + "." + quotedIdent(tx, primaryColumn(child)) + ")"
|
||||
q = q.Where(linked, parent.id)
|
||||
}
|
||||
if lock {
|
||||
q = q.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
}
|
||||
if err := q.Take(child).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, recordNotFound{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return child, nil
|
||||
}
|
||||
|
||||
// childForm is the form a child is shown with: the manage form when the
|
||||
// relation declares update, else the view form.
|
||||
func (cr *CompiledRelation) childForm() *CompiledController {
|
||||
if cr.allows("update") && cr.child != nil {
|
||||
return cr.child
|
||||
}
|
||||
return cr.view
|
||||
}
|
||||
|
||||
// ShowChild loads one child of the parent (D-15) and projects it through
|
||||
// the manage form when the relation declares update, else the view form.
|
||||
func (s RelationService) ShowChild(ctx context.Context, cc *CompiledController, relation string, ownerID, childID uint) (RecordResult, error) {
|
||||
if s.DB == nil {
|
||||
return RecordResult{}, errors.New("cabana: database is not configured")
|
||||
}
|
||||
cr, err := relationOf(cc, relation)
|
||||
if err != nil {
|
||||
return RecordResult{}, err
|
||||
}
|
||||
form := cr.childForm()
|
||||
if form == nil {
|
||||
return RecordResult{}, recordNotFound{}
|
||||
}
|
||||
var result RecordResult
|
||||
err = lagoon.Transaction(ctx, s.DB, func(ctx context.Context, tx *gorm.DB) error {
|
||||
ctx = withTx(ctx, tx)
|
||||
parent, err := s.loadParent(ctx, tx, cc, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
child, err := loadChild(ctx, tx, cr, parent, childID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err = projectFullRecord(ctx, tx, form, child)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return RecordResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateChild saves one child of the parent through the manage form (D-16):
|
||||
// the child is loaded under the parent scope and locked, filled and
|
||||
// validated like a controller update, and saved through its model between
|
||||
// the optional pact.RelationBeforeUpdate and pact.RelationAfterUpdate hooks.
|
||||
func (s RelationService) UpdateChild(ctx context.Context, cc *CompiledController, relation string, ownerID, childID uint, in RecordInput) (RecordResult, error) {
|
||||
if s.DB == nil {
|
||||
return RecordResult{}, errors.New("cabana: database is not configured")
|
||||
}
|
||||
cr, err := relationOf(cc, relation)
|
||||
if err != nil {
|
||||
return RecordResult{}, err
|
||||
}
|
||||
if cr.child == nil {
|
||||
return RecordResult{}, &CapabilityError{ControllerID: controllerID(cc)}
|
||||
}
|
||||
var result RecordResult
|
||||
err = lagoon.Transaction(ctx, s.DB, func(ctx context.Context, tx *gorm.DB) error {
|
||||
ctx = withTx(ctx, tx)
|
||||
parent, err := s.loadParent(ctx, tx, cc, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
child, err := loadChild(ctx, tx, cr, parent, childID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.fillChild(ctx, tx, cr.child, child, in.Body, "update"); err != nil {
|
||||
return err
|
||||
}
|
||||
if hook, ok := cc.Controller.(pact.RelationBeforeUpdate); ok && hook != nil {
|
||||
if err := hook.RelationBeforeUpdate(ctx, cr.Contract.Name, parent.model, child); err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
}
|
||||
if err := tx.WithContext(ctx).Save(child).Error; err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
if hook, ok := cc.Controller.(pact.RelationAfterUpdate); ok && hook != nil {
|
||||
if err := hook.RelationAfterUpdate(ctx, cr.Contract.Name, parent.model, child); err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
}
|
||||
result, err = projectFullRecord(ctx, tx, cr.child, child)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return RecordResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteChildren deletes children of the parent (D-12). Every id must be a
|
||||
// child of this parent, or the whole request is recordNotFound and nothing
|
||||
// is deleted. Per child the optional pact.RelationBeforeDelete runs, then a
|
||||
// hasMany child is deleted through its model (hooks and soft delete run),
|
||||
// a belongsToMany record first loses this parent's pivot row and is then
|
||||
// deleted through its model, then pact.RelationAfterDelete runs.
|
||||
func (s RelationService) DeleteChildren(ctx context.Context, cc *CompiledController, relation string, ownerID uint, in BulkDeleteInput) (BulkResult, error) {
|
||||
if s.DB == nil {
|
||||
return BulkResult{}, errors.New("cabana: database is not configured")
|
||||
}
|
||||
ids, err := normalizeIDs(in.IDs)
|
||||
if err != nil {
|
||||
return BulkResult{}, err
|
||||
}
|
||||
cr, err := relationOf(cc, relation)
|
||||
if err != nil {
|
||||
return BulkResult{}, err
|
||||
}
|
||||
var result BulkResult
|
||||
err = lagoon.Transaction(ctx, s.DB, func(ctx context.Context, tx *gorm.DB) error {
|
||||
ctx = withTx(ctx, tx)
|
||||
parent, err := s.loadParent(ctx, tx, cc, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
children := make([]any, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
child, err := loadChild(ctx, tx, cr, parent, id, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
children = append(children, child)
|
||||
}
|
||||
for _, child := range children {
|
||||
if err := s.deleteChild(ctx, tx, cc, cr, parent, child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result.Deleted = len(children)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return BulkResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// deleteChild deletes one loaded child of the parent through its model.
|
||||
func (s RelationService) deleteChild(ctx context.Context, tx *gorm.DB, cc *CompiledController, cr *CompiledRelation, parent *relationParent, child any) error {
|
||||
if hook, ok := cc.Controller.(pact.RelationBeforeDelete); ok && hook != nil {
|
||||
if err := hook.RelationBeforeDelete(ctx, cr.Contract.Name, parent.model, child); err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
}
|
||||
if !cr.hasMany() && parent.id > 0 {
|
||||
rows, err := lockPivotRows(ctx, tx, cr, parent.id, []uint{pkUint(child)})
|
||||
if err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
if err := tx.WithContext(ctx).Delete(row).Error; err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := tx.WithContext(ctx).Delete(child).Error; err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
if hook, ok := cc.Controller.(pact.RelationAfterDelete); ok && hook != nil {
|
||||
if err := hook.RelationAfterDelete(ctx, cr.Contract.Name, parent.model, child); err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadPivotRow loads this parent's pivot row for the related id, locked; a
|
||||
// missing row is recordNotFound.
|
||||
func loadPivotRow(ctx context.Context, tx *gorm.DB, cr *CompiledRelation, parent *relationParent, childID uint) (any, error) {
|
||||
if childID == 0 {
|
||||
return nil, recordNotFound{}
|
||||
}
|
||||
rows, err := lockPivotRows(ctx, tx, cr, parent.id, []uint{childID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil, recordNotFound{}
|
||||
}
|
||||
return rows[0], nil
|
||||
}
|
||||
|
||||
// ShowPivot returns the pivot form values of the link between the parent
|
||||
// and one related record (D-14).
|
||||
func (s RelationService) ShowPivot(ctx context.Context, cc *CompiledController, relation string, ownerID, childID uint) (map[string]any, error) {
|
||||
if s.DB == nil {
|
||||
return nil, errors.New("cabana: database is not configured")
|
||||
}
|
||||
cr, err := relationOf(cc, relation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cr.pivot == nil {
|
||||
return nil, recordNotFound{}
|
||||
}
|
||||
var data map[string]any
|
||||
err = lagoon.Transaction(ctx, s.DB, func(ctx context.Context, tx *gorm.DB) error {
|
||||
ctx = withTx(ctx, tx)
|
||||
parent, err := s.loadParent(ctx, tx, cc, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row, err := loadPivotRow(ctx, tx, cr, parent, childID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = pivotRecord(cr, row, childID)
|
||||
return nil
|
||||
})
|
||||
return data, err
|
||||
}
|
||||
|
||||
// UpdatePivot saves pivot form values on the link between the parent and
|
||||
// one related record (D-14): only pivot form fields are accepted (an
|
||||
// unknown key is a 422), and the pivot model is saved through GORM.
|
||||
func (s RelationService) UpdatePivot(ctx context.Context, cc *CompiledController, relation string, ownerID, childID uint, values map[string]any) (map[string]any, error) {
|
||||
if s.DB == nil {
|
||||
return nil, errors.New("cabana: database is not configured")
|
||||
}
|
||||
cr, err := relationOf(cc, relation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cr.pivot == nil {
|
||||
return nil, recordNotFound{}
|
||||
}
|
||||
var data map[string]any
|
||||
err = lagoon.Transaction(ctx, s.DB, func(ctx context.Context, tx *gorm.DB) error {
|
||||
ctx = withTx(ctx, tx)
|
||||
parent, err := s.loadParent(ctx, tx, cc, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row, err := loadPivotRow(ctx, tx, cr, parent, childID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.fillPivot(ctx, tx, cr, row, values); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).Save(row).Error; err != nil {
|
||||
return lifecycleFailure(cc, err)
|
||||
}
|
||||
data = pivotRecord(cr, row, childID)
|
||||
return nil
|
||||
})
|
||||
return data, err
|
||||
}
|
||||
|
||||
// pivotRecord projects a pivot row through the pivot form, keyed by the
|
||||
// related record's id.
|
||||
func pivotRecord(cr *CompiledRelation, row any, childID uint) map[string]any {
|
||||
data := projectRecord(cr.pivot, row)
|
||||
data["id"] = childID
|
||||
return data
|
||||
}
|
||||
|
||||
// relationButton resolves the route's relation and refuses (403) a route
|
||||
// whose toolbar button the view panel does not declare. An unknown relation
|
||||
// is 404. It writes the response and returns nil on refusal.
|
||||
func (s *service) relationButton(w http.ResponseWriter, r *http.Request, cc *CompiledController, button string) *CompiledRelation {
|
||||
cr, err := relationOf(cc, r.PathValue("name"))
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return nil
|
||||
}
|
||||
if !cr.allows(button) {
|
||||
if principal, _ := bouncer.User(r.Context()); principal != nil {
|
||||
s.logAuth(r, "denied", principal.ID)
|
||||
}
|
||||
WriteError(w, http.StatusForbidden, "forbidden", msgForbidden)
|
||||
return nil
|
||||
}
|
||||
return cr
|
||||
return s.relationAllowed(w, r, cc, func(cr *CompiledRelation) bool { return cr.allows(button) })
|
||||
}
|
||||
|
||||
// decodeCappedObject decodes a JSON object body capped at
|
||||
@@ -221,3 +507,218 @@ func (s *service) relationChildCreate(w http.ResponseWriter, r *http.Request) {
|
||||
WriteData(w, http.StatusCreated, rec.Data, rec.Meta)
|
||||
})
|
||||
}
|
||||
|
||||
// pathChildID parses {child}; anything but a positive integer is not found.
|
||||
func pathChildID(r *http.Request) (uint, error) {
|
||||
n, err := strconv.ParseUint(strings.TrimSpace(r.PathValue("child")), 10, 64)
|
||||
if err != nil || n == 0 {
|
||||
return 0, recordNotFound{}
|
||||
}
|
||||
return uint(n), nil
|
||||
}
|
||||
|
||||
// relationAllowed resolves the route's relation and refuses (403) unless
|
||||
// allowed reports the relation declares what the route needs. An unknown
|
||||
// relation is 404. It writes the response and returns nil on refusal.
|
||||
func (s *service) relationAllowed(w http.ResponseWriter, r *http.Request, cc *CompiledController, allowed func(*CompiledRelation) bool) *CompiledRelation {
|
||||
cr, err := relationOf(cc, r.PathValue("name"))
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return nil
|
||||
}
|
||||
if !allowed(cr) {
|
||||
if principal, _ := bouncer.User(r.Context()); principal != nil {
|
||||
s.logAuth(r, "denied", principal.ID)
|
||||
}
|
||||
WriteError(w, http.StatusForbidden, "forbidden", msgForbidden)
|
||||
return nil
|
||||
}
|
||||
return cr
|
||||
}
|
||||
|
||||
// relationIDs parses {id} and {child}.
|
||||
func relationIDs(r *http.Request) (uint, uint, error) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
child, err := pathChildID(r)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return id, child, nil
|
||||
}
|
||||
|
||||
// relationChildShow serves GET .../{id}/relations/{name}/records/{child}:
|
||||
// allowed when the relation declares update (with a manage form) or has a
|
||||
// view form.
|
||||
func (s *service) relationChildShow(w http.ResponseWriter, r *http.Request) {
|
||||
s.protect(w, r, func(cc *CompiledController) {
|
||||
cr := s.relationAllowed(w, r, cc, func(cr *CompiledRelation) bool { return cr.childForm() != nil })
|
||||
if cr == nil {
|
||||
return
|
||||
}
|
||||
id, child, err := relationIDs(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
svc, err := s.relations()
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
rec, err := svc.ShowChild(r.Context(), cc, cr.Contract.Name, id, child)
|
||||
if err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
WriteData(w, http.StatusOK, rec.Data, rec.Meta)
|
||||
})
|
||||
}
|
||||
|
||||
// relationChildUpdate serves PUT .../{id}/relations/{name}/records/{child}.
|
||||
func (s *service) relationChildUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
s.protect(w, r, func(cc *CompiledController) {
|
||||
cr := s.relationButton(w, r, cc, "update")
|
||||
if cr == nil {
|
||||
return
|
||||
}
|
||||
id, child, err := relationIDs(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
body, err := s.decodeCappedObject(w, r)
|
||||
if err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
svc, err := s.relations()
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
rec, err := svc.UpdateChild(r.Context(), cc, cr.Contract.Name, id, child, RecordInput{Body: body})
|
||||
if err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
WriteData(w, http.StatusOK, rec.Data, rec.Meta)
|
||||
})
|
||||
}
|
||||
|
||||
// relationChildDelete serves POST .../{id}/relations/{name}/delete with
|
||||
// {ids}.
|
||||
func (s *service) relationChildDelete(w http.ResponseWriter, r *http.Request) {
|
||||
s.protect(w, r, func(cc *CompiledController) {
|
||||
cr := s.relationButton(w, r, cc, "delete")
|
||||
if cr == nil {
|
||||
return
|
||||
}
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
var in BulkDeleteInput
|
||||
if err := s.decodeStrictNumbers(w, r, &in); err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
svc, err := s.relations()
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
result, err := svc.DeleteChildren(r.Context(), cc, cr.Contract.Name, id, in)
|
||||
if err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
WriteData(w, http.StatusOK, result, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// decodeStrictNumbers decodes a capped JSON body into dest with numbers
|
||||
// kept as json.Number, unknown keys and trailing data refused.
|
||||
func (s *service) decodeStrictNumbers(w http.ResponseWriter, r *http.Request, dest any) error {
|
||||
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, s.jsonCap()))
|
||||
dec.UseNumber()
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(dest); err != nil {
|
||||
var tooBig *http.MaxBytesError
|
||||
if errors.As(err, &tooBig) {
|
||||
return err
|
||||
}
|
||||
return invalidBody()
|
||||
}
|
||||
var trailing any
|
||||
if err := dec.Decode(&trailing); err != io.EOF {
|
||||
return invalidBody()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pivotAllowed: the pivot routes need a pivot form and the link or update
|
||||
// button.
|
||||
func pivotAllowed(cr *CompiledRelation) bool {
|
||||
return cr.pivot != nil && (cr.allows("link") || cr.allows("update"))
|
||||
}
|
||||
|
||||
// relationPivotShow serves GET .../{id}/relations/{name}/pivot/{child}.
|
||||
func (s *service) relationPivotShow(w http.ResponseWriter, r *http.Request) {
|
||||
s.protect(w, r, func(cc *CompiledController) {
|
||||
cr := s.relationAllowed(w, r, cc, pivotAllowed)
|
||||
if cr == nil {
|
||||
return
|
||||
}
|
||||
id, child, err := relationIDs(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
svc, err := s.relations()
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
data, err := svc.ShowPivot(r.Context(), cc, cr.Contract.Name, id, child)
|
||||
if err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
WriteData(w, http.StatusOK, data, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// relationPivotUpdate serves PUT .../{id}/relations/{name}/pivot/{child}.
|
||||
func (s *service) relationPivotUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
s.protect(w, r, func(cc *CompiledController) {
|
||||
cr := s.relationAllowed(w, r, cc, pivotAllowed)
|
||||
if cr == nil {
|
||||
return
|
||||
}
|
||||
id, child, err := relationIDs(r)
|
||||
if err != nil {
|
||||
writeCRUDError(w, err)
|
||||
return
|
||||
}
|
||||
body, err := s.decodeCappedObject(w, r)
|
||||
if err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
svc, err := s.relations()
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
data, err := svc.UpdatePivot(r.Context(), cc, cr.Contract.Name, id, child, body)
|
||||
if err != nil {
|
||||
writeRelationError(w, err)
|
||||
return
|
||||
}
|
||||
WriteData(w, http.StatusOK, data, nil)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user