store/memo_attachment.go
Rama main · réplica de lectura del repositorio original.
package store
import (
"context"
"errors"
"slices"
)
// ErrMemoMutationConflict indicates that memo or attachment state changed
// after an API request prepared its mutation.
var ErrMemoMutationConflict = errors.New("memo state changed")
// MemoAttachmentBinding describes one attachment that should be bound to a
// memo. WasBoundToMemo distinguishes an existing binding from a new one so the
// driver can reject ownership transfers while preserving legacy rows already
// attached to the memo. A new binding must be owned by the memo author, or by
// the acting instance administrator.
type MemoAttachmentBinding struct {
ID int32
UID string
UpdatedTs int64
WasBoundToMemo bool
}
// MemoMutation atomically updates a memo, its attachment bindings, and its
// reference relations. Removed attachment rows are deleted in the same
// transaction; callers may clean up external objects after the commit.
type MemoMutation struct {
// MemoCreate requests that the memo row be created in this same transaction
// before attachments and relations are applied. CommentContextMemoID creates
// one immutable COMMENT relation from the new memo to the named context memo.
MemoCreate *Memo
CommentContextMemoID *int32
MemoID int32
MemoCreatorID int32
ExpectedMemoContent string
MemoUpdate *UpdateMemo
Bindings []*MemoAttachmentBinding
RemovedAttachmentIDs []int32
RequiredAttachmentIDs []int32
ReplaceReferenceRelations bool
ReferenceRelations []*MemoRelation
Policy *MemoWritePolicy
}
// MemoAttachmentBindingOwnerAllowed reports whether an unbound attachment may
// be bound to a memo: it must be owned by the memo author, or by the acting
// instance administrator. Ownership is never transferred between accounts.
func MemoAttachmentBindingOwnerAllowed(attachmentCreatorID, memoCreatorID, actorUserID int32, actorIsAdmin bool) bool {
if attachmentCreatorID == memoCreatorID {
return true
}
return actorIsAdmin && attachmentCreatorID == actorUserID
}
// MemoAttachmentRemovalAllowed reports whether an attachment may be removed
// from a memo. Removal deletes the attachment, so it must be bound to that
// memo and owned by the actor unless the actor is an instance administrator.
func MemoAttachmentRemovalAllowed(attachment *Attachment, memoID, actorUserID int32, actorIsAdmin bool) bool {
if attachment == nil || attachment.MemoID == nil || *attachment.MemoID != memoID {
return false
}
return actorIsAdmin || attachment.CreatorID == actorUserID
}
// WritePolicy returns the transport-facing policy governing the mutation, if
// any: the mutation's own policy, else the one carried by its memo update.
func (m *MemoMutation) WritePolicy() *MemoWritePolicy {
if m.Policy != nil {
return m.Policy
}
if m.MemoUpdate != nil {
return m.MemoUpdate.Policy
}
return nil
}
// ActorUserID returns the user performing the mutation. Transport-facing
// updates carry the actor in their policy, which may differ from the memo
// author when an instance administrator acts; creations act as the author.
func (m *MemoMutation) ActorUserID() int32 {
if policy := m.WritePolicy(); policy != nil {
return policy.ActorUserID
}
return m.MemoCreatorID
}
// ApplyMemoMutation atomically applies memo fields, attachment bindings, and
// reference relations after rechecking the state used by API validation.
func (s *Store) ApplyMemoMutation(ctx context.Context, mutation *MemoMutation) error {
if mutation == nil {
return errors.New("memo mutation is required")
}
if mutation.MemoCreate != nil {
if mutation.MemoUpdate != nil || mutation.Policy != nil || len(mutation.RemovedAttachmentIDs) != 0 {
return errors.New("memo creation mutation cannot update or remove existing state")
}
create := mutation.MemoCreate
if create.CreatorID <= 0 {
return errors.New("memo creation mutation requires creator")
}
if err := validateMemoCreate(create); err != nil {
return err
}
if mutation.CommentContextMemoID != nil && *mutation.CommentContextMemoID <= 0 {
return errors.New("comment creation mutation requires a valid context memo")
}
mutation.MemoCreatorID = create.CreatorID
mutation.ExpectedMemoContent = create.Content
}
policy := mutation.WritePolicy()
if err := validateMemoWritePolicy(policy); err != nil {
return err
}
mutation.RemovedAttachmentIDs = slices.Clone(mutation.RemovedAttachmentIDs)
slices.Sort(mutation.RemovedAttachmentIDs)
removedAttachmentIDs := make(map[int32]struct{}, len(mutation.RemovedAttachmentIDs))
for index, attachmentID := range mutation.RemovedAttachmentIDs {
if attachmentID <= 0 {
return errors.New("removed attachment IDs must be positive")
}
if index > 0 && mutation.RemovedAttachmentIDs[index-1] == attachmentID {
return errors.New("duplicate removed attachment ID")
}
removedAttachmentIDs[attachmentID] = struct{}{}
}
for _, binding := range mutation.Bindings {
if binding == nil {
return errors.New("attachment binding is required")
}
if _, removed := removedAttachmentIDs[binding.ID]; removed {
return errors.New("attachment cannot be both bound and removed")
}
}
for _, attachmentID := range mutation.RequiredAttachmentIDs {
if _, removed := removedAttachmentIDs[attachmentID]; removed {
return errors.New("attachment cannot be both required and removed")
}
}
if policy != nil && policy.LifecycleOnly &&
(len(mutation.Bindings) != 0 || len(mutation.RemovedAttachmentIDs) != 0 || len(mutation.RequiredAttachmentIDs) != 0 || mutation.ReplaceReferenceRelations) {
return ErrMemoSpaceMembershipRequired
}
if mutation.ReplaceReferenceRelations {
if mutation.MemoCreatorID <= 0 {
return errors.New("reference relation mutation requires an actor")
}
seenRelatedMemoIDs := make(map[int32]struct{}, len(mutation.ReferenceRelations))
for _, relation := range mutation.ReferenceRelations {
if relation == nil || relation.RelatedMemoID <= 0 || relation.Type != MemoRelationReference {
return errors.New("only REFERENCE memo relations may be mutated")
}
if mutation.MemoCreate == nil && relation.MemoID != mutation.MemoID {
return errors.New("reference relation source does not match memo mutation")
}
if mutation.MemoID > 0 && relation.RelatedMemoID == mutation.MemoID {
return errors.New("reflexive memo relations are not allowed")
}
if _, ok := seenRelatedMemoIDs[relation.RelatedMemoID]; ok {
return errors.New("duplicate memo reference relation")
}
seenRelatedMemoIDs[relation.RelatedMemoID] = struct{}{}
}
}
return s.driver.ApplyMemoMutation(ctx, mutation)
}