Ctrl K
ComunidadGuías…

store/space.go

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

Archivo original ↗Preguntar a KiBot
package store

import (
	"context"
	"errors"
	"strings"

	"github.com/usememos/memos/internal/identifier"
	storepb "github.com/usememos/memos/proto/gen/store"
)

// ErrLastSpaceAdmin indicates that a membership mutation would leave an active
// space without an administrator.
var ErrLastSpaceAdmin = errors.New("cannot remove the last space admin")

// ErrSpacePermissionDenied indicates that the actor cannot perform a space mutation.
var ErrSpacePermissionDenied = errors.New("space permission denied")

// ErrSpaceMemberNotActive indicates that a membership target is not an active user.
var ErrSpaceMemberNotActive = errors.New("space member user is not active")

// ErrSpaceAlreadyExists indicates a duplicate immutable space ID.
var ErrSpaceAlreadyExists = errors.New("space already exists")

// ErrSpaceMemberAlreadyExists indicates an existing membership or invitation.
var ErrSpaceMemberAlreadyExists = errors.New("space member already exists")

// ErrSpaceNotFound indicates a missing mutation target.
var ErrSpaceNotFound = errors.New("space not found")

// ErrSpaceMemberNotFound indicates a missing membership mutation target.
var ErrSpaceMemberNotFound = errors.New("space member not found")

// ErrSpaceInvitationNotFound indicates a missing pending invitation.
var ErrSpaceInvitationNotFound = errors.New("space invitation not found")

// SpaceMemberStatus is the state of a user's relationship with a space.
type SpaceMemberStatus string

const (
	// SpaceMemberStatusInvited represents an invitation that the user has not accepted.
	SpaceMemberStatusInvited SpaceMemberStatus = "INVITED"
	// SpaceMemberStatusActive represents a membership that the user has accepted.
	SpaceMemberStatusActive SpaceMemberStatus = "ACTIVE"
)

// IsValid reports whether the status is recognized.
func (s SpaceMemberStatus) IsValid() bool {
	return s == SpaceMemberStatusInvited || s == SpaceMemberStatusActive
}

// SpaceMemberRole is the role of a user within a space.
type SpaceMemberRole string

const (
	// SpaceMemberRoleAdmin can manage the space and its membership.
	SpaceMemberRoleAdmin SpaceMemberRole = "ADMIN"
	// SpaceMemberRoleUser is a regular space member.
	SpaceMemberRoleUser SpaceMemberRole = "USER"
)

// IsActiveMember reports whether the role can grant space membership. The
// relationship must separately be ACTIVE before the role grants access.
func (r SpaceMemberRole) IsActiveMember() bool {
	return r == SpaceMemberRoleAdmin || r == SpaceMemberRoleUser
}

// Space groups memos and memberships.
type Space struct {
	ID int32

	UID             string
	Title           string
	Description     string
	Payload         *storepb.SpacePayload
	CurrentUserRole SpaceMemberRole
	MemberCount     int32
}

// FindSpace selects spaces. MemberUserID restricts results to spaces where the
// user has a membership, populates the viewer summary, and is applied before
// pagination.
type FindSpace struct {
	ID           *int32
	IDList       []int32
	UID          *string
	MemberUserID *int32
	Limit        *int
	Offset       *int
}

// UpdateSpace contains mutable space fields.
type UpdateSpace struct {
	ID          int32
	Title       *string
	Description *string
	Payload     *storepb.SpacePayload
}

// DeleteSpace identifies a Space to hard-delete.
type DeleteSpace struct {
	ID          int32
	ActorUserID int32
}

// DeleteSpaceResult contains external resources to clean after commit.
type DeleteSpaceResult struct {
	Attachments []*Attachment
}

// SpaceMember associates a user with a space.
type SpaceMember struct {
	SpaceID int32
	UserID  int32
	Role    SpaceMemberRole
}

// FindSpaceMember selects memberships.
type FindSpaceMember struct {
	SpaceID      *int32
	UserID       *int32
	ViewerUserID *int32
	Limit        *int
	Offset       *int
}

// UpdateSpaceMember contains mutable membership fields.
type UpdateSpaceMember struct {
	SpaceID int32
	UserID  int32
	Role    *SpaceMemberRole
}

