Ctrl K
ComunidadGuías…

store/memo_attachment.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
}