// DeleteSpaceMember identifies a membership to delete.
type DeleteSpaceMember struct {
	SpaceID int32
	UserID  int32
}

// SpaceInvitation is a pending offer for an existing user to join a space.
type SpaceInvitation struct {
	SpaceID int32
	UserID  int32
	Role    SpaceMemberRole
}

// FindSpaceInvitation selects pending invitations. ViewerUserID restricts
// results to invitations visible to that user: their own or those in a space
// they actively administer.
type FindSpaceInvitation struct {
	SpaceID      *int32
	UserID       *int32
	ViewerUserID *int32
	Limit        *int
	Offset       *int
}

// AcceptSpaceInvitation identifies an invitation accepted by its invitee.
type AcceptSpaceInvitation struct {
	SpaceID int32
	UserID  int32
}

// DeclineSpaceInvitation identifies an invitation declined by its invitee.
type DeclineSpaceInvitation struct {
	SpaceID int32
	UserID  int32
}

// RevokeSpaceInvitation identifies an invitation revoked by a space administrator.
type RevokeSpaceInvitation struct {
	SpaceID int32
	UserID  int32
}

// CreateSpace creates a space and its first ADMIN membership atomically.
func (s *Store) CreateSpace(ctx context.Context, create *Space, creatorID int32) (*Space, error) {
	if create == nil {
		return nil, errors.New("space is required")
	}
	if !identifier.UIDMatcher.MatchString(create.UID) {
		return nil, errors.New("invalid uid")
	}
	if strings.TrimSpace(create.Title) == "" {
		return nil, errors.New("space title is required")
	}
	if err := ValidateSpaceIcon(create.Payload.GetIcon()); err != nil {
		return nil, err
	}
	return s.driver.CreateSpace(ctx, create, creatorID)
}

// ListSpaces returns spaces matching find.
func (s *Store) ListSpaces(ctx context.Context, find *FindSpace) ([]*Space, error) {
	return s.driver.ListSpaces(ctx, find)
}

// GetSpace returns the first matching space.
func (s *Store) GetSpace(ctx context.Context, find *FindSpace) (*Space, error) {
	spaces, err := s.ListSpaces(ctx, find)
	if err != nil {
		return nil, err
	}
	if len(spaces) == 0 {
		return nil, nil
	}
	return spaces[0], nil
}

// UpdateSpace updates and returns a space.
func (s *Store) UpdateSpace(ctx context.Context, update *UpdateSpace, actorUserID int32) (*Space, error) {
	if update == nil {
		return nil, errors.New("space update is required")
	}
	if update.Title != nil && strings.TrimSpace(*update.Title) == "" {
		return nil, errors.New("space title is required")
	}
	if err := ValidateSpaceIcon(update.Payload.GetIcon()); err != nil {
		return nil, err
	}
	if update.Title == nil && update.Description == nil && update.Payload == nil {
		return nil, errors.New("space update requires at least one field")
	}
	return s.driver.UpdateSpace(ctx, update, actorUserID)
}

// DeleteSpace atomically deletes a Space, all directly assigned memos, and
// their owned database resources. Memo relations are not followed.
func (s *Store) DeleteSpace(ctx context.Context, delete *DeleteSpace) (*DeleteSpaceResult, error) {
	if delete == nil || delete.ID <= 0 || delete.ActorUserID <= 0 {
		return nil, errors.New("space deletion requires space and actor")
	}
	result, err := s.driver.DeleteSpace(ctx, delete)
	if err != nil {
		return nil, err
	}
	if result == nil {
		return nil, errors.New("unexpected nil delete space result")
	}
	return result, nil
}

// ListSpaceMembers returns memberships matching find.
func (s *Store) ListSpaceMembers(ctx context.Context, find *FindSpaceMember) ([]*SpaceMember, error) {
	return s.driver.ListSpaceMembers(ctx, find)
}

// GetSpaceMember returns the first matching membership.
func (s *Store) GetSpaceMember(ctx context.Context, find *FindSpaceMember) (*SpaceMember, error) {
	members, err := s.ListSpaceMembers(ctx, find)
	if err != nil {
		return nil, err
	}
	if len(members) == 0 {
		return nil, nil
	}
	return members[0], nil
}

// UpdateSpaceMember updates and returns a membership after checking that the
// change keeps an active ADMIN.
func (s *Store) UpdateSpaceMember(ctx context.Context, update *UpdateSpaceMember, actorUserID int32) (*SpaceMember, error) {
	if update == nil || update.Role == nil {
		return nil, errors.New("space member role is required")
	}
	if update.Role != nil && !update.Role.IsActiveMember() {
		return nil, errors.New("invalid space member role")
	}
	return s.driver.UpdateSpaceMember(ctx, update, actorUserID)
}

// DeleteSpaceMember deletes a membership after checking that the change keeps
// an active ADMIN.
func (s *Store) DeleteSpaceMember(ctx context.Context, delete *DeleteSpaceMember, actorUserID int32) error {
	if delete == nil || delete.SpaceID <= 0 || delete.UserID <= 0 || actorUserID <= 0 {
		return errors.New("space member deletion requires space, user, and actor")
	}
	return s.driver.DeleteSpaceMember(ctx, delete, actorUserID)
}

// CreateSpaceInvitation creates a pending invitation. It never creates an
// active membership; only the invitee can do that by accepting it.
func (s *Store) CreateSpaceInvitation(ctx context.Context, create *SpaceInvitation, actorUserID int32) (*SpaceInvitation, error) {
	if create == nil || create.SpaceID <= 0 || create.UserID <= 0 || actorUserID <= 0 {
		return nil, errors.New("space invitation requires space, user, and actor")
	}
	if !create.Role.IsActiveMember() {
		return nil, errors.New("invalid space invitation role")
	}
	return s.driver.CreateSpaceInvitation(ctx, create, actorUserID)
}

// ListSpaceInvitations returns pending invitations matching find.
func (s *Store) ListSpaceInvitations(ctx context.Context, find *FindSpaceInvitation) ([]*SpaceInvitation, error) {
	return s.driver.ListSpaceInvitations(ctx, find)
}

// GetSpaceInvitation returns the first matching pending invitation.
func (s *Store) GetSpaceInvitation(ctx context.Context, find *FindSpaceInvitation) (*SpaceInvitation, error) {
	invitations, err := s.ListSpaceInvitations(ctx, find)
	if err != nil {
		return nil, err
	}
	if len(invitations) == 0 {
		return nil, nil
	}
	return invitations[0], nil
}

// AcceptSpaceInvitation activates a pending invitation only when the actor is
// the invitee.
func (s *Store) AcceptSpaceInvitation(ctx context.Context, accept *AcceptSpaceInvitation, actorUserID int32) (*SpaceMember, error) {
	if accept == nil || accept.SpaceID <= 0 || accept.UserID <= 0 || actorUserID <= 0 {
		return nil, errors.New("space invitation acceptance requires space, user, and actor")
	}
	if accept.UserID != actorUserID {
		return nil, ErrSpacePermissionDenied
	}
	return s.driver.AcceptSpaceInvitation(ctx, accept, actorUserID)
}

// DeclineSpaceInvitation deletes a pending invitation only when the actor is
// the invitee.
func (s *Store) DeclineSpaceInvitation(ctx context.Context, decline *DeclineSpaceInvitation, actorUserID int32) error {
	if decline == nil || decline.SpaceID <= 0 || decline.UserID <= 0 || actorUserID <= 0 {
		return errors.New("space invitation decline requires space, user, and actor")
	}
	if decline.UserID != actorUserID {
		return ErrSpacePermissionDenied
	}
	return s.driver.DeclineSpaceInvitation(ctx, decline, actorUserID)
}

// RevokeSpaceInvitation deletes a pending invitation after authorizing an
// active space administrator.
func (s *Store) RevokeSpaceInvitation(ctx context.Context, revoke *RevokeSpaceInvitation, actorUserID int32) error {
	if revoke == nil || revoke.SpaceID <= 0 || revoke.UserID <= 0 || actorUserID <= 0 {
		return errors.New("space invitation revocation requires space, user, and actor")
	}
	return s.driver.RevokeSpaceInvitation(ctx, revoke, actorUserID)
}