Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,54 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/check.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
type CheckBlockedEmailUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, email string) (bool, error)
|
||||
}
|
||||
|
||||
type checkBlockedEmailUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo dom_blockedemail.BlockedEmailRepository
|
||||
}
|
||||
|
||||
func NewCheckBlockedEmailUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_blockedemail.BlockedEmailRepository,
|
||||
) CheckBlockedEmailUseCase {
|
||||
logger = logger.Named("CheckBlockedEmailUseCase")
|
||||
return &checkBlockedEmailUseCaseImpl{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *checkBlockedEmailUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string) (bool, error) {
|
||||
// Normalize email
|
||||
normalizedEmail := strings.ToLower(strings.TrimSpace(email))
|
||||
|
||||
isBlocked, err := uc.repo.IsBlocked(ctx, userID, normalizedEmail)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to check if email is blocked",
|
||||
zap.Any("error", err),
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
return false, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Checked blocked status",
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)),
|
||||
zap.Bool("is_blocked", isBlocked))
|
||||
|
||||
return isBlocked, nil
|
||||
}
|
||||
100
cloud/maplefile-backend/internal/usecase/blockedemail/create.go
Normal file
100
cloud/maplefile-backend/internal/usecase/blockedemail/create.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/create.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
const MaxBlockedEmails = 100
|
||||
|
||||
type CreateBlockedEmailUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, email string, blockedUserID gocql.UUID, reason string) (*dom_blockedemail.BlockedEmail, error)
|
||||
}
|
||||
|
||||
type createBlockedEmailUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo dom_blockedemail.BlockedEmailRepository
|
||||
}
|
||||
|
||||
func NewCreateBlockedEmailUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_blockedemail.BlockedEmailRepository,
|
||||
) CreateBlockedEmailUseCase {
|
||||
logger = logger.Named("CreateBlockedEmailUseCase")
|
||||
return &createBlockedEmailUseCaseImpl{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *createBlockedEmailUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string, blockedUserID gocql.UUID, reason string) (*dom_blockedemail.BlockedEmail, error) {
|
||||
// Normalize email
|
||||
normalizedEmail := strings.ToLower(strings.TrimSpace(email))
|
||||
|
||||
// Check if email is already blocked
|
||||
existing, err := uc.repo.Get(ctx, userID, normalizedEmail)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to check existing blocked email",
|
||||
zap.Any("error", err),
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
uc.logger.Debug("Email already blocked",
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
return nil, httperror.NewConflictError("This email is already blocked")
|
||||
}
|
||||
|
||||
// Check limit
|
||||
count, err := uc.repo.Count(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to count blocked emails",
|
||||
zap.Any("error", err),
|
||||
zap.Any("user_id", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if count >= MaxBlockedEmails {
|
||||
uc.logger.Warn("Blocked email limit reached",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Int("count", count),
|
||||
zap.Int("limit", MaxBlockedEmails))
|
||||
return nil, httperror.NewBadRequestError("You have reached the maximum number of blocked emails")
|
||||
}
|
||||
|
||||
// Create blocked email entry
|
||||
blockedEmail := &dom_blockedemail.BlockedEmail{
|
||||
UserID: userID,
|
||||
BlockedEmail: normalizedEmail,
|
||||
BlockedUserID: blockedUserID,
|
||||
Reason: reason,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
err = uc.repo.Create(ctx, blockedEmail)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to create blocked email",
|
||||
zap.Any("error", err),
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Info("Blocked email created",
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
|
||||
return blockedEmail, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/delete.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
type DeleteBlockedEmailUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, email string) error
|
||||
}
|
||||
|
||||
type deleteBlockedEmailUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo dom_blockedemail.BlockedEmailRepository
|
||||
}
|
||||
|
||||
func NewDeleteBlockedEmailUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_blockedemail.BlockedEmailRepository,
|
||||
) DeleteBlockedEmailUseCase {
|
||||
logger = logger.Named("DeleteBlockedEmailUseCase")
|
||||
return &deleteBlockedEmailUseCaseImpl{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *deleteBlockedEmailUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string) error {
|
||||
// Normalize email
|
||||
normalizedEmail := strings.ToLower(strings.TrimSpace(email))
|
||||
|
||||
// Check if email exists in blocked list
|
||||
existing, err := uc.repo.Get(ctx, userID, normalizedEmail)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to check existing blocked email",
|
||||
zap.Any("error", err),
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
return err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
uc.logger.Debug("Blocked email not found",
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
return httperror.NewNotFoundError("Email not found in blocked list")
|
||||
}
|
||||
|
||||
// Delete blocked email
|
||||
err = uc.repo.Delete(ctx, userID, normalizedEmail)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to delete blocked email",
|
||||
zap.Any("error", err),
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("Blocked email deleted",
|
||||
zap.Any("user_id", userID),
|
||||
zap.String("email", validation.MaskEmail(normalizedEmail)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/list.go
|
||||
package blockedemail
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
|
||||
)
|
||||
|
||||
type ListBlockedEmailsUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) ([]*dom_blockedemail.BlockedEmail, error)
|
||||
}
|
||||
|
||||
type listBlockedEmailsUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo dom_blockedemail.BlockedEmailRepository
|
||||
}
|
||||
|
||||
func NewListBlockedEmailsUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_blockedemail.BlockedEmailRepository,
|
||||
) ListBlockedEmailsUseCase {
|
||||
logger = logger.Named("ListBlockedEmailsUseCase")
|
||||
return &listBlockedEmailsUseCaseImpl{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *listBlockedEmailsUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_blockedemail.BlockedEmail, error) {
|
||||
blockedEmails, err := uc.repo.List(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to list blocked emails",
|
||||
zap.Any("error", err),
|
||||
zap.Any("user_id", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Listed blocked emails",
|
||||
zap.Any("user_id", userID),
|
||||
zap.Int("count", len(blockedEmails)))
|
||||
|
||||
return blockedEmails, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/add_member.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type AddCollectionMemberUseCase interface {
|
||||
Execute(ctx context.Context, collectionID gocql.UUID, membership *dom_collection.CollectionMembership) error
|
||||
}
|
||||
|
||||
type addCollectionMemberUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewAddCollectionMemberUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AddCollectionMemberUseCase {
|
||||
logger = logger.Named("AddCollectionMemberUseCase")
|
||||
return &addCollectionMemberUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *addCollectionMemberUseCaseImpl) Execute(ctx context.Context, collectionID gocql.UUID, membership *dom_collection.CollectionMembership) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if membership == nil {
|
||||
e["membership"] = "Membership details are required"
|
||||
} else {
|
||||
// Generate member ID if not provided
|
||||
if membership.ID.String() == "" || membership.ID.String() == "00000000-0000-0000-0000-000000000000" {
|
||||
membership.ID = gocql.TimeUUID()
|
||||
}
|
||||
|
||||
if membership.RecipientID.String() == "" {
|
||||
e["recipient_id"] = "Recipient ID is required"
|
||||
}
|
||||
if membership.RecipientEmail == "" {
|
||||
e["recipient_email"] = "Recipient email is required"
|
||||
}
|
||||
if membership.GrantedByID.String() == "" {
|
||||
e["granted_by_id"] = "Granted by ID is required"
|
||||
}
|
||||
if len(membership.EncryptedCollectionKey) == 0 {
|
||||
e["encrypted_collection_key"] = "Encrypted collection key is required"
|
||||
}
|
||||
if membership.PermissionLevel == "" {
|
||||
// Default permission level will be set in the repository
|
||||
} else if membership.PermissionLevel != dom_collection.CollectionPermissionReadOnly &&
|
||||
membership.PermissionLevel != dom_collection.CollectionPermissionReadWrite &&
|
||||
membership.PermissionLevel != dom_collection.CollectionPermissionAdmin {
|
||||
e["permission_level"] = "Invalid permission level"
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating add collection member",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Add member to collection.
|
||||
//
|
||||
|
||||
return uc.repo.AddMember(ctx, collectionID, membership)
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/add_member_to_hierarchy.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type AddMemberToHierarchyUseCase interface {
|
||||
Execute(ctx context.Context, rootID gocql.UUID, membership *dom_collection.CollectionMembership) error
|
||||
}
|
||||
|
||||
type addMemberToHierarchyUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewAddMemberToHierarchyUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AddMemberToHierarchyUseCase {
|
||||
logger = logger.Named("AddMemberToHierarchyUseCase")
|
||||
return &addMemberToHierarchyUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *addMemberToHierarchyUseCaseImpl) Execute(ctx context.Context, rootID gocql.UUID, membership *dom_collection.CollectionMembership) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if rootID.String() == "" {
|
||||
e["root_id"] = "Root collection ID is required"
|
||||
}
|
||||
if membership == nil {
|
||||
e["membership"] = "Membership details are required"
|
||||
} else {
|
||||
// Generate member ID if not provided
|
||||
if membership.ID.String() == "" || membership.ID.String() == "00000000-0000-0000-0000-000000000000" {
|
||||
membership.ID = gocql.TimeUUID()
|
||||
}
|
||||
|
||||
if membership.RecipientID.String() == "" {
|
||||
e["recipient_id"] = "Recipient ID is required"
|
||||
}
|
||||
if membership.RecipientEmail == "" {
|
||||
e["recipient_email"] = "Recipient email is required"
|
||||
}
|
||||
if membership.GrantedByID.String() == "" {
|
||||
e["granted_by_id"] = "Granted by ID is required"
|
||||
}
|
||||
if len(membership.EncryptedCollectionKey) == 0 {
|
||||
e["encrypted_collection_key"] = "Encrypted collection key is required"
|
||||
}
|
||||
if membership.PermissionLevel == "" {
|
||||
// Default permission level will be set in the repository
|
||||
} else if membership.PermissionLevel != dom_collection.CollectionPermissionReadOnly &&
|
||||
membership.PermissionLevel != dom_collection.CollectionPermissionReadWrite &&
|
||||
membership.PermissionLevel != dom_collection.CollectionPermissionAdmin {
|
||||
e["permission_level"] = "Invalid permission level"
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating add member to hierarchy",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Add member to collection hierarchy.
|
||||
//
|
||||
|
||||
return uc.repo.AddMemberToHierarchy(ctx, rootID, membership)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// monorepo/cloud/backend/internal/usecase/collection/anonymize_old_ips.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
)
|
||||
|
||||
type AnonymizeOldIPsUseCase interface {
|
||||
Execute(ctx context.Context, cutoffDate time.Time) (int, error)
|
||||
}
|
||||
|
||||
type anonymizeOldIPsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewAnonymizeOldIPsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AnonymizeOldIPsUseCase {
|
||||
logger = logger.Named("CollectionAnonymizeOldIPsUseCase")
|
||||
return &anonymizeOldIPsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *anonymizeOldIPsUseCaseImpl) Execute(ctx context.Context, cutoffDate time.Time) (int, error) {
|
||||
uc.logger.Debug("Anonymizing old IPs in collection tables",
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
|
||||
count, err := uc.repo.AnonymizeOldIPs(ctx, cutoffDate)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to anonymize old IPs in collection tables",
|
||||
zap.Error(err),
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
uc.logger.Info("Successfully anonymized old IPs in collection tables",
|
||||
zap.Int("count", count),
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/collection/anonymize_user_references.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
)
|
||||
|
||||
// AnonymizeUserReferencesUseCase handles anonymizing CreatedByUserID and ModifiedByUserID
|
||||
// references when a user is deleted, replacing them with a special "deleted user" UUID.
|
||||
type AnonymizeUserReferencesUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) (int, error)
|
||||
}
|
||||
|
||||
type anonymizeUserReferencesUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
// NewAnonymizeUserReferencesUseCase creates a new use case for anonymizing user references in collections
|
||||
func NewAnonymizeUserReferencesUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AnonymizeUserReferencesUseCase {
|
||||
return &anonymizeUserReferencesUseCaseImpl{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// DeletedUserUUID is a well-known UUID representing a deleted user
|
||||
// UUID: 00000000-0000-0000-0000-000000000001 (DELETED_USER)
|
||||
var DeletedUserUUID = gocql.UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
|
||||
|
||||
func (uc *anonymizeUserReferencesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (int, error) {
|
||||
uc.logger.Info("Anonymizing user references in collection metadata",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
// Get all collections owned by this user
|
||||
collections, err := uc.repo.GetAllByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get collections by owner",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("failed to get collections by owner: %w", err)
|
||||
}
|
||||
|
||||
updatedCount := 0
|
||||
|
||||
// Update each collection to replace user references with deleted user UUID
|
||||
for _, collection := range collections {
|
||||
needsUpdate := false
|
||||
|
||||
// Check if this collection has references to the deleted user
|
||||
if collection.CreatedByUserID == userID {
|
||||
collection.CreatedByUserID = DeletedUserUUID
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if collection.ModifiedByUserID == userID {
|
||||
collection.ModifiedByUserID = DeletedUserUUID
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
// Also anonymize GrantedByID in collection memberships
|
||||
for i := range collection.Members {
|
||||
if collection.Members[i].GrantedByID == userID {
|
||||
collection.Members[i].GrantedByID = DeletedUserUUID
|
||||
needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
// Update the collection with anonymized references
|
||||
if err := uc.repo.Update(ctx, collection); err != nil {
|
||||
uc.logger.Error("Failed to anonymize user references in collection",
|
||||
zap.String("collection_id", collection.ID.String()),
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
// Continue with other collections even if one fails
|
||||
continue
|
||||
}
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
uc.logger.Info("✅ Anonymized user references in collection metadata",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("collections_updated", updatedCount))
|
||||
|
||||
return updatedCount, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/collection/archive.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ArchiveCollectionUseCase interface {
|
||||
Execute(ctx context.Context, id gocql.UUID) error
|
||||
}
|
||||
|
||||
type archiveCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewArchiveCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) ArchiveCollectionUseCase {
|
||||
logger = logger.Named("ArchiveCollectionUseCase")
|
||||
return &archiveCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *archiveCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating collection archival",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Archive collection using repository method.
|
||||
//
|
||||
|
||||
return uc.repo.Archive(ctx, id)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/check_access.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CheckCollectionAccessUseCase interface {
|
||||
Execute(ctx context.Context, collectionID, userID gocql.UUID, requiredPermission string) (bool, error)
|
||||
}
|
||||
|
||||
type checkCollectionAccessUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewCheckCollectionAccessUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CheckCollectionAccessUseCase {
|
||||
logger = logger.Named("CheckCollectionAccessUseCase")
|
||||
return &checkCollectionAccessUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *checkCollectionAccessUseCaseImpl) Execute(ctx context.Context, collectionID, userID gocql.UUID, requiredPermission string) (bool, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if requiredPermission == "" {
|
||||
// Default to read-only if not specified
|
||||
requiredPermission = dom_collection.CollectionPermissionReadOnly
|
||||
} else if requiredPermission != dom_collection.CollectionPermissionReadOnly &&
|
||||
requiredPermission != dom_collection.CollectionPermissionReadWrite &&
|
||||
requiredPermission != dom_collection.CollectionPermissionAdmin {
|
||||
e["required_permission"] = "Invalid permission level"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating check collection access",
|
||||
zap.Any("error", e))
|
||||
return false, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Check access.
|
||||
//
|
||||
|
||||
return uc.repo.CheckAccess(ctx, collectionID, userID, requiredPermission)
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/collection/count_collections.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// CountCollectionsResponse contains the collection counts for a user
|
||||
type CountCollectionsResponse struct {
|
||||
OwnedCollections int `json:"owned_collections"`
|
||||
SharedCollections int `json:"shared_collections"`
|
||||
TotalCollections int `json:"total_collections"`
|
||||
}
|
||||
|
||||
// CountFoldersResponse contains the folder counts for a user (folders only, not albums)
|
||||
type CountFoldersResponse struct {
|
||||
OwnedFolders int `json:"owned_folders"`
|
||||
SharedFolders int `json:"shared_folders"`
|
||||
TotalFolders int `json:"total_folders"`
|
||||
}
|
||||
|
||||
type CountUserCollectionsUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) (*CountCollectionsResponse, error)
|
||||
}
|
||||
|
||||
// NEW: Use case specifically for counting folders only
|
||||
type CountUserFoldersUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) (*CountFoldersResponse, error)
|
||||
}
|
||||
|
||||
type countUserCollectionsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
type countUserFoldersUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewCountUserCollectionsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CountUserCollectionsUseCase {
|
||||
logger = logger.Named("CountUserCollectionsUseCase")
|
||||
return &countUserCollectionsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func NewCountUserFoldersUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CountUserFoldersUseCase {
|
||||
logger = logger.Named("CountUserFoldersUseCase")
|
||||
return &countUserFoldersUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *countUserCollectionsUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*CountCollectionsResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating count user collections",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Count collections.
|
||||
//
|
||||
|
||||
ownedCollections, err := uc.repo.CountOwnedCollections(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to count owned collections",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sharedCollections, err := uc.repo.CountSharedCollections(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to count shared collections",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &CountCollectionsResponse{
|
||||
OwnedCollections: ownedCollections,
|
||||
SharedCollections: sharedCollections,
|
||||
TotalCollections: ownedCollections + sharedCollections,
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully counted user collections",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("owned_collections", ownedCollections),
|
||||
zap.Int("shared_collections", sharedCollections),
|
||||
zap.Int("total_collections", response.TotalCollections))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (uc *countUserFoldersUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*CountFoldersResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating count user folders",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: DEBUG - Check what's actually in the database
|
||||
//
|
||||
|
||||
// ADD DEBUG LOGGING - Cast to concrete type to access debug method
|
||||
if debugRepo, ok := uc.repo.(interface {
|
||||
DebugCollectionRecords(context.Context, gocql.UUID) error
|
||||
}); ok {
|
||||
if debugErr := debugRepo.DebugCollectionRecords(ctx, userID); debugErr != nil {
|
||||
uc.logger.Warn("Failed to debug collection records", zap.Error(debugErr))
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Count folders with separate owned/shared counts AND total unique count
|
||||
//
|
||||
|
||||
ownedFolders, err := uc.repo.CountOwnedFolders(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to count owned folders",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sharedFolders, err := uc.repo.CountSharedFolders(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to count shared folders",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NEW: Get the deduplicated total count
|
||||
var totalUniqueFolders int
|
||||
if uniqueRepo, ok := uc.repo.(interface {
|
||||
CountTotalUniqueFolders(context.Context, gocql.UUID) (int, error)
|
||||
}); ok {
|
||||
totalUniqueFolders, err = uniqueRepo.CountTotalUniqueFolders(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to count unique total folders",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Fallback to simple addition if the method is not available
|
||||
uc.logger.Warn("CountTotalUniqueFolders method not available, using simple addition")
|
||||
totalUniqueFolders = ownedFolders + sharedFolders
|
||||
}
|
||||
|
||||
response := &CountFoldersResponse{
|
||||
OwnedFolders: ownedFolders,
|
||||
SharedFolders: sharedFolders,
|
||||
TotalFolders: totalUniqueFolders, // Use deduplicated count
|
||||
}
|
||||
|
||||
uc.logger.Info("Successfully counted user folders with deduplication",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("owned_folders", ownedFolders),
|
||||
zap.Int("shared_folders", sharedFolders),
|
||||
zap.Int("total_unique_folders", totalUniqueFolders),
|
||||
zap.Int("would_be_simple_sum", ownedFolders+sharedFolders))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/create.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CreateCollectionUseCase interface {
|
||||
Execute(ctx context.Context, collection *dom_collection.Collection) error
|
||||
}
|
||||
|
||||
type createCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewCreateCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CreateCollectionUseCase {
|
||||
logger = logger.Named("CreateCollectionUseCase")
|
||||
return &createCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *createCollectionUseCaseImpl) Execute(ctx context.Context, collection *dom_collection.Collection) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collection == nil {
|
||||
e["collection"] = "Collection is required"
|
||||
} else {
|
||||
if collection.OwnerID.String() == "" {
|
||||
e["owner_id"] = "Owner ID is required"
|
||||
}
|
||||
if collection.EncryptedName == "" {
|
||||
e["encrypted_name"] = "Collection name is required"
|
||||
}
|
||||
if collection.CollectionType == "" {
|
||||
e["collection_type"] = "Collection type is required"
|
||||
} else if collection.CollectionType != dom_collection.CollectionTypeFolder && collection.CollectionType != dom_collection.CollectionTypeAlbum {
|
||||
e["collection_type"] = "Collection type must be either 'folder' or 'album'"
|
||||
}
|
||||
if collection.EncryptedCollectionKey.Ciphertext == nil || len(collection.EncryptedCollectionKey.Ciphertext) == 0 {
|
||||
e["encrypted_collection_key"] = "Encrypted collection key is required"
|
||||
}
|
||||
if collection.State == "" {
|
||||
e["state"] = "File state is required"
|
||||
} else if collection.State != dom_collection.CollectionStateActive &&
|
||||
collection.State != dom_collection.CollectionStateDeleted &&
|
||||
collection.State != dom_collection.CollectionStateArchived {
|
||||
e["state"] = "Invalid collection state"
|
||||
}
|
||||
if err := dom_collection.IsValidStateTransition(dom_collection.CollectionStateActive, collection.State); err != nil {
|
||||
e["state"] = err.Error()
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating collection creation",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Insert into database.
|
||||
//
|
||||
|
||||
return uc.repo.Create(ctx, collection)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/find_by_parent.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type FindCollectionsByParentUseCase interface {
|
||||
Execute(ctx context.Context, parentID gocql.UUID) ([]*dom_collection.Collection, error)
|
||||
}
|
||||
|
||||
type findCollectionsByParentUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewFindCollectionsByParentUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) FindCollectionsByParentUseCase {
|
||||
logger = logger.Named("FindCollectionsByParentUseCase")
|
||||
return &findCollectionsByParentUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *findCollectionsByParentUseCaseImpl) Execute(ctx context.Context, parentID gocql.UUID) ([]*dom_collection.Collection, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if parentID.String() == "" {
|
||||
e["parent_id"] = "Parent ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating find collections by parent",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Find collections by parent.
|
||||
//
|
||||
|
||||
return uc.repo.FindByParent(ctx, parentID)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/find_descendants.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type FindDescendantsUseCase interface {
|
||||
Execute(ctx context.Context, collectionID gocql.UUID) ([]*dom_collection.Collection, error)
|
||||
}
|
||||
|
||||
type findDescendantsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewFindDescendantsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) FindDescendantsUseCase {
|
||||
logger = logger.Named("FindDescendantsUseCase")
|
||||
return &findDescendantsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *findDescendantsUseCaseImpl) Execute(ctx context.Context, collectionID gocql.UUID) ([]*dom_collection.Collection, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating find descendants",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Find descendants.
|
||||
//
|
||||
|
||||
return uc.repo.FindDescendants(ctx, collectionID)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/find_root_collections.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type FindRootCollectionsUseCase interface {
|
||||
Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_collection.Collection, error)
|
||||
}
|
||||
|
||||
type findRootCollectionsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewFindRootCollectionsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) FindRootCollectionsUseCase {
|
||||
logger = logger.Named("FindRootCollectionsUseCase")
|
||||
return &findRootCollectionsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *findRootCollectionsUseCaseImpl) Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_collection.Collection, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if ownerID.String() == "" {
|
||||
e["owner_id"] = "Owner ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating find root collections",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Find root collections.
|
||||
//
|
||||
|
||||
return uc.repo.FindRootCollections(ctx, ownerID)
|
||||
}
|
||||
65
cloud/maplefile-backend/internal/usecase/collection/get.go
Normal file
65
cloud/maplefile-backend/internal/usecase/collection/get.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/get.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetCollectionUseCase interface {
|
||||
Execute(ctx context.Context, id gocql.UUID) (*dom_collection.Collection, error)
|
||||
}
|
||||
|
||||
type getCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewGetCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) GetCollectionUseCase {
|
||||
logger = logger.Named("GetCollectionUseCase")
|
||||
return &getCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) (*dom_collection.Collection, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating collection retrieval",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
collection, err := uc.repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if collection == nil {
|
||||
uc.logger.Debug("Collection not found",
|
||||
zap.Any("id", id))
|
||||
return nil, httperror.NewForNotFoundWithSingleField("message", "Collection not found")
|
||||
}
|
||||
|
||||
return collection, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/get_filtered.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFilteredCollectionsUseCase interface {
|
||||
Execute(ctx context.Context, options dom_collection.CollectionFilterOptions) (*dom_collection.CollectionFilterResult, error)
|
||||
}
|
||||
|
||||
type getFilteredCollectionsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewGetFilteredCollectionsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) GetFilteredCollectionsUseCase {
|
||||
logger = logger.Named("GetFilteredCollectionsUseCase")
|
||||
return &getFilteredCollectionsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getFilteredCollectionsUseCaseImpl) Execute(ctx context.Context, options dom_collection.CollectionFilterOptions) (*dom_collection.CollectionFilterResult, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if options.UserID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if !options.IsValid() {
|
||||
e["filter_options"] = "At least one filter option (include_owned or include_shared) must be enabled"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get filtered collections",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get filtered collections from repository.
|
||||
//
|
||||
|
||||
result, err := uc.repo.GetCollectionsWithFilter(ctx, options)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get filtered collections from repository",
|
||||
zap.Any("error", err),
|
||||
zap.Any("options", options))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully retrieved filtered collections",
|
||||
zap.Int("owned_count", len(result.OwnedCollections)),
|
||||
zap.Int("shared_count", len(result.SharedCollections)),
|
||||
zap.Int("total_count", result.TotalCount),
|
||||
zap.Any("user_id", options.UserID))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/get_sync_data.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
)
|
||||
|
||||
type GetCollectionSyncDataUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, cursor *dom_collection.CollectionSyncCursor, limit int64, accessType string) (*dom_collection.CollectionSyncResponse, error)
|
||||
}
|
||||
|
||||
type getCollectionSyncDataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewGetCollectionSyncDataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) GetCollectionSyncDataUseCase {
|
||||
logger = logger.Named("GetCollectionSyncDataUseCase")
|
||||
return &getCollectionSyncDataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getCollectionSyncDataUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, cursor *dom_collection.CollectionSyncCursor, limit int64, accessType string) (*dom_collection.CollectionSyncResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
// (Skip)
|
||||
|
||||
//
|
||||
// STEP 2: Get filtered collections from repository.
|
||||
//
|
||||
|
||||
if accessType != dom_collection.CollectionAccessTypeMember && accessType != dom_collection.CollectionAccessTypeOwner {
|
||||
result, err := uc.repo.GetCollectionSyncData(ctx, userID, cursor, limit)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get filtered collections from repository",
|
||||
zap.Any("error", err),
|
||||
zap.Any("userID", userID),
|
||||
zap.Any("cursor", cursor),
|
||||
zap.Int64("limit", limit))
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result, err := uc.repo.GetCollectionSyncDataByAccessType(ctx, userID, cursor, limit, accessType)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get filtered collections from repository",
|
||||
zap.Any("error", err),
|
||||
zap.Any("userID", userID),
|
||||
zap.Any("cursor", cursor),
|
||||
zap.Int64("limit", limit),
|
||||
zap.String("access_type", accessType))
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/harddelete.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// HardDeleteCollectionUseCase permanently deletes a collection
|
||||
// Used for GDPR right-to-be-forgotten implementation
|
||||
type HardDeleteCollectionUseCase interface {
|
||||
Execute(ctx context.Context, id gocql.UUID) error
|
||||
}
|
||||
|
||||
type hardDeleteCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewHardDeleteCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) HardDeleteCollectionUseCase {
|
||||
logger = logger.Named("HardDeleteCollectionUseCase")
|
||||
return &hardDeleteCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *hardDeleteCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating collection hard deletion",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Hard delete from database (no tombstone).
|
||||
//
|
||||
|
||||
uc.logger.Info("Hard deleting collection (GDPR mode)",
|
||||
zap.String("collection_id", id.String()))
|
||||
|
||||
err := uc.repo.HardDelete(ctx, id)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to hard delete collection",
|
||||
zap.String("collection_id", id.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("✅ Collection hard deleted successfully",
|
||||
zap.String("collection_id", id.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package collection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
// NOTE: Unit tests for HardDeleteCollectionUseCase would require mocks.
|
||||
// For now, this use case will be tested via integration tests.
|
||||
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
|
||||
|
||||
func TestHardDeleteCollectionUseCase_Constructor(t *testing.T) {
|
||||
// Test that constructor creates use case successfully
|
||||
cfg := &config.Configuration{}
|
||||
logger := zap.NewNop()
|
||||
|
||||
useCase := NewHardDeleteCollectionUseCase(cfg, logger, nil)
|
||||
|
||||
if useCase == nil {
|
||||
t.Error("Expected use case to be created, got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/list_by_user.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListCollectionsByUserUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error)
|
||||
}
|
||||
|
||||
type listCollectionsByUserUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewListCollectionsByUserUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) ListCollectionsByUserUseCase {
|
||||
logger = logger.Named("ListCollectionsByUserUseCase")
|
||||
return &listCollectionsByUserUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *listCollectionsByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating list collections by user",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
return uc.repo.GetAllByUserID(ctx, userID)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/list_shared_with_user.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListCollectionsSharedWithUserUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error)
|
||||
}
|
||||
|
||||
type listCollectionsSharedWithUserUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewListCollectionsSharedWithUserUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) ListCollectionsSharedWithUserUseCase {
|
||||
logger = logger.Named("ListCollectionsSharedWithUserUseCase")
|
||||
return &listCollectionsSharedWithUserUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *listCollectionsSharedWithUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating list shared collections",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
return uc.repo.GetCollectionsSharedWithUser(ctx, userID)
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/move_collection.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// MoveCollectionRequest contains data needed to move a collection
|
||||
type MoveCollectionRequest struct {
|
||||
CollectionID gocql.UUID `json:"collection_id"`
|
||||
NewParentID gocql.UUID `json:"new_parent_id"`
|
||||
UpdatedAncestors []gocql.UUID `json:"updated_ancestors"`
|
||||
UpdatedPathSegments []string `json:"updated_path_segments"`
|
||||
}
|
||||
|
||||
type MoveCollectionUseCase interface {
|
||||
Execute(ctx context.Context, request MoveCollectionRequest) error
|
||||
}
|
||||
|
||||
type moveCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewMoveCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) MoveCollectionUseCase {
|
||||
logger = logger.Named("MoveCollectionUseCase")
|
||||
return &moveCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *moveCollectionUseCaseImpl) Execute(ctx context.Context, request MoveCollectionRequest) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if request.CollectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if request.NewParentID.String() == "" {
|
||||
e["new_parent_id"] = "New parent ID is required"
|
||||
}
|
||||
if len(request.UpdatedAncestors) == 0 {
|
||||
e["updated_ancestors"] = "Updated ancestors are required"
|
||||
}
|
||||
if len(request.UpdatedPathSegments) == 0 {
|
||||
e["updated_path_segments"] = "Updated path segments are required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating move collection",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Move collection.
|
||||
//
|
||||
|
||||
return uc.repo.MoveCollection(
|
||||
ctx,
|
||||
request.CollectionID,
|
||||
request.NewParentID,
|
||||
request.UpdatedAncestors,
|
||||
request.UpdatedPathSegments,
|
||||
)
|
||||
}
|
||||
216
cloud/maplefile-backend/internal/usecase/collection/provider.go
Normal file
216
cloud/maplefile-backend/internal/usecase/collection/provider.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package collection
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
)
|
||||
|
||||
// Wire providers for collection use cases
|
||||
|
||||
func ProvideCreateCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CreateCollectionUseCase {
|
||||
return NewCreateCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideUpdateCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) UpdateCollectionUseCase {
|
||||
return NewUpdateCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) GetCollectionUseCase {
|
||||
return NewGetCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideSoftDeleteCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) SoftDeleteCollectionUseCase {
|
||||
return NewSoftDeleteCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideArchiveCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) ArchiveCollectionUseCase {
|
||||
return NewArchiveCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideRestoreCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) RestoreCollectionUseCase {
|
||||
return NewRestoreCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideListCollectionsByUserUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) ListCollectionsByUserUseCase {
|
||||
return NewListCollectionsByUserUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideListCollectionsSharedWithUserUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) ListCollectionsSharedWithUserUseCase {
|
||||
return NewListCollectionsSharedWithUserUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideFindRootCollectionsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) FindRootCollectionsUseCase {
|
||||
return NewFindRootCollectionsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideFindCollectionsByParentUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) FindCollectionsByParentUseCase {
|
||||
return NewFindCollectionsByParentUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetCollectionSyncDataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) GetCollectionSyncDataUseCase {
|
||||
return NewGetCollectionSyncDataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideCheckCollectionAccessUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CheckCollectionAccessUseCase {
|
||||
return NewCheckCollectionAccessUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideCountUserCollectionsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CountUserCollectionsUseCase {
|
||||
return NewCountUserCollectionsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideCountUserFoldersUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) CountUserFoldersUseCase {
|
||||
return NewCountUserFoldersUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideUpdateMemberPermissionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) UpdateMemberPermissionUseCase {
|
||||
return NewUpdateMemberPermissionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideMoveCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) MoveCollectionUseCase {
|
||||
return NewMoveCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetFilteredCollectionsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) GetFilteredCollectionsUseCase {
|
||||
return NewGetFilteredCollectionsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideAddMemberToHierarchyUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AddMemberToHierarchyUseCase {
|
||||
return NewAddMemberToHierarchyUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideRemoveMemberFromHierarchyUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) RemoveMemberFromHierarchyUseCase {
|
||||
return NewRemoveMemberFromHierarchyUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideFindDescendantsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) FindDescendantsUseCase {
|
||||
return NewFindDescendantsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideAddCollectionMemberUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AddCollectionMemberUseCase {
|
||||
return NewAddCollectionMemberUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideRemoveCollectionMemberUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) RemoveCollectionMemberUseCase {
|
||||
return NewRemoveCollectionMemberUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideAnonymizeOldIPsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AnonymizeOldIPsUseCase {
|
||||
return NewAnonymizeOldIPsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideRemoveUserFromAllCollectionsUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) RemoveUserFromAllCollectionsUseCase {
|
||||
return NewRemoveUserFromAllCollectionsUseCase(logger, repo)
|
||||
}
|
||||
|
||||
func ProvideHardDeleteCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) HardDeleteCollectionUseCase {
|
||||
return NewHardDeleteCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideAnonymizeUserReferencesUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) AnonymizeUserReferencesUseCase {
|
||||
return NewAnonymizeUserReferencesUseCase(logger, repo)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/remove_member.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RemoveCollectionMemberUseCase interface {
|
||||
Execute(ctx context.Context, collectionID, recipientID gocql.UUID) error
|
||||
}
|
||||
|
||||
type removeCollectionMemberUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewRemoveCollectionMemberUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) RemoveCollectionMemberUseCase {
|
||||
logger = logger.Named("RemoveCollectionMemberUseCase")
|
||||
return &removeCollectionMemberUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *removeCollectionMemberUseCaseImpl) Execute(ctx context.Context, collectionID, recipientID gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if recipientID.String() == "" {
|
||||
e["recipient_id"] = "Recipient ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating remove collection member",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Remove member from collection.
|
||||
//
|
||||
|
||||
return uc.repo.RemoveMember(ctx, collectionID, recipientID)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/remove_member_from_hierarchy.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RemoveMemberFromHierarchyUseCase interface {
|
||||
Execute(ctx context.Context, rootID, recipientID gocql.UUID) error
|
||||
}
|
||||
|
||||
type removeMemberFromHierarchyUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewRemoveMemberFromHierarchyUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) RemoveMemberFromHierarchyUseCase {
|
||||
logger = logger.Named("RemoveMemberFromHierarchyUseCase")
|
||||
return &removeMemberFromHierarchyUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *removeMemberFromHierarchyUseCaseImpl) Execute(ctx context.Context, rootID, recipientID gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if rootID.String() == "" {
|
||||
e["root_id"] = "Root collection ID is required"
|
||||
}
|
||||
if recipientID.String() == "" {
|
||||
e["recipient_id"] = "Recipient ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating remove member from hierarchy",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Remove member from collection hierarchy.
|
||||
//
|
||||
|
||||
return uc.repo.RemoveMemberFromHierarchy(ctx, rootID, recipientID)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
)
|
||||
|
||||
// RemoveUserFromAllCollectionsUseCase removes a user from all collections they are a member of
|
||||
// Used for GDPR right-to-be-forgotten implementation
|
||||
type RemoveUserFromAllCollectionsUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, userEmail string) (int, error)
|
||||
}
|
||||
|
||||
type removeUserFromAllCollectionsUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo collection.CollectionRepository
|
||||
}
|
||||
|
||||
// NewRemoveUserFromAllCollectionsUseCase creates a new use case for removing user from all collections
|
||||
func NewRemoveUserFromAllCollectionsUseCase(
|
||||
logger *zap.Logger,
|
||||
repo collection.CollectionRepository,
|
||||
) RemoveUserFromAllCollectionsUseCase {
|
||||
return &removeUserFromAllCollectionsUseCaseImpl{
|
||||
logger: logger.Named("RemoveUserFromAllCollectionsUseCase"),
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *removeUserFromAllCollectionsUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, userEmail string) (int, error) {
|
||||
uc.logger.Info("Removing user from all shared collections",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
modifiedCollections, err := uc.repo.RemoveUserFromAllCollections(ctx, userID, userEmail)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to remove user from all collections",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := len(modifiedCollections)
|
||||
|
||||
uc.logger.Info("✅ Successfully removed user from all shared collections",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("collections_modified", count))
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package collection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// NOTE: Unit tests for RemoveUserFromAllCollectionsUseCase would require mocks.
|
||||
// For now, this use case will be tested via integration tests.
|
||||
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
|
||||
|
||||
func TestRemoveUserFromAllCollectionsUseCase_Constructor(t *testing.T) {
|
||||
// Test that constructor creates use case successfully
|
||||
logger := zap.NewNop()
|
||||
|
||||
useCase := NewRemoveUserFromAllCollectionsUseCase(logger, nil)
|
||||
|
||||
if useCase == nil {
|
||||
t.Error("Expected use case to be created, got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/collection/restore.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RestoreCollectionUseCase interface {
|
||||
Execute(ctx context.Context, id gocql.UUID) error
|
||||
}
|
||||
|
||||
type restoreCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewRestoreCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) RestoreCollectionUseCase {
|
||||
logger = logger.Named("RestoreCollectionUseCase")
|
||||
return &restoreCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *restoreCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating collection restoration",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Restore collection using repository method.
|
||||
//
|
||||
|
||||
return uc.repo.Restore(ctx, id)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/softdelete.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type SoftDeleteCollectionUseCase interface {
|
||||
Execute(ctx context.Context, id gocql.UUID) error
|
||||
}
|
||||
|
||||
type softDeleteCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewSoftDeleteCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) SoftDeleteCollectionUseCase {
|
||||
logger = logger.Named("SoftDeleteCollectionUseCase")
|
||||
return &softDeleteCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *softDeleteCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating collection deletion",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Delete from database.
|
||||
//
|
||||
|
||||
return uc.repo.SoftDelete(ctx, id)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/update.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UpdateCollectionUseCase interface {
|
||||
Execute(ctx context.Context, collection *dom_collection.Collection) error
|
||||
}
|
||||
|
||||
type updateCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewUpdateCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) UpdateCollectionUseCase {
|
||||
logger = logger.Named("UpdateCollectionUseCase")
|
||||
return &updateCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *updateCollectionUseCaseImpl) Execute(ctx context.Context, collection *dom_collection.Collection) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collection == nil {
|
||||
e["collection"] = "Collection is required"
|
||||
} else {
|
||||
if collection.ID.String() == "" {
|
||||
e["id"] = "Collection ID is required"
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating collection update",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Update in database.
|
||||
//
|
||||
|
||||
return uc.repo.Update(ctx, collection)
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/collection/update_member_permission.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UpdateMemberPermissionUseCase interface {
|
||||
Execute(ctx context.Context, collectionID, recipientID gocql.UUID, newPermission string) error
|
||||
}
|
||||
|
||||
type updateMemberPermissionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewUpdateMemberPermissionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_collection.CollectionRepository,
|
||||
) UpdateMemberPermissionUseCase {
|
||||
logger = logger.Named("UpdateMemberPermissionUseCase")
|
||||
return &updateMemberPermissionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *updateMemberPermissionUseCaseImpl) Execute(ctx context.Context, collectionID, recipientID gocql.UUID, newPermission string) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if recipientID.String() == "" {
|
||||
e["recipient_id"] = "Recipient ID is required"
|
||||
}
|
||||
if newPermission == "" {
|
||||
// Default permission level will be set in the repository
|
||||
} else if newPermission != dom_collection.CollectionPermissionReadOnly &&
|
||||
newPermission != dom_collection.CollectionPermissionReadWrite &&
|
||||
newPermission != dom_collection.CollectionPermissionAdmin {
|
||||
e["permission_level"] = "Invalid permission level"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating update member permission",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Update member permission.
|
||||
//
|
||||
|
||||
return uc.repo.UpdateMemberPermission(ctx, collectionID, recipientID, newPermission)
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package emailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
domain "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/templatedemailer"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type SendUserPasswordResetEmailUseCase interface {
|
||||
Execute(ctx context.Context, user *domain.User) error
|
||||
}
|
||||
type sendUserPasswordResetEmailUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
emailer templatedemailer.TemplatedEmailer
|
||||
}
|
||||
|
||||
func NewSendUserPasswordResetEmailUseCase(config *config.Configuration, logger *zap.Logger, emailer templatedemailer.TemplatedEmailer) SendUserPasswordResetEmailUseCase {
|
||||
logger = logger.Named("SendUserPasswordResetEmailUseCase")
|
||||
return &sendUserPasswordResetEmailUseCaseImpl{config, logger, emailer}
|
||||
}
|
||||
|
||||
func (uc *sendUserPasswordResetEmailUseCaseImpl) Execute(ctx context.Context, user *domain.User) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if user == nil {
|
||||
e["user"] = "User is missing value"
|
||||
} else {
|
||||
if user.FirstName == "" {
|
||||
e["first_name"] = "First name is required"
|
||||
}
|
||||
if user.Email == "" {
|
||||
e["email"] = "Email is required"
|
||||
}
|
||||
if user.SecurityData.Code == "" {
|
||||
e["code"] = "Code is required for password reset verification "
|
||||
}
|
||||
if user.SecurityData.CodeType != domain.UserCodeTypePasswordReset {
|
||||
e["code_type"] = "Code type is required for password reset verification "
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Validation failed for upsert",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Send email
|
||||
//
|
||||
|
||||
return uc.emailer.SendUserPasswordResetEmail(ctx, user.Email, user.SecurityData.Code, user.FirstName)
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package emailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
domain "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/templatedemailer"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type SendUserVerificationEmailUseCase interface {
|
||||
Execute(ctx context.Context, user *domain.User) error
|
||||
}
|
||||
type sendUserVerificationEmailUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
emailer templatedemailer.TemplatedEmailer
|
||||
}
|
||||
|
||||
func NewSendUserVerificationEmailUseCase(config *config.Configuration, logger *zap.Logger, emailer templatedemailer.TemplatedEmailer) SendUserVerificationEmailUseCase {
|
||||
logger = logger.Named("SendUserVerificationEmailUseCase")
|
||||
return &sendUserVerificationEmailUseCaseImpl{config, logger, emailer}
|
||||
}
|
||||
|
||||
func (uc *sendUserVerificationEmailUseCaseImpl) Execute(ctx context.Context, user *domain.User) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if user == nil {
|
||||
e["user"] = "User is missing value"
|
||||
} else {
|
||||
if user.FirstName == "" {
|
||||
e["first_name"] = "First name is required"
|
||||
}
|
||||
if user.Email == "" {
|
||||
e["email"] = "Email is required"
|
||||
}
|
||||
if user.SecurityData.Code == "" {
|
||||
e["code"] = "Code is required for password reset verification "
|
||||
}
|
||||
if user.SecurityData.CodeType != domain.UserCodeTypePasswordReset {
|
||||
e["code_type"] = "Code type is required for password reset verification "
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Validation failed for upsert",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Send email
|
||||
//
|
||||
|
||||
return uc.emailer.SendUserVerificationEmail(ctx, user.Email, user.SecurityData.Code, user.FirstName)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// monorepo/cloud/backend/internal/usecase/filemetadata/anonymize_old_ips.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
)
|
||||
|
||||
type AnonymizeOldIPsUseCase interface {
|
||||
Execute(ctx context.Context, cutoffDate time.Time) (int, error)
|
||||
}
|
||||
|
||||
type anonymizeOldIPsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewAnonymizeOldIPsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) AnonymizeOldIPsUseCase {
|
||||
logger = logger.Named("FileMetadataAnonymizeOldIPsUseCase")
|
||||
return &anonymizeOldIPsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *anonymizeOldIPsUseCaseImpl) Execute(ctx context.Context, cutoffDate time.Time) (int, error) {
|
||||
uc.logger.Debug("Anonymizing old IPs in file metadata tables",
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
|
||||
count, err := uc.repo.AnonymizeOldIPs(ctx, cutoffDate)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to anonymize old IPs in file metadata tables",
|
||||
zap.Error(err),
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
uc.logger.Info("Successfully anonymized old IPs in file metadata tables",
|
||||
zap.Int("count", count),
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata/anonymize_user_references.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
)
|
||||
|
||||
// AnonymizeUserReferencesUseCase handles anonymizing CreatedByUserID and ModifiedByUserID
|
||||
// references when a user is deleted, replacing them with a special "deleted user" UUID.
|
||||
type AnonymizeUserReferencesUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) (int, error)
|
||||
}
|
||||
|
||||
type anonymizeUserReferencesUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
// NewAnonymizeUserReferencesUseCase creates a new use case for anonymizing user references in files
|
||||
func NewAnonymizeUserReferencesUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) AnonymizeUserReferencesUseCase {
|
||||
return &anonymizeUserReferencesUseCaseImpl{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
// DeletedUserUUID is a well-known UUID representing a deleted user
|
||||
// UUID: 00000000-0000-0000-0000-000000000001 (DELETED_USER)
|
||||
var DeletedUserUUID = gocql.UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
|
||||
|
||||
func (uc *anonymizeUserReferencesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (int, error) {
|
||||
uc.logger.Info("Anonymizing user references in file metadata",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
// Get all files created or modified by this user
|
||||
files, err := uc.repo.GetByCreatedByUserID(userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get files by created_by_user_id",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return 0, fmt.Errorf("failed to get files by creator: %w", err)
|
||||
}
|
||||
|
||||
updatedCount := 0
|
||||
|
||||
// Update each file to replace user references with deleted user UUID
|
||||
for _, file := range files {
|
||||
needsUpdate := false
|
||||
|
||||
// Check if this file has references to the deleted user
|
||||
if file.CreatedByUserID == userID {
|
||||
file.CreatedByUserID = DeletedUserUUID
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if file.ModifiedByUserID == userID {
|
||||
file.ModifiedByUserID = DeletedUserUUID
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if needsUpdate {
|
||||
// Update the file with anonymized references
|
||||
if err := uc.repo.Update(file); err != nil {
|
||||
uc.logger.Error("Failed to anonymize user references in file",
|
||||
zap.String("file_id", file.ID.String()),
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
// Continue with other files even if one fails
|
||||
continue
|
||||
}
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
uc.logger.Info("✅ Anonymized user references in file metadata",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("files_updated", updatedCount))
|
||||
|
||||
return updatedCount, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/check_access.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CheckFileAccessUseCase interface {
|
||||
Execute(fileID, userID gocql.UUID) (bool, error)
|
||||
}
|
||||
|
||||
type checkFileAccessUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewCheckFileAccessUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CheckFileAccessUseCase {
|
||||
logger = logger.Named("CheckFileAccessUseCase")
|
||||
return &checkFileAccessUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *checkFileAccessUseCaseImpl) Execute(fileID, userID gocql.UUID) (bool, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if fileID.String() == "" {
|
||||
e["file_id"] = "File ID is required"
|
||||
}
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file access check",
|
||||
zap.Any("error", e))
|
||||
return false, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Check access in database.
|
||||
//
|
||||
|
||||
return uc.repo.CheckIfUserHasAccess(fileID, userID)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/check_exists.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CheckFileExistsUseCase interface {
|
||||
Execute(id gocql.UUID) (bool, error)
|
||||
}
|
||||
|
||||
type checkFileExistsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewCheckFileExistsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CheckFileExistsUseCase {
|
||||
logger = logger.Named("CheckFileExistsUseCase")
|
||||
return &checkFileExistsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *checkFileExistsUseCaseImpl) Execute(id gocql.UUID) (bool, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "File ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file existence check",
|
||||
zap.Any("error", e))
|
||||
return false, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Check existence in database.
|
||||
//
|
||||
|
||||
return uc.repo.CheckIfExistsByID(id)
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/count_files.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// CountFilesResponse contains the file count for a user
|
||||
type CountFilesResponse struct {
|
||||
TotalFiles int `json:"total_files"`
|
||||
}
|
||||
|
||||
type CountUserFilesUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) (*CountFilesResponse, error)
|
||||
}
|
||||
|
||||
type countUserFilesUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewCountUserFilesUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) CountUserFilesUseCase {
|
||||
logger = logger.Named("CountUserFilesUseCase")
|
||||
return &countUserFilesUseCaseImpl{config, logger, fileRepo, collectionRepo}
|
||||
}
|
||||
|
||||
func (uc *countUserFilesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*CountFilesResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating count user files",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get accessible collections for the user.
|
||||
//
|
||||
|
||||
// Get collections using the efficient filtered query
|
||||
filterOptions := dom_collection.CollectionFilterOptions{
|
||||
UserID: userID,
|
||||
IncludeOwned: true,
|
||||
IncludeShared: true,
|
||||
}
|
||||
|
||||
collectionResult, err := uc.collectionRepo.GetCollectionsWithFilter(ctx, filterOptions)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get accessible collections for file count",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract collection IDs
|
||||
allCollections := collectionResult.GetAllCollections()
|
||||
accessibleCollectionIDs := make([]gocql.UUID, 0, len(allCollections))
|
||||
for _, collection := range allCollections {
|
||||
accessibleCollectionIDs = append(accessibleCollectionIDs, collection.ID)
|
||||
}
|
||||
|
||||
uc.logger.Debug("Found accessible collections for file counting",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("owned_collections", len(collectionResult.OwnedCollections)),
|
||||
zap.Int("shared_collections", len(collectionResult.SharedCollections)),
|
||||
zap.Int("total_accessible", len(accessibleCollectionIDs)))
|
||||
|
||||
//
|
||||
// STEP 3: Count files in accessible collections.
|
||||
//
|
||||
|
||||
fileCount, err := uc.fileRepo.CountFilesByUser(ctx, userID, accessibleCollectionIDs)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to count files for user",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &CountFilesResponse{
|
||||
TotalFiles: fileCount,
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully counted user files",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
|
||||
zap.Int("total_files", fileCount))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/create.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CreateFileMetadataUseCase interface {
|
||||
Execute(file *dom_file.File) error
|
||||
}
|
||||
|
||||
type createFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewCreateFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CreateFileMetadataUseCase {
|
||||
logger = logger.Named("CreateFileMetadataUseCase")
|
||||
return &createFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *createFileMetadataUseCaseImpl) Execute(file *dom_file.File) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if file == nil {
|
||||
e["file"] = "File is required"
|
||||
} else {
|
||||
if file.CollectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if file.OwnerID.String() == "" {
|
||||
e["owner_id"] = "Owner ID is required"
|
||||
}
|
||||
if file.EncryptedMetadata == "" {
|
||||
e["encrypted_metadata"] = "Encrypted metadata is required"
|
||||
}
|
||||
if file.EncryptedFileKey.Ciphertext == nil || len(file.EncryptedFileKey.Ciphertext) == 0 {
|
||||
e["encrypted_file_key"] = "Encrypted file key is required"
|
||||
}
|
||||
if file.EncryptionVersion == "" {
|
||||
e["encryption_version"] = "Encryption version is required"
|
||||
}
|
||||
if file.EncryptedHash == "" {
|
||||
e["encrypted_hash"] = "Encrypted hash is required"
|
||||
}
|
||||
if file.EncryptedFileObjectKey == "" {
|
||||
e["encrypted_file_object_key"] = "Encrypted file object key is required"
|
||||
}
|
||||
if file.EncryptedFileSizeInBytes <= 0 {
|
||||
e["encrypted_file_size_in_bytes"] = "Encrypted file size must be greater than 0"
|
||||
}
|
||||
if file.State == "" {
|
||||
e["state"] = "File state is required"
|
||||
} else if file.State != dom_file.FileStatePending &&
|
||||
file.State != dom_file.FileStateActive &&
|
||||
file.State != dom_file.FileStateDeleted &&
|
||||
file.State != dom_file.FileStateArchived {
|
||||
e["state"] = "Invalid file state"
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata creation",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Insert into database.
|
||||
//
|
||||
|
||||
return uc.repo.Create(file)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/create_many.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CreateManyFileMetadataUseCase interface {
|
||||
Execute(files []*dom_file.File) error
|
||||
}
|
||||
|
||||
type createManyFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewCreateManyFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CreateManyFileMetadataUseCase {
|
||||
logger = logger.Named("CreateManyFileMetadataUseCase")
|
||||
return &createManyFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *createManyFileMetadataUseCaseImpl) Execute(files []*dom_file.File) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if files == nil || len(files) == 0 {
|
||||
e["files"] = "Files are required"
|
||||
} else {
|
||||
for i, file := range files {
|
||||
if file == nil {
|
||||
e[fmt.Sprintf("files[%d]", i)] = "File is required"
|
||||
continue
|
||||
}
|
||||
if file.CollectionID.String() == "" {
|
||||
e[fmt.Sprintf("files[%d].collection_id", i)] = "Collection ID is required"
|
||||
}
|
||||
if file.OwnerID.String() == "" {
|
||||
e[fmt.Sprintf("files[%d].owner_id", i)] = "Owner ID is required"
|
||||
}
|
||||
if file.EncryptedMetadata == "" {
|
||||
e[fmt.Sprintf("files[%d].encrypted_metadata", i)] = "Encrypted metadata is required"
|
||||
}
|
||||
if file.EncryptedFileKey.Ciphertext == nil || len(file.EncryptedFileKey.Ciphertext) == 0 {
|
||||
e[fmt.Sprintf("files[%d].encrypted_file_key", i)] = "Encrypted file key is required"
|
||||
}
|
||||
if file.EncryptionVersion == "" {
|
||||
e[fmt.Sprintf("files[%d].encryption_version", i)] = "Encryption version is required"
|
||||
}
|
||||
if file.EncryptedHash == "" {
|
||||
e[fmt.Sprintf("files[%d].encrypted_hash", i)] = "Encrypted hash is required"
|
||||
}
|
||||
if file.EncryptedFileObjectKey == "" {
|
||||
e[fmt.Sprintf("files[%d].encrypted_file_object_key", i)] = "Encrypted file object key is required"
|
||||
}
|
||||
if file.EncryptedFileSizeInBytes <= 0 {
|
||||
e[fmt.Sprintf("files[%d].encrypted_file_size_in_bytes", i)] = "Encrypted file size must be greater than 0"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata batch creation",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Insert into database.
|
||||
//
|
||||
|
||||
return uc.repo.CreateMany(files)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/delete_many.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type DeleteManyFileMetadataUseCase interface {
|
||||
Execute(ids []gocql.UUID) error
|
||||
}
|
||||
|
||||
type deleteManyFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewDeleteManyFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) DeleteManyFileMetadataUseCase {
|
||||
logger = logger.Named("DeleteManyFileMetadataUseCase")
|
||||
return &deleteManyFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *deleteManyFileMetadataUseCaseImpl) Execute(ids []gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if ids == nil || len(ids) == 0 {
|
||||
e["ids"] = "File IDs are required"
|
||||
} else {
|
||||
for i, id := range ids {
|
||||
if id.String() == "" {
|
||||
e[fmt.Sprintf("ids[%d]", i)] = "File ID is required"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata batch deletion",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Delete from database.
|
||||
//
|
||||
|
||||
return uc.repo.SoftDeleteMany(ids)
|
||||
}
|
||||
63
cloud/maplefile-backend/internal/usecase/filemetadata/get.go
Normal file
63
cloud/maplefile-backend/internal/usecase/filemetadata/get.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFileMetadataUseCase interface {
|
||||
Execute(id gocql.UUID) (*dom_file.File, error)
|
||||
}
|
||||
|
||||
type getFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewGetFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataUseCase {
|
||||
logger = logger.Named("GetFileMetadataUseCase")
|
||||
return &getFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getFileMetadataUseCaseImpl) Execute(id gocql.UUID) (*dom_file.File, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "File ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata retrieval",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
file, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
uc.logger.Debug("File metadata not found",
|
||||
zap.Any("id", id))
|
||||
return nil, httperror.NewForNotFoundWithSingleField("message", "File not found")
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_collection.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFileMetadataByCollectionUseCase interface {
|
||||
Execute(collectionID gocql.UUID) ([]*dom_file.File, error)
|
||||
}
|
||||
|
||||
type getFileMetadataByCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewGetFileMetadataByCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByCollectionUseCase {
|
||||
logger = logger.Named("GetFileMetadataByCollectionUseCase")
|
||||
return &getFileMetadataByCollectionUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getFileMetadataByCollectionUseCaseImpl) Execute(collectionID gocql.UUID) ([]*dom_file.File, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata retrieval by collection",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
return uc.repo.GetByCollection(collectionID)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_created_by_user_id.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFileMetadataByCreatedByUserIDUseCase interface {
|
||||
Execute(createdByUserID gocql.UUID) ([]*dom_file.File, error)
|
||||
}
|
||||
|
||||
type getFileMetadataByCreatedByUserIDUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewGetFileMetadataByCreatedByUserIDUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByCreatedByUserIDUseCase {
|
||||
logger = logger.Named("GetFileMetadataByCreatedByUserIDUseCase")
|
||||
return &getFileMetadataByCreatedByUserIDUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getFileMetadataByCreatedByUserIDUseCaseImpl) Execute(createdByUserID gocql.UUID) ([]*dom_file.File, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if createdByUserID.String() == "" {
|
||||
e["created_by_user_id"] = "Created by user ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata retrieval by created_by_user_id",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
return uc.repo.GetByCreatedByUserID(createdByUserID)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_ids.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFileMetadataByIDsUseCase interface {
|
||||
Execute(ids []gocql.UUID) ([]*dom_file.File, error)
|
||||
}
|
||||
|
||||
type getFileMetadataByIDsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewGetFileMetadataByIDsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByIDsUseCase {
|
||||
logger = logger.Named("GetFileMetadataByIDsUseCase")
|
||||
return &getFileMetadataByIDsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getFileMetadataByIDsUseCaseImpl) Execute(ids []gocql.UUID) ([]*dom_file.File, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if ids == nil || len(ids) == 0 {
|
||||
e["ids"] = "File IDs are required"
|
||||
} else {
|
||||
for i, id := range ids {
|
||||
if id.String() == "" {
|
||||
e[fmt.Sprintf("ids[%d]", i)] = "File ID is required"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata retrieval by IDs",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
return uc.repo.GetByIDs(ids)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_owner_id.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetFileMetadataByOwnerIDUseCase interface {
|
||||
Execute(ownerID gocql.UUID) ([]*dom_file.File, error)
|
||||
}
|
||||
|
||||
type getFileMetadataByOwnerIDUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewGetFileMetadataByOwnerIDUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByOwnerIDUseCase {
|
||||
logger = logger.Named("GetFileMetadataByOwnerIDUseCase")
|
||||
return &getFileMetadataByOwnerIDUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getFileMetadataByOwnerIDUseCaseImpl) Execute(ownerID gocql.UUID) ([]*dom_file.File, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if ownerID.String() == "" {
|
||||
e["owner_id"] = "Created by user ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata retrieval by owner_id",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
return uc.repo.GetByOwnerID(ownerID)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/harddelete.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// HardDeleteFileMetadataUseCase permanently deletes file metadata
|
||||
// Used for GDPR right-to-be-forgotten implementation
|
||||
type HardDeleteFileMetadataUseCase interface {
|
||||
Execute(id gocql.UUID) error
|
||||
}
|
||||
|
||||
type hardDeleteFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewHardDeleteFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) HardDeleteFileMetadataUseCase {
|
||||
logger = logger.Named("HardDeleteFileMetadataUseCase")
|
||||
return &hardDeleteFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *hardDeleteFileMetadataUseCaseImpl) Execute(id gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "File ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata hard deletion",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Hard delete from database (no tombstone).
|
||||
//
|
||||
|
||||
uc.logger.Info("Hard deleting file metadata (GDPR mode)",
|
||||
zap.String("file_id", id.String()))
|
||||
|
||||
err := uc.repo.HardDelete(id)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to hard delete file metadata",
|
||||
zap.String("file_id", id.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("✅ File metadata hard deleted successfully",
|
||||
zap.String("file_id", id.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package filemetadata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
// NOTE: Unit tests for HardDeleteFileMetadataUseCase would require mocks.
|
||||
// For now, this use case will be tested via integration tests.
|
||||
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
|
||||
|
||||
func TestHardDeleteFileMetadataUseCase_Constructor(t *testing.T) {
|
||||
// Test that constructor creates use case successfully
|
||||
cfg := &config.Configuration{}
|
||||
logger := zap.NewNop()
|
||||
|
||||
useCase := NewHardDeleteFileMetadataUseCase(cfg, logger, nil)
|
||||
|
||||
if useCase == nil {
|
||||
t.Error("Expected use case to be created, got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata/list_by_owner.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListFilesByOwnerIDUseCase interface {
|
||||
Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_file.File, error)
|
||||
}
|
||||
|
||||
type listFilesByOwnerIDUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewListFilesByOwnerIDUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) ListFilesByOwnerIDUseCase {
|
||||
logger = logger.Named("ListFilesByOwnerIDUseCase")
|
||||
return &listFilesByOwnerIDUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *listFilesByOwnerIDUseCaseImpl) Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_file.File, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if ownerID.String() == "" {
|
||||
e["owner_id"] = "Owner ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating list files by owner",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
files, err := uc.repo.GetByOwnerID(ownerID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get files by owner ID",
|
||||
zap.String("owner_id", ownerID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Files successfully retrieved by owner ID",
|
||||
zap.String("owner_id", ownerID.String()),
|
||||
zap.Int("count", len(files)))
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/list_recent_files.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListRecentFilesUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.RecentFilesCursor, limit int64) (*dom_file.RecentFilesResponse, error)
|
||||
}
|
||||
|
||||
type listRecentFilesUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewListRecentFilesUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) ListRecentFilesUseCase {
|
||||
logger = logger.Named("ListRecentFilesUseCase")
|
||||
return &listRecentFilesUseCaseImpl{config, logger, fileRepo, collectionRepo}
|
||||
}
|
||||
|
||||
func (uc *listRecentFilesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.RecentFilesCursor, limit int64) (*dom_file.RecentFilesResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if limit <= 0 {
|
||||
e["limit"] = "Limit must be greater than 0"
|
||||
}
|
||||
if limit > 100 {
|
||||
e["limit"] = "Limit cannot exceed 100"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating list recent files",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get accessible collections for the user.
|
||||
//
|
||||
|
||||
uc.logger.Debug("Getting accessible collections for recent files",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
// Get collections using the efficient filtered query
|
||||
filterOptions := dom_collection.CollectionFilterOptions{
|
||||
UserID: userID,
|
||||
IncludeOwned: true,
|
||||
IncludeShared: true,
|
||||
}
|
||||
|
||||
collectionResult, err := uc.collectionRepo.GetCollectionsWithFilter(ctx, filterOptions)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get accessible collections for recent files",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract collection IDs
|
||||
allCollections := collectionResult.GetAllCollections()
|
||||
accessibleCollectionIDs := make([]gocql.UUID, 0, len(allCollections))
|
||||
for _, collection := range allCollections {
|
||||
// Only include active collections
|
||||
if collection.State == "active" {
|
||||
accessibleCollectionIDs = append(accessibleCollectionIDs, collection.ID)
|
||||
}
|
||||
}
|
||||
|
||||
uc.logger.Debug("Found accessible collections for recent files",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("owned_collections", len(collectionResult.OwnedCollections)),
|
||||
zap.Int("shared_collections", len(collectionResult.SharedCollections)),
|
||||
zap.Int("total_accessible", len(accessibleCollectionIDs)))
|
||||
|
||||
// If no accessible collections, return empty response
|
||||
if len(accessibleCollectionIDs) == 0 {
|
||||
uc.logger.Info("User has no accessible collections for recent files",
|
||||
zap.String("user_id", userID.String()))
|
||||
return &dom_file.RecentFilesResponse{
|
||||
Files: []dom_file.RecentFilesItem{},
|
||||
NextCursor: nil,
|
||||
HasMore: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: List recent files for accessible collections.
|
||||
//
|
||||
|
||||
recentFiles, err := uc.fileRepo.ListRecentFiles(ctx, userID, cursor, limit, accessibleCollectionIDs)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to list recent files",
|
||||
zap.Any("error", err),
|
||||
zap.String("user_id", userID.String()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if recentFiles == nil {
|
||||
uc.logger.Debug("Recent files not found",
|
||||
zap.String("user_id", userID.String()))
|
||||
return nil, httperror.NewForNotFoundWithSingleField("message", "Recent files not found")
|
||||
}
|
||||
|
||||
uc.logger.Debug("Recent files successfully retrieved",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Any("next_cursor", recentFiles.NextCursor),
|
||||
zap.Int("files_count", len(recentFiles.Files)))
|
||||
|
||||
return recentFiles, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/list_sync_data.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type ListFileMetadataSyncDataUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.FileSyncCursor, limit int64, accessibleCollectionIDs []gocql.UUID) (*dom_file.FileSyncResponse, error)
|
||||
}
|
||||
|
||||
type listFileMetadataSyncDataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewListFileMetadataSyncDataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) ListFileMetadataSyncDataUseCase {
|
||||
logger = logger.Named("ListFileMetadataSyncDataUseCase")
|
||||
return &listFileMetadataSyncDataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *listFileMetadataSyncDataUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.FileSyncCursor, limit int64, accessibleCollectionIDs []gocql.UUID) (*dom_file.FileSyncResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(accessibleCollectionIDs) == 0 {
|
||||
e["accessible_collections"] = "At least one accessible collection is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating list file sync data",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
uc.logger.Debug("Listing file sync data",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("accessible_collections_count", len(accessibleCollectionIDs)),
|
||||
zap.Any("cursor", cursor),
|
||||
zap.Int64("limit", limit))
|
||||
|
||||
//
|
||||
// STEP 2: List file sync data from repository for accessible collections.
|
||||
//
|
||||
|
||||
result, err := uc.repo.ListSyncData(ctx, userID, cursor, limit, accessibleCollectionIDs)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to list file sync data from repository",
|
||||
zap.Any("error", err),
|
||||
zap.String("user_id", userID.String()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Log the sync items for debugging
|
||||
uc.logger.Debug("File sync data retrieved from repository",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("files_count", len(result.Files)),
|
||||
zap.Bool("has_more", result.HasMore))
|
||||
|
||||
// Log each sync item to verify all fields are populated
|
||||
for i, item := range result.Files {
|
||||
uc.logger.Debug("File sync item",
|
||||
zap.Int("index", i),
|
||||
zap.String("file_id", item.ID.String()),
|
||||
zap.String("collection_id", item.CollectionID.String()),
|
||||
zap.Uint64("version", item.Version),
|
||||
zap.Time("modified_at", item.ModifiedAt),
|
||||
zap.String("state", item.State),
|
||||
zap.Uint64("tombstone_version", item.TombstoneVersion),
|
||||
zap.Time("tombstone_expiry", item.TombstoneExpiry),
|
||||
zap.Int64("encrypted_file_size_in_bytes", item.EncryptedFileSizeInBytes))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
)
|
||||
|
||||
// Wire providers for file metadata use cases
|
||||
|
||||
func ProvideCreateFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CreateFileMetadataUseCase {
|
||||
return NewCreateFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideUpdateFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) UpdateFileMetadataUseCase {
|
||||
return NewUpdateFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataUseCase {
|
||||
return NewGetFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideSoftDeleteFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) SoftDeleteFileMetadataUseCase {
|
||||
return NewSoftDeleteFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetFileMetadataByCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByCollectionUseCase {
|
||||
return NewGetFileMetadataByCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetFileMetadataByCreatedByUserIDUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByCreatedByUserIDUseCase {
|
||||
return NewGetFileMetadataByCreatedByUserIDUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideDeleteManyFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) DeleteManyFileMetadataUseCase {
|
||||
return NewDeleteManyFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideCountUserFilesUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) CountUserFilesUseCase {
|
||||
return NewCountUserFilesUseCase(cfg, logger, fileRepo, collectionRepo)
|
||||
}
|
||||
|
||||
func ProvideCheckFileAccessUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CheckFileAccessUseCase {
|
||||
return NewCheckFileAccessUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetStorageSizeByCollectionUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetStorageSizeByCollectionUseCase {
|
||||
return NewGetStorageSizeByCollectionUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetFileMetadataByOwnerIDUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByOwnerIDUseCase {
|
||||
return NewGetFileMetadataByOwnerIDUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideCheckFileExistsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CheckFileExistsUseCase {
|
||||
return NewCheckFileExistsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetStorageSizeByUserUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) GetStorageSizeByUserUseCase {
|
||||
return NewGetStorageSizeByUserUseCase(cfg, logger, fileRepo, collectionRepo)
|
||||
}
|
||||
|
||||
func ProvideListRecentFilesUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) ListRecentFilesUseCase {
|
||||
return NewListRecentFilesUseCase(cfg, logger, fileRepo, collectionRepo)
|
||||
}
|
||||
|
||||
func ProvideGetFileMetadataByIDsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetFileMetadataByIDsUseCase {
|
||||
return NewGetFileMetadataByIDsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideCreateManyFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) CreateManyFileMetadataUseCase {
|
||||
return NewCreateManyFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideListFileMetadataSyncDataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) ListFileMetadataSyncDataUseCase {
|
||||
return NewListFileMetadataSyncDataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetStorageSizeByOwnerUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) GetStorageSizeByOwnerUseCase {
|
||||
return NewGetStorageSizeByOwnerUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideAnonymizeOldIPsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) AnonymizeOldIPsUseCase {
|
||||
return NewAnonymizeOldIPsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideListFilesByOwnerIDUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) ListFilesByOwnerIDUseCase {
|
||||
return NewListFilesByOwnerIDUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideRestoreFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) RestoreFileMetadataUseCase {
|
||||
return NewRestoreFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideHardDeleteFileMetadataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) HardDeleteFileMetadataUseCase {
|
||||
return NewHardDeleteFileMetadataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideAnonymizeUserReferencesUseCase(
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) AnonymizeUserReferencesUseCase {
|
||||
return NewAnonymizeUserReferencesUseCase(logger, repo)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata/restore.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type RestoreFileMetadataUseCase interface {
|
||||
Execute(ctx context.Context, id gocql.UUID) error
|
||||
}
|
||||
|
||||
type restoreFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewRestoreFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) RestoreFileMetadataUseCase {
|
||||
logger = logger.Named("RestoreFileMetadataUseCase")
|
||||
return &restoreFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *restoreFileMetadataUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "File ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata restoration",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Restore file metadata using repository method.
|
||||
//
|
||||
|
||||
err := uc.repo.Restore(id)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to restore file metadata",
|
||||
zap.String("file_id", id.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("File metadata successfully restored",
|
||||
zap.String("file_id", id.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/delete.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type SoftDeleteFileMetadataUseCase interface {
|
||||
Execute(id gocql.UUID) error
|
||||
}
|
||||
|
||||
type softDeleteFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewSoftDeleteFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) SoftDeleteFileMetadataUseCase {
|
||||
logger = logger.Named("SoftDeleteFileMetadataUseCase")
|
||||
return &softDeleteFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *softDeleteFileMetadataUseCaseImpl) Execute(id gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if id.String() == "" {
|
||||
e["id"] = "File ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata deletion",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Soft-delete from database.
|
||||
//
|
||||
|
||||
return uc.repo.SoftDelete(id)
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/storage_size.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// StorageSizeBreakdownResponse contains detailed storage breakdown
|
||||
type StorageSizeBreakdownResponse struct {
|
||||
OwnedSizeBytes int64 `json:"owned_size_bytes"`
|
||||
SharedSizeBytes int64 `json:"shared_size_bytes"`
|
||||
TotalSizeBytes int64 `json:"total_size_bytes"`
|
||||
CollectionBreakdownBytes map[string]int64 `json:"collection_breakdown_bytes"`
|
||||
OwnedCollectionsCount int `json:"owned_collections_count"`
|
||||
SharedCollectionsCount int `json:"shared_collections_count"`
|
||||
}
|
||||
|
||||
// Use case interfaces
|
||||
|
||||
type GetStorageSizeByCollectionUseCase interface {
|
||||
Execute(ctx context.Context, collectionID gocql.UUID) (*StorageSizeResponse, error)
|
||||
}
|
||||
|
||||
// Use case implementations
|
||||
|
||||
type getStorageSizeByCollectionUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
// Constructors
|
||||
|
||||
func NewGetStorageSizeByCollectionUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) GetStorageSizeByCollectionUseCase {
|
||||
logger = logger.Named("GetStorageSizeByCollectionUseCase")
|
||||
return &getStorageSizeByCollectionUseCaseImpl{config, logger, fileRepo}
|
||||
}
|
||||
|
||||
// Use case implementations
|
||||
|
||||
func (uc *getStorageSizeByCollectionUseCaseImpl) Execute(ctx context.Context, collectionID gocql.UUID) (*StorageSizeResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if collectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage size by collection",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Calculate storage size.
|
||||
//
|
||||
|
||||
totalSize, err := uc.fileRepo.GetTotalStorageSizeByCollection(ctx, collectionID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage size by collection",
|
||||
zap.String("collection_id", collectionID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &StorageSizeResponse{
|
||||
TotalSizeBytes: totalSize,
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully calculated storage size by collection",
|
||||
zap.String("collection_id", collectionID.String()),
|
||||
zap.Int64("total_size_bytes", totalSize))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/storage_size.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// StorageSizeResponse contains storage size information
|
||||
type StorageSizeResponse struct {
|
||||
TotalSizeBytes int64 `json:"total_size_bytes"`
|
||||
}
|
||||
|
||||
// Use case interfaces
|
||||
type GetStorageSizeByOwnerUseCase interface {
|
||||
Execute(ctx context.Context, ownerID gocql.UUID) (*StorageSizeResponse, error)
|
||||
}
|
||||
|
||||
// Use case implementations
|
||||
type getStorageSizeByOwnerUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
// Constructors
|
||||
func NewGetStorageSizeByOwnerUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) GetStorageSizeByOwnerUseCase {
|
||||
logger = logger.Named("GetStorageSizeByOwnerUseCase")
|
||||
return &getStorageSizeByOwnerUseCaseImpl{config, logger, fileRepo}
|
||||
}
|
||||
|
||||
// Use case implementations
|
||||
|
||||
func (uc *getStorageSizeByOwnerUseCaseImpl) Execute(ctx context.Context, ownerID gocql.UUID) (*StorageSizeResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if ownerID.String() == "" {
|
||||
e["owner_id"] = "Owner ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage size by owner",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Calculate storage size.
|
||||
//
|
||||
|
||||
totalSize, err := uc.fileRepo.GetTotalStorageSizeByOwner(ctx, ownerID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage size by owner",
|
||||
zap.String("owner_id", ownerID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &StorageSizeResponse{
|
||||
TotalSizeBytes: totalSize,
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully calculated storage size by owner",
|
||||
zap.String("owner_id", ownerID.String()),
|
||||
zap.Int64("total_size_bytes", totalSize))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/storage_size.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// Use case interfaces
|
||||
|
||||
type GetStorageSizeByUserUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) (*StorageSizeResponse, error)
|
||||
}
|
||||
|
||||
// Use case implementations
|
||||
|
||||
type getStorageSizeByUserUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
// Constructors
|
||||
|
||||
func NewGetStorageSizeByUserUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) GetStorageSizeByUserUseCase {
|
||||
logger = logger.Named("GetStorageSizeByUserUseCase")
|
||||
return &getStorageSizeByUserUseCaseImpl{config, logger, fileRepo, collectionRepo}
|
||||
}
|
||||
|
||||
// Use case implementations
|
||||
|
||||
func (uc *getStorageSizeByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*StorageSizeResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage size by user",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get accessible collections for the user.
|
||||
//
|
||||
|
||||
filterOptions := dom_collection.CollectionFilterOptions{
|
||||
UserID: userID,
|
||||
IncludeOwned: true,
|
||||
IncludeShared: true,
|
||||
}
|
||||
|
||||
collectionResult, err := uc.collectionRepo.GetCollectionsWithFilter(ctx, filterOptions)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get accessible collections for storage size calculation",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract collection IDs
|
||||
allCollections := collectionResult.GetAllCollections()
|
||||
accessibleCollectionIDs := make([]gocql.UUID, 0, len(allCollections))
|
||||
for _, collection := range allCollections {
|
||||
accessibleCollectionIDs = append(accessibleCollectionIDs, collection.ID)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Calculate storage size.
|
||||
//
|
||||
|
||||
totalSize, err := uc.fileRepo.GetTotalStorageSizeByUser(ctx, userID, accessibleCollectionIDs)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage size by user",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &StorageSizeResponse{
|
||||
TotalSizeBytes: totalSize,
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully calculated storage size by user",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
|
||||
zap.Int64("total_size_bytes", totalSize))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/update.go
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UpdateFileMetadataUseCase interface {
|
||||
Execute(ctx context.Context, file *dom_file.File) error
|
||||
}
|
||||
|
||||
type updateFileMetadataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewUpdateFileMetadataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileMetadataRepository,
|
||||
) UpdateFileMetadataUseCase {
|
||||
logger = logger.Named("UpdateFileMetadataUseCase")
|
||||
return &updateFileMetadataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *updateFileMetadataUseCaseImpl) Execute(ctx context.Context, file *dom_file.File) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if file == nil {
|
||||
e["file"] = "File is required"
|
||||
} else {
|
||||
if file.ID.String() == "" {
|
||||
e["id"] = "File ID is required"
|
||||
}
|
||||
if file.CollectionID.String() == "" {
|
||||
e["collection_id"] = "Collection ID is required"
|
||||
}
|
||||
if file.OwnerID.String() == "" {
|
||||
e["owner_id"] = "Owner ID is required"
|
||||
}
|
||||
if file.EncryptedMetadata == "" {
|
||||
e["encrypted_metadata"] = "Encrypted metadata is required"
|
||||
}
|
||||
if file.EncryptedFileKey.Ciphertext == nil || len(file.EncryptedFileKey.Ciphertext) == 0 {
|
||||
e["encrypted_file_key"] = "Encrypted file key is required"
|
||||
}
|
||||
if file.EncryptionVersion == "" {
|
||||
e["encryption_version"] = "Encryption version is required"
|
||||
}
|
||||
if file.EncryptedHash == "" {
|
||||
e["encrypted_hash"] = "Encrypted hash is required"
|
||||
}
|
||||
if file.EncryptedFileObjectKey == "" {
|
||||
e["encrypted_file_object_key"] = "Encrypted file object key is required"
|
||||
}
|
||||
if file.EncryptedFileSizeInBytes <= 0 {
|
||||
e["encrypted_file_size_in_bytes"] = "Encrypted file size must be greater than 0"
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating file metadata update",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Update in database.
|
||||
//
|
||||
|
||||
return uc.repo.Update(file)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/delete_encrypted_data.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type DeleteEncryptedDataUseCase interface {
|
||||
Execute(storagePath string) error
|
||||
}
|
||||
|
||||
type deleteEncryptedDataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewDeleteEncryptedDataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) DeleteEncryptedDataUseCase {
|
||||
logger = logger.Named("DeleteEncryptedDataUseCase")
|
||||
return &deleteEncryptedDataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *deleteEncryptedDataUseCaseImpl) Execute(storagePath string) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if storagePath == "" {
|
||||
e["storage_path"] = "Storage path is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating delete encrypted data",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Delete encrypted data.
|
||||
//
|
||||
|
||||
err := uc.repo.DeleteEncryptedData(storagePath)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to delete encrypted data",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("Successfully deleted encrypted data",
|
||||
zap.String("storage_path", storagePath))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/delete_multiple_encrypted_data.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type DeleteMultipleEncryptedDataUseCase interface {
|
||||
Execute(storagePaths []string) error
|
||||
}
|
||||
|
||||
type deleteMultipleEncryptedDataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewDeleteMultipleEncryptedDataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) DeleteMultipleEncryptedDataUseCase {
|
||||
logger = logger.Named("DeleteMultipleEncryptedDataUseCase")
|
||||
return &deleteMultipleEncryptedDataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *deleteMultipleEncryptedDataUseCaseImpl) Execute(storagePaths []string) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if storagePaths == nil || len(storagePaths) == 0 {
|
||||
e["storage_paths"] = "Storage paths are required"
|
||||
} else {
|
||||
for i, path := range storagePaths {
|
||||
if path == "" {
|
||||
e[fmt.Sprintf("storage_paths[%d]", i)] = "Storage path is required"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating delete multiple encrypted data",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Delete encrypted data files.
|
||||
//
|
||||
|
||||
var errors []error
|
||||
successCount := 0
|
||||
|
||||
for _, storagePath := range storagePaths {
|
||||
err := uc.repo.DeleteEncryptedData(storagePath)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to delete encrypted data",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Error(err))
|
||||
errors = append(errors, fmt.Errorf("failed to delete %s: %w", storagePath, err))
|
||||
} else {
|
||||
successCount++
|
||||
uc.logger.Debug("Successfully deleted encrypted data",
|
||||
zap.String("storage_path", storagePath))
|
||||
}
|
||||
}
|
||||
|
||||
// Log summary
|
||||
uc.logger.Info("Completed bulk delete operation",
|
||||
zap.Int("total_requested", len(storagePaths)),
|
||||
zap.Int("successful_deletions", successCount),
|
||||
zap.Int("failed_deletions", len(errors)))
|
||||
|
||||
// If all operations failed, return the first error
|
||||
if len(errors) == len(storagePaths) {
|
||||
return errors[0]
|
||||
}
|
||||
|
||||
// If some operations failed, log but don't return error (partial success)
|
||||
if len(errors) > 0 {
|
||||
uc.logger.Warn("Some delete operations failed",
|
||||
zap.Int("failed_count", len(errors)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/get_encrypted_data.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetEncryptedDataUseCase interface {
|
||||
Execute(storagePath string) ([]byte, error)
|
||||
}
|
||||
|
||||
type getEncryptedDataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewGetEncryptedDataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GetEncryptedDataUseCase {
|
||||
logger = logger.Named("GetEncryptedDataUseCase")
|
||||
return &getEncryptedDataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getEncryptedDataUseCaseImpl) Execute(storagePath string) ([]byte, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if storagePath == "" {
|
||||
e["storage_path"] = "Storage path is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get encrypted data",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get encrypted data.
|
||||
//
|
||||
|
||||
data, err := uc.repo.GetEncryptedData(storagePath)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get encrypted data",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully retrieved encrypted data",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Int("data_size", len(data)))
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/get_object_size.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GetObjectSizeUseCase interface {
|
||||
Execute(storagePath string) (int64, error)
|
||||
}
|
||||
|
||||
type getObjectSizeUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewGetObjectSizeUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GetObjectSizeUseCase {
|
||||
logger = logger.Named("GetObjectSizeUseCase")
|
||||
return &getObjectSizeUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getObjectSizeUseCaseImpl) Execute(storagePath string) (int64, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if storagePath == "" {
|
||||
e["storage_path"] = "Storage path is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get object size",
|
||||
zap.Any("error", e))
|
||||
return 0, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get object size.
|
||||
//
|
||||
|
||||
size, err := uc.repo.GetObjectSize(storagePath)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get object size",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Retrieved object size",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Int64("size", size))
|
||||
|
||||
return size, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/presigned_download_url.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GeneratePresignedDownloadURLUseCase interface {
|
||||
Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error)
|
||||
}
|
||||
|
||||
type generatePresignedDownloadURLUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewGeneratePresignedDownloadURLUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GeneratePresignedDownloadURLUseCase {
|
||||
logger = logger.Named("GeneratePresignedDownloadURLUseCase")
|
||||
return &generatePresignedDownloadURLUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *generatePresignedDownloadURLUseCaseImpl) Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if storagePath == "" {
|
||||
e["storage_path"] = "Storage path is required"
|
||||
}
|
||||
if duration <= 0 {
|
||||
e["duration"] = "Duration must be greater than 0"
|
||||
}
|
||||
// Set reasonable limits for presigned URL duration
|
||||
maxDuration := 24 * time.Hour // 24 hours max
|
||||
if duration > maxDuration {
|
||||
e["duration"] = "Duration cannot exceed 24 hours"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating generate presigned download URL",
|
||||
zap.Any("error", e))
|
||||
return "", httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Generate and get presigned download URL.
|
||||
//
|
||||
|
||||
url, err := uc.repo.GeneratePresignedDownloadURL(storagePath, duration)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to generate presigned download URL",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Duration("duration", duration),
|
||||
zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/presigned_upload_url.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type GeneratePresignedUploadURLUseCase interface {
|
||||
Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error)
|
||||
}
|
||||
|
||||
type generatePresignedUploadURLUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewGeneratePresignedUploadURLUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GeneratePresignedUploadURLUseCase {
|
||||
logger = logger.Named("GeneratePresignedUploadURLUseCase")
|
||||
return &generatePresignedUploadURLUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *generatePresignedUploadURLUseCaseImpl) Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if storagePath == "" {
|
||||
e["storage_path"] = "Storage path is required"
|
||||
}
|
||||
if duration <= 0 {
|
||||
e["duration"] = "Duration must be greater than 0"
|
||||
}
|
||||
// Set reasonable limits for presigned URL duration
|
||||
maxDuration := 24 * time.Hour // 24 hours max
|
||||
if duration > maxDuration {
|
||||
e["duration"] = "Duration cannot exceed 24 hours"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating generate presigned upload URL",
|
||||
zap.Any("error", e))
|
||||
return "", httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Generate and get presigned upload URL.
|
||||
//
|
||||
|
||||
url, err := uc.repo.GeneratePresignedUploadURL(storagePath, duration)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to generate presigned upload URL",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Duration("duration", duration),
|
||||
zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
)
|
||||
|
||||
// Wire providers for file object storage use cases
|
||||
|
||||
func ProvideStoreEncryptedDataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) StoreEncryptedDataUseCase {
|
||||
return NewStoreEncryptedDataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetEncryptedDataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GetEncryptedDataUseCase {
|
||||
return NewGetEncryptedDataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideDeleteEncryptedDataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) DeleteEncryptedDataUseCase {
|
||||
return NewDeleteEncryptedDataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideStoreMultipleEncryptedDataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) StoreMultipleEncryptedDataUseCase {
|
||||
return NewStoreMultipleEncryptedDataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideDeleteMultipleEncryptedDataUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) DeleteMultipleEncryptedDataUseCase {
|
||||
return NewDeleteMultipleEncryptedDataUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideVerifyObjectExistsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) VerifyObjectExistsUseCase {
|
||||
return NewVerifyObjectExistsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGeneratePresignedUploadURLUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GeneratePresignedUploadURLUseCase {
|
||||
return NewGeneratePresignedUploadURLUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetObjectSizeUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GetObjectSizeUseCase {
|
||||
return NewGetObjectSizeUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGeneratePresignedDownloadURLUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) GeneratePresignedDownloadURLUseCase {
|
||||
return NewGeneratePresignedDownloadURLUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/store_encrypted_data.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type StoreEncryptedDataUseCase interface {
|
||||
Execute(ownerID string, fileID string, encryptedData []byte) (string, error)
|
||||
}
|
||||
|
||||
type storeEncryptedDataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewStoreEncryptedDataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) StoreEncryptedDataUseCase {
|
||||
logger = logger.Named("StoreEncryptedDataUseCase")
|
||||
return &storeEncryptedDataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *storeEncryptedDataUseCaseImpl) Execute(ownerID string, fileID string, encryptedData []byte) (string, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if ownerID == "" {
|
||||
e["owner_id"] = "Owner ID is required"
|
||||
}
|
||||
if fileID == "" {
|
||||
e["file_id"] = "File ID is required"
|
||||
}
|
||||
if encryptedData == nil || len(encryptedData) == 0 {
|
||||
e["encrypted_data"] = "Encrypted data is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating store encrypted data",
|
||||
zap.Any("error", e))
|
||||
return "", httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Store encrypted data.
|
||||
//
|
||||
|
||||
storagePath, err := uc.repo.StoreEncryptedData(ownerID, fileID, encryptedData)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to store encrypted data",
|
||||
zap.String("owner_id", ownerID),
|
||||
zap.String("file_id", fileID),
|
||||
zap.Int("data_size", len(encryptedData)),
|
||||
zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
uc.logger.Info("Successfully stored encrypted data",
|
||||
zap.String("owner_id", ownerID),
|
||||
zap.String("file_id", fileID),
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Int("data_size", len(encryptedData)))
|
||||
|
||||
return storagePath, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/store_multiple_encrypted_data.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// EncryptedDataItem represents a single item to be stored
|
||||
type EncryptedDataItem struct {
|
||||
OwnerID string `json:"owner_id"`
|
||||
FileID string `json:"file_id"`
|
||||
EncryptedData []byte `json:"encrypted_data"`
|
||||
}
|
||||
|
||||
// StorageResult represents the result of storing a single item
|
||||
type StorageResult struct {
|
||||
FileID string `json:"file_id"`
|
||||
StoragePath string `json:"storage_path,omitempty"`
|
||||
Error error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type StoreMultipleEncryptedDataUseCase interface {
|
||||
Execute(items []EncryptedDataItem) ([]StorageResult, error)
|
||||
}
|
||||
|
||||
type storeMultipleEncryptedDataUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewStoreMultipleEncryptedDataUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) StoreMultipleEncryptedDataUseCase {
|
||||
logger = logger.Named("StoreMultipleEncryptedDataUseCase")
|
||||
return &storeMultipleEncryptedDataUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *storeMultipleEncryptedDataUseCaseImpl) Execute(items []EncryptedDataItem) ([]StorageResult, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if items == nil || len(items) == 0 {
|
||||
e["items"] = "Items are required"
|
||||
} else {
|
||||
for i, item := range items {
|
||||
if item.OwnerID == "" {
|
||||
e[fmt.Sprintf("items[%d].owner_id", i)] = "Owner ID is required"
|
||||
}
|
||||
if item.FileID == "" {
|
||||
e[fmt.Sprintf("items[%d].file_id", i)] = "File ID is required"
|
||||
}
|
||||
if item.EncryptedData == nil || len(item.EncryptedData) == 0 {
|
||||
e[fmt.Sprintf("items[%d].encrypted_data", i)] = "Encrypted data is required"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating store multiple encrypted data",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Store encrypted data files.
|
||||
//
|
||||
|
||||
results := make([]StorageResult, len(items))
|
||||
successCount := 0
|
||||
|
||||
for i, item := range items {
|
||||
storagePath, err := uc.repo.StoreEncryptedData(item.OwnerID, item.FileID, item.EncryptedData)
|
||||
|
||||
results[i] = StorageResult{
|
||||
FileID: item.FileID,
|
||||
StoragePath: storagePath,
|
||||
Error: err,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to store encrypted data",
|
||||
zap.String("owner_id", item.OwnerID),
|
||||
zap.String("file_id", item.FileID),
|
||||
zap.Int("data_size", len(item.EncryptedData)),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
successCount++
|
||||
uc.logger.Debug("Successfully stored encrypted data",
|
||||
zap.String("owner_id", item.OwnerID),
|
||||
zap.String("file_id", item.FileID),
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Int("data_size", len(item.EncryptedData)))
|
||||
}
|
||||
}
|
||||
|
||||
// Log summary
|
||||
uc.logger.Info("Completed bulk store operation",
|
||||
zap.Int("total_requested", len(items)),
|
||||
zap.Int("successful_stores", successCount),
|
||||
zap.Int("failed_stores", len(items)-successCount))
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/verify_object_exists.go
|
||||
package fileobjectstorage
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type VerifyObjectExistsUseCase interface {
|
||||
Execute(storagePath string) (bool, error)
|
||||
}
|
||||
|
||||
type verifyObjectExistsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_file.FileObjectStorageRepository
|
||||
}
|
||||
|
||||
func NewVerifyObjectExistsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo dom_file.FileObjectStorageRepository,
|
||||
) VerifyObjectExistsUseCase {
|
||||
logger = logger.Named("VerifyObjectExistsUseCase")
|
||||
return &verifyObjectExistsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *verifyObjectExistsUseCaseImpl) Execute(storagePath string) (bool, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if storagePath == "" {
|
||||
e["storage_path"] = "Storage path is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating verify if object exists",
|
||||
zap.Any("error", e))
|
||||
return false, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Verify if object exists.
|
||||
//
|
||||
|
||||
exists, err := uc.repo.VerifyObjectExists(storagePath)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to verify if object exists",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Object existence verified",
|
||||
zap.String("storage_path", storagePath),
|
||||
zap.Bool("exists", exists))
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package storagedailyusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
|
||||
)
|
||||
|
||||
// DeleteByUserUseCase deletes all storage daily usage records for a user
|
||||
// Used for GDPR right-to-be-forgotten implementation
|
||||
type DeleteByUserUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) error
|
||||
}
|
||||
|
||||
type deleteByUserUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo storagedailyusage.StorageDailyUsageRepository
|
||||
}
|
||||
|
||||
// NewDeleteByUserUseCase creates a new use case for deleting all storage daily usage by user ID
|
||||
func NewDeleteByUserUseCase(
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) DeleteByUserUseCase {
|
||||
return &deleteByUserUseCaseImpl{
|
||||
logger: logger.Named("DeleteStorageDailyUsageByUserUseCase"),
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *deleteByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) error {
|
||||
uc.logger.Info("Deleting all storage daily usage for user",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
err := uc.repo.DeleteByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to delete storage daily usage",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("✅ Successfully deleted all storage daily usage for user",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package storagedailyusage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// NOTE: Unit tests for DeleteByUserUseCase would require mocks.
|
||||
// For now, this use case will be tested via integration tests.
|
||||
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
|
||||
|
||||
func TestDeleteByUserUseCase_Constructor(t *testing.T) {
|
||||
// Test that constructor creates use case successfully
|
||||
logger := zap.NewNop()
|
||||
|
||||
useCase := NewDeleteByUserUseCase(logger, nil)
|
||||
|
||||
if useCase == nil {
|
||||
t.Error("Expected use case to be created, got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/get_trend.go
|
||||
package storagedailyusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// GetStorageDailyUsageTrendRequest contains the trend parameters
|
||||
type GetStorageDailyUsageTrendRequest struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
TrendPeriod string `json:"trend_period"` // "7days", "monthly", "yearly"
|
||||
Year *int `json:"year,omitempty"`
|
||||
Month *time.Month `json:"month,omitempty"`
|
||||
}
|
||||
|
||||
type GetStorageDailyUsageTrendUseCase interface {
|
||||
Execute(ctx context.Context, req *GetStorageDailyUsageTrendRequest) (*storagedailyusage.StorageUsageTrend, error)
|
||||
}
|
||||
|
||||
type getStorageDailyUsageTrendUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo storagedailyusage.StorageDailyUsageRepository
|
||||
}
|
||||
|
||||
func NewGetStorageDailyUsageTrendUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) GetStorageDailyUsageTrendUseCase {
|
||||
logger = logger.Named("GetStorageDailyUsageTrendUseCase")
|
||||
return &getStorageDailyUsageTrendUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getStorageDailyUsageTrendUseCaseImpl) Execute(ctx context.Context, req *GetStorageDailyUsageTrendRequest) (*storagedailyusage.StorageUsageTrend, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req == nil {
|
||||
e["request"] = "Request is required"
|
||||
} else {
|
||||
if req.UserID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if req.TrendPeriod == "" {
|
||||
e["trend_period"] = "Trend period is required"
|
||||
} else if req.TrendPeriod != "7days" && req.TrendPeriod != "monthly" && req.TrendPeriod != "yearly" {
|
||||
e["trend_period"] = "Trend period must be one of: 7days, monthly, yearly"
|
||||
}
|
||||
|
||||
// Validate period-specific parameters
|
||||
switch req.TrendPeriod {
|
||||
case "monthly":
|
||||
if req.Year == nil {
|
||||
e["year"] = "Year is required for monthly trend"
|
||||
}
|
||||
if req.Month == nil {
|
||||
e["month"] = "Month is required for monthly trend"
|
||||
}
|
||||
case "yearly":
|
||||
if req.Year == nil {
|
||||
e["year"] = "Year is required for yearly trend"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage daily usage trend",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get trend based on period.
|
||||
//
|
||||
|
||||
var trend *storagedailyusage.StorageUsageTrend
|
||||
var err error
|
||||
|
||||
switch req.TrendPeriod {
|
||||
case "7days":
|
||||
trend, err = uc.repo.GetLast7DaysTrend(ctx, req.UserID)
|
||||
|
||||
case "monthly":
|
||||
trend, err = uc.repo.GetMonthlyTrend(ctx, req.UserID, *req.Year, *req.Month)
|
||||
|
||||
case "yearly":
|
||||
trend, err = uc.repo.GetYearlyTrend(ctx, req.UserID, *req.Year)
|
||||
|
||||
default:
|
||||
return nil, httperror.NewForBadRequestWithSingleField("trend_period", "Invalid trend period")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage daily usage trend",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("trend_period", req.TrendPeriod),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully retrieved storage daily usage trend",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("trend_period", req.TrendPeriod),
|
||||
zap.Int("daily_usages_count", len(trend.DailyUsages)),
|
||||
zap.Int64("total_added", trend.TotalAdded),
|
||||
zap.Int64("total_removed", trend.TotalRemoved),
|
||||
zap.Int64("net_change", trend.NetChange))
|
||||
|
||||
return trend, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/get_usage_by_date_range.go
|
||||
package storagedailyusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// GetStorageUsageByDateRangeRequest contains the date range parameters
|
||||
type GetStorageUsageByDateRangeRequest struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
}
|
||||
|
||||
// GetStorageUsageByDateRangeResponse contains the usage data for the date range
|
||||
type GetStorageUsageByDateRangeResponse struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
DailyUsages []*storagedailyusage.StorageDailyUsage `json:"daily_usages"`
|
||||
Summary *DateRangeSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// DateRangeSummary contains aggregated statistics for the date range
|
||||
type DateRangeSummary struct {
|
||||
TotalDays int `json:"total_days"`
|
||||
DaysWithData int `json:"days_with_data"`
|
||||
TotalAdded int64 `json:"total_added"`
|
||||
TotalRemoved int64 `json:"total_removed"`
|
||||
NetChange int64 `json:"net_change"`
|
||||
AverageDailyAdd float64 `json:"average_daily_add"`
|
||||
PeakUsageDay *time.Time `json:"peak_usage_day,omitempty"`
|
||||
PeakUsageBytes int64 `json:"peak_usage_bytes"`
|
||||
LowestUsageDay *time.Time `json:"lowest_usage_day,omitempty"`
|
||||
LowestUsageBytes int64 `json:"lowest_usage_bytes"`
|
||||
}
|
||||
|
||||
type GetStorageUsageByDateRangeUseCase interface {
|
||||
Execute(ctx context.Context, req *GetStorageUsageByDateRangeRequest) (*GetStorageUsageByDateRangeResponse, error)
|
||||
}
|
||||
|
||||
type getStorageUsageByDateRangeUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo storagedailyusage.StorageDailyUsageRepository
|
||||
}
|
||||
|
||||
func NewGetStorageUsageByDateRangeUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) GetStorageUsageByDateRangeUseCase {
|
||||
logger = logger.Named("GetStorageUsageByDateRangeUseCase")
|
||||
return &getStorageUsageByDateRangeUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getStorageUsageByDateRangeUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageByDateRangeRequest) (*GetStorageUsageByDateRangeResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req == nil {
|
||||
e["request"] = "Request is required"
|
||||
} else {
|
||||
if req.UserID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if req.StartDate.IsZero() {
|
||||
e["start_date"] = "Start date is required"
|
||||
}
|
||||
if req.EndDate.IsZero() {
|
||||
e["end_date"] = "End date is required"
|
||||
}
|
||||
if !req.StartDate.IsZero() && !req.EndDate.IsZero() && req.StartDate.After(req.EndDate) {
|
||||
e["date_range"] = "Start date must be before or equal to end date"
|
||||
}
|
||||
// Check for reasonable date range (max 1 year)
|
||||
if !req.StartDate.IsZero() && !req.EndDate.IsZero() {
|
||||
daysDiff := int(req.EndDate.Sub(req.StartDate).Hours() / 24)
|
||||
if daysDiff > 365 {
|
||||
e["date_range"] = "Date range cannot exceed 365 days"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage usage by date range",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get usage data from repository.
|
||||
//
|
||||
|
||||
// Truncate dates to ensure we're working with date-only values
|
||||
startDate := req.StartDate.Truncate(24 * time.Hour)
|
||||
endDate := req.EndDate.Truncate(24 * time.Hour)
|
||||
|
||||
dailyUsages, err := uc.repo.GetByUserDateRange(ctx, req.UserID, startDate, endDate)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage usage by date range",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Generate summary statistics.
|
||||
//
|
||||
|
||||
summary := uc.generateDateRangeSummary(startDate, endDate, dailyUsages)
|
||||
|
||||
response := &GetStorageUsageByDateRangeResponse{
|
||||
UserID: req.UserID,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
DailyUsages: dailyUsages,
|
||||
Summary: summary,
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully retrieved storage usage by date range",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Int("daily_usages_count", len(dailyUsages)),
|
||||
zap.Int("days_with_data", summary.DaysWithData),
|
||||
zap.Int64("net_change", summary.NetChange))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// generateDateRangeSummary creates summary statistics for the date range
|
||||
func (uc *getStorageUsageByDateRangeUseCaseImpl) generateDateRangeSummary(startDate, endDate time.Time, dailyUsages []*storagedailyusage.StorageDailyUsage) *DateRangeSummary {
|
||||
totalDays := int(endDate.Sub(startDate).Hours()/24) + 1
|
||||
|
||||
summary := &DateRangeSummary{
|
||||
TotalDays: totalDays,
|
||||
DaysWithData: len(dailyUsages),
|
||||
LowestUsageBytes: int64(^uint64(0) >> 1), // Max int64 value as initial
|
||||
}
|
||||
|
||||
if len(dailyUsages) == 0 {
|
||||
summary.LowestUsageBytes = 0
|
||||
return summary
|
||||
}
|
||||
|
||||
for _, usage := range dailyUsages {
|
||||
summary.TotalAdded += usage.TotalAddBytes
|
||||
summary.TotalRemoved += usage.TotalRemoveBytes
|
||||
|
||||
// Track peak usage
|
||||
if usage.TotalBytes > summary.PeakUsageBytes {
|
||||
summary.PeakUsageBytes = usage.TotalBytes
|
||||
peakDay := usage.UsageDay
|
||||
summary.PeakUsageDay = &peakDay
|
||||
}
|
||||
|
||||
// Track lowest usage
|
||||
if usage.TotalBytes < summary.LowestUsageBytes {
|
||||
summary.LowestUsageBytes = usage.TotalBytes
|
||||
lowestDay := usage.UsageDay
|
||||
summary.LowestUsageDay = &lowestDay
|
||||
}
|
||||
}
|
||||
|
||||
summary.NetChange = summary.TotalAdded - summary.TotalRemoved
|
||||
|
||||
// Calculate average daily add (only for days with data)
|
||||
if summary.DaysWithData > 0 {
|
||||
summary.AverageDailyAdd = float64(summary.TotalAdded) / float64(summary.DaysWithData)
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/get_usage_summary.go
|
||||
package storagedailyusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// GetStorageUsageSummaryRequest contains the summary parameters
|
||||
type GetStorageUsageSummaryRequest struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
SummaryType string `json:"summary_type"` // "current_month", "current_year"
|
||||
}
|
||||
|
||||
type GetStorageUsageSummaryUseCase interface {
|
||||
Execute(ctx context.Context, req *GetStorageUsageSummaryRequest) (*storagedailyusage.StorageUsageSummary, error)
|
||||
}
|
||||
|
||||
type getStorageUsageSummaryUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo storagedailyusage.StorageDailyUsageRepository
|
||||
}
|
||||
|
||||
func NewGetStorageUsageSummaryUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) GetStorageUsageSummaryUseCase {
|
||||
logger = logger.Named("GetStorageUsageSummaryUseCase")
|
||||
return &getStorageUsageSummaryUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getStorageUsageSummaryUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageSummaryRequest) (*storagedailyusage.StorageUsageSummary, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req == nil {
|
||||
e["request"] = "Request is required"
|
||||
} else {
|
||||
if req.UserID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if req.SummaryType == "" {
|
||||
e["summary_type"] = "Summary type is required"
|
||||
} else if req.SummaryType != "current_month" && req.SummaryType != "current_year" {
|
||||
e["summary_type"] = "Summary type must be one of: current_month, current_year"
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage usage summary",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get summary based on type.
|
||||
//
|
||||
|
||||
var summary *storagedailyusage.StorageUsageSummary
|
||||
var err error
|
||||
|
||||
switch req.SummaryType {
|
||||
case "current_month":
|
||||
summary, err = uc.repo.GetCurrentMonthUsage(ctx, req.UserID)
|
||||
|
||||
case "current_year":
|
||||
summary, err = uc.repo.GetCurrentYearUsage(ctx, req.UserID)
|
||||
|
||||
default:
|
||||
return nil, httperror.NewForBadRequestWithSingleField("summary_type", "Invalid summary type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage usage summary",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("summary_type", req.SummaryType),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully retrieved storage usage summary",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("summary_type", req.SummaryType),
|
||||
zap.Int64("current_usage", summary.CurrentUsage),
|
||||
zap.Int64("total_added", summary.TotalAdded),
|
||||
zap.Int64("total_removed", summary.TotalRemoved),
|
||||
zap.Int64("net_change", summary.NetChange),
|
||||
zap.Int("days_with_data", summary.DaysWithData))
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package storagedailyusage
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
|
||||
)
|
||||
|
||||
// Wire providers for storage daily usage use cases
|
||||
|
||||
func ProvideGetStorageDailyUsageTrendUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) GetStorageDailyUsageTrendUseCase {
|
||||
return NewGetStorageDailyUsageTrendUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetStorageUsageSummaryUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) GetStorageUsageSummaryUseCase {
|
||||
return NewGetStorageUsageSummaryUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetStorageUsageByDateRangeUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) GetStorageUsageByDateRangeUseCase {
|
||||
return NewGetStorageUsageByDateRangeUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideUpdateStorageUsageUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) UpdateStorageUsageUseCase {
|
||||
return NewUpdateStorageUsageUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideDeleteByUserUseCase(
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) DeleteByUserUseCase {
|
||||
return NewDeleteByUserUseCase(logger, repo)
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/update_usage.go
|
||||
package storagedailyusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// UpdateStorageUsageRequest contains the update parameters
|
||||
type UpdateStorageUsageRequest struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
UsageDay *time.Time `json:"usage_day,omitempty"` // Optional, defaults to today
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
AddBytes int64 `json:"add_bytes"`
|
||||
RemoveBytes int64 `json:"remove_bytes"`
|
||||
IsIncrement bool `json:"is_increment"` // If true, increment existing values; if false, set absolute values
|
||||
}
|
||||
|
||||
type UpdateStorageUsageUseCase interface {
|
||||
Execute(ctx context.Context, req *UpdateStorageUsageRequest) error
|
||||
}
|
||||
|
||||
type updateStorageUsageUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo storagedailyusage.StorageDailyUsageRepository
|
||||
}
|
||||
|
||||
func NewUpdateStorageUsageUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storagedailyusage.StorageDailyUsageRepository,
|
||||
) UpdateStorageUsageUseCase {
|
||||
logger = logger.Named("UpdateStorageUsageUseCase")
|
||||
return &updateStorageUsageUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *updateStorageUsageUseCaseImpl) Execute(ctx context.Context, req *UpdateStorageUsageRequest) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req == nil {
|
||||
e["request"] = "Request is required"
|
||||
} else {
|
||||
if req.UserID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if req.AddBytes < 0 {
|
||||
e["add_bytes"] = "Add bytes cannot be negative"
|
||||
}
|
||||
if req.RemoveBytes < 0 {
|
||||
e["remove_bytes"] = "Remove bytes cannot be negative"
|
||||
}
|
||||
if !req.IsIncrement && req.TotalBytes < 0 {
|
||||
e["total_bytes"] = "Total bytes cannot be negative when setting absolute values"
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating update storage usage",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Set usage day if not provided.
|
||||
//
|
||||
|
||||
usageDay := time.Now().Truncate(24 * time.Hour)
|
||||
if req.UsageDay != nil {
|
||||
usageDay = req.UsageDay.Truncate(24 * time.Hour)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Update or increment usage.
|
||||
//
|
||||
|
||||
var err error
|
||||
|
||||
if req.IsIncrement {
|
||||
// Increment existing values
|
||||
err = uc.repo.IncrementUsage(ctx, req.UserID, usageDay, req.TotalBytes, req.AddBytes, req.RemoveBytes)
|
||||
} else {
|
||||
// Set absolute values
|
||||
usage := &storagedailyusage.StorageDailyUsage{
|
||||
UserID: req.UserID,
|
||||
UsageDay: usageDay,
|
||||
TotalBytes: req.TotalBytes,
|
||||
TotalAddBytes: req.AddBytes,
|
||||
TotalRemoveBytes: req.RemoveBytes,
|
||||
}
|
||||
err = uc.repo.UpdateOrCreate(ctx, usage)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to update storage usage",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.Time("usage_day", usageDay),
|
||||
zap.Int64("total_bytes", req.TotalBytes),
|
||||
zap.Int64("add_bytes", req.AddBytes),
|
||||
zap.Int64("remove_bytes", req.RemoveBytes),
|
||||
zap.Bool("is_increment", req.IsIncrement),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully updated storage usage",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.Time("usage_day", usageDay),
|
||||
zap.Int64("total_bytes", req.TotalBytes),
|
||||
zap.Int64("add_bytes", req.AddBytes),
|
||||
zap.Int64("remove_bytes", req.RemoveBytes),
|
||||
zap.Bool("is_increment", req.IsIncrement))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storageusageevent/create_event.go
|
||||
package storageusageevent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type CreateStorageUsageEventUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, fileSize int64, operation string) error
|
||||
}
|
||||
|
||||
type createStorageUsageEventUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo storageusageevent.StorageUsageEventRepository
|
||||
}
|
||||
|
||||
func NewCreateStorageUsageEventUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) CreateStorageUsageEventUseCase {
|
||||
logger = logger.Named("CreateStorageUsageEventUseCase")
|
||||
return &createStorageUsageEventUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *createStorageUsageEventUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, fileSize int64, operation string) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if fileSize <= 0 {
|
||||
e["file_size"] = "File size must be greater than 0"
|
||||
}
|
||||
if operation == "" {
|
||||
e["operation"] = "Operation is required"
|
||||
} else if operation != "add" && operation != "remove" {
|
||||
e["operation"] = "Operation must be 'add' or 'remove'"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating create storage usage event",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Create storage usage event.
|
||||
//
|
||||
|
||||
now := time.Now()
|
||||
event := &storageusageevent.StorageUsageEvent{
|
||||
UserID: userID,
|
||||
EventDay: now.Truncate(24 * time.Hour),
|
||||
EventTime: now,
|
||||
FileSize: fileSize,
|
||||
Operation: operation,
|
||||
}
|
||||
|
||||
err := uc.repo.Create(ctx, event)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to create storage usage event",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int64("file_size", fileSize),
|
||||
zap.String("operation", operation),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully created storage usage event",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int64("file_size", fileSize),
|
||||
zap.String("operation", operation))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package storageusageevent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
|
||||
)
|
||||
|
||||
// DeleteByUserUseCase deletes all storage usage events for a user
|
||||
// Used for GDPR right-to-be-forgotten implementation
|
||||
type DeleteByUserUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) error
|
||||
}
|
||||
|
||||
type deleteByUserUseCaseImpl struct {
|
||||
logger *zap.Logger
|
||||
repo storageusageevent.StorageUsageEventRepository
|
||||
}
|
||||
|
||||
// NewDeleteByUserUseCase creates a new use case for deleting all storage usage events by user ID
|
||||
func NewDeleteByUserUseCase(
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) DeleteByUserUseCase {
|
||||
return &deleteByUserUseCaseImpl{
|
||||
logger: logger.Named("DeleteStorageUsageEventByUserUseCase"),
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *deleteByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) error {
|
||||
uc.logger.Info("Deleting all storage usage events for user",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
err := uc.repo.DeleteByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to delete storage usage events",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("✅ Successfully deleted all storage usage events for user",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package storageusageevent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// NOTE: Unit tests for DeleteByUserUseCase would require mocks.
|
||||
// For now, this use case will be tested via integration tests.
|
||||
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
|
||||
|
||||
func TestDeleteByUserUseCase_Constructor(t *testing.T) {
|
||||
// Test that constructor creates use case successfully
|
||||
logger := zap.NewNop()
|
||||
|
||||
useCase := NewDeleteByUserUseCase(logger, nil)
|
||||
|
||||
if useCase == nil {
|
||||
t.Error("Expected use case to be created, got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storageusageevent/get_events.go
|
||||
package storageusageevent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// GetStorageUsageEventsRequest contains the filtering parameters
|
||||
type GetStorageUsageEventsRequest struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
TrendPeriod string `json:"trend_period"` // "7days", "monthly", "yearly"
|
||||
Year *int `json:"year,omitempty"`
|
||||
Month *time.Month `json:"month,omitempty"`
|
||||
Days *int `json:"days,omitempty"` // For custom day ranges
|
||||
}
|
||||
|
||||
// GetStorageUsageEventsResponse contains the filtered events
|
||||
type GetStorageUsageEventsResponse struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
TrendPeriod string `json:"trend_period"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
Events []*storageusageevent.StorageUsageEvent `json:"events"`
|
||||
EventCount int `json:"event_count"`
|
||||
}
|
||||
|
||||
type GetStorageUsageEventsUseCase interface {
|
||||
Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*GetStorageUsageEventsResponse, error)
|
||||
}
|
||||
|
||||
type getStorageUsageEventsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo storageusageevent.StorageUsageEventRepository
|
||||
}
|
||||
|
||||
func NewGetStorageUsageEventsUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) GetStorageUsageEventsUseCase {
|
||||
logger = logger.Named("GetStorageUsageEventsUseCase")
|
||||
return &getStorageUsageEventsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getStorageUsageEventsUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*GetStorageUsageEventsResponse, error) {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req == nil {
|
||||
e["request"] = "Request is required"
|
||||
} else {
|
||||
if req.UserID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if req.TrendPeriod == "" {
|
||||
e["trend_period"] = "Trend period is required"
|
||||
} else if req.TrendPeriod != "7days" && req.TrendPeriod != "monthly" && req.TrendPeriod != "yearly" && req.TrendPeriod != "custom" {
|
||||
e["trend_period"] = "Trend period must be one of: 7days, monthly, yearly, custom"
|
||||
}
|
||||
|
||||
// Validate period-specific parameters
|
||||
switch req.TrendPeriod {
|
||||
case "monthly":
|
||||
if req.Year == nil {
|
||||
e["year"] = "Year is required for monthly trend"
|
||||
}
|
||||
if req.Month == nil {
|
||||
e["month"] = "Month is required for monthly trend"
|
||||
}
|
||||
case "yearly":
|
||||
if req.Year == nil {
|
||||
e["year"] = "Year is required for yearly trend"
|
||||
}
|
||||
case "custom":
|
||||
if req.Days == nil || *req.Days <= 0 {
|
||||
e["days"] = "Days must be greater than 0 for custom trend"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage usage events",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get events based on trend period.
|
||||
//
|
||||
|
||||
var events []*storageusageevent.StorageUsageEvent
|
||||
var err error
|
||||
var startDate, endDate time.Time
|
||||
|
||||
switch req.TrendPeriod {
|
||||
case "7days":
|
||||
events, err = uc.repo.GetLast7DaysEvents(ctx, req.UserID)
|
||||
endDate = time.Now().Truncate(24 * time.Hour)
|
||||
startDate = endDate.Add(-6 * 24 * time.Hour)
|
||||
|
||||
case "monthly":
|
||||
events, err = uc.repo.GetMonthlyEvents(ctx, req.UserID, *req.Year, *req.Month)
|
||||
startDate = time.Date(*req.Year, *req.Month, 1, 0, 0, 0, 0, time.UTC)
|
||||
endDate = startDate.AddDate(0, 1, -1) // Last day of the month
|
||||
|
||||
case "yearly":
|
||||
events, err = uc.repo.GetYearlyEvents(ctx, req.UserID, *req.Year)
|
||||
startDate = time.Date(*req.Year, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
endDate = time.Date(*req.Year, 12, 31, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
case "custom":
|
||||
events, err = uc.repo.GetLastNDaysEvents(ctx, req.UserID, *req.Days)
|
||||
endDate = time.Now().Truncate(24 * time.Hour)
|
||||
startDate = endDate.Add(-time.Duration(*req.Days-1) * 24 * time.Hour)
|
||||
|
||||
default:
|
||||
return nil, httperror.NewForBadRequestWithSingleField("trend_period", "Invalid trend period")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage usage events",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("trend_period", req.TrendPeriod),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Build response.
|
||||
//
|
||||
|
||||
response := &GetStorageUsageEventsResponse{
|
||||
UserID: req.UserID,
|
||||
TrendPeriod: req.TrendPeriod,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Events: events,
|
||||
EventCount: len(events),
|
||||
}
|
||||
|
||||
uc.logger.Debug("Successfully retrieved storage usage events",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("trend_period", req.TrendPeriod),
|
||||
zap.Int("event_count", len(events)),
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storageusageevent/get_trend_analysis.go
|
||||
package storageusageevent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// StorageEventTrendAnalysis contains aggregated trend data
|
||||
type StorageEventTrendAnalysis struct {
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
TrendPeriod string `json:"trend_period"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
TotalEvents int `json:"total_events"`
|
||||
AddEvents int `json:"add_events"`
|
||||
RemoveEvents int `json:"remove_events"`
|
||||
TotalBytesAdded int64 `json:"total_bytes_added"`
|
||||
TotalBytesRemoved int64 `json:"total_bytes_removed"`
|
||||
NetBytesChange int64 `json:"net_bytes_change"`
|
||||
AverageBytesPerAdd float64 `json:"average_bytes_per_add"`
|
||||
AverageBytesPerRemove float64 `json:"average_bytes_per_remove"`
|
||||
LargestAddEvent int64 `json:"largest_add_event"`
|
||||
LargestRemoveEvent int64 `json:"largest_remove_event"`
|
||||
DailyBreakdown []DailyStats `json:"daily_breakdown,omitempty"`
|
||||
}
|
||||
|
||||
// DailyStats represents daily aggregated statistics
|
||||
type DailyStats struct {
|
||||
Date time.Time `json:"date"`
|
||||
AddEvents int `json:"add_events"`
|
||||
RemoveEvents int `json:"remove_events"`
|
||||
BytesAdded int64 `json:"bytes_added"`
|
||||
BytesRemoved int64 `json:"bytes_removed"`
|
||||
NetChange int64 `json:"net_change"`
|
||||
}
|
||||
|
||||
type GetStorageUsageEventsTrendAnalysisUseCase interface {
|
||||
Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*StorageEventTrendAnalysis, error)
|
||||
}
|
||||
|
||||
type getStorageUsageEventsTrendAnalysisUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo storageusageevent.StorageUsageEventRepository
|
||||
}
|
||||
|
||||
func NewGetStorageUsageEventsTrendAnalysisUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) GetStorageUsageEventsTrendAnalysisUseCase {
|
||||
logger = logger.Named("GetStorageUsageEventsTrendAnalysisUseCase")
|
||||
return &getStorageUsageEventsTrendAnalysisUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *getStorageUsageEventsTrendAnalysisUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*StorageEventTrendAnalysis, error) {
|
||||
//
|
||||
// STEP 1: Validation (reuse from GetStorageUsageEventsUseCase).
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req == nil {
|
||||
e["request"] = "Request is required"
|
||||
} else {
|
||||
if req.UserID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if req.TrendPeriod == "" {
|
||||
e["trend_period"] = "Trend period is required"
|
||||
} else if req.TrendPeriod != "7days" && req.TrendPeriod != "monthly" && req.TrendPeriod != "yearly" && req.TrendPeriod != "custom" {
|
||||
e["trend_period"] = "Trend period must be one of: 7days, monthly, yearly, custom"
|
||||
}
|
||||
|
||||
switch req.TrendPeriod {
|
||||
case "monthly":
|
||||
if req.Year == nil {
|
||||
e["year"] = "Year is required for monthly trend"
|
||||
}
|
||||
if req.Month == nil {
|
||||
e["month"] = "Month is required for monthly trend"
|
||||
}
|
||||
case "yearly":
|
||||
if req.Year == nil {
|
||||
e["year"] = "Year is required for yearly trend"
|
||||
}
|
||||
case "custom":
|
||||
if req.Days == nil || *req.Days <= 0 {
|
||||
e["days"] = "Days must be greater than 0 for custom trend"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating get storage usage events trend analysis",
|
||||
zap.Any("error", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get events based on trend period.
|
||||
//
|
||||
|
||||
var events []*storageusageevent.StorageUsageEvent
|
||||
var err error
|
||||
var startDate, endDate time.Time
|
||||
|
||||
switch req.TrendPeriod {
|
||||
case "7days":
|
||||
events, err = uc.repo.GetLast7DaysEvents(ctx, req.UserID)
|
||||
endDate = time.Now().Truncate(24 * time.Hour)
|
||||
startDate = endDate.Add(-6 * 24 * time.Hour)
|
||||
|
||||
case "monthly":
|
||||
events, err = uc.repo.GetMonthlyEvents(ctx, req.UserID, *req.Year, *req.Month)
|
||||
startDate = time.Date(*req.Year, *req.Month, 1, 0, 0, 0, 0, time.UTC)
|
||||
endDate = startDate.AddDate(0, 1, -1)
|
||||
|
||||
case "yearly":
|
||||
events, err = uc.repo.GetYearlyEvents(ctx, req.UserID, *req.Year)
|
||||
startDate = time.Date(*req.Year, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
endDate = time.Date(*req.Year, 12, 31, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
case "custom":
|
||||
events, err = uc.repo.GetLastNDaysEvents(ctx, req.UserID, *req.Days)
|
||||
endDate = time.Now().Truncate(24 * time.Hour)
|
||||
startDate = endDate.Add(-time.Duration(*req.Days-1) * 24 * time.Hour)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to get storage usage events for trend analysis",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("trend_period", req.TrendPeriod),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Analyze events and build trend analysis.
|
||||
//
|
||||
|
||||
analysis := uc.analyzeEvents(req.UserID, req.TrendPeriod, startDate, endDate, events)
|
||||
|
||||
uc.logger.Debug("Successfully analyzed storage usage events trend",
|
||||
zap.String("user_id", req.UserID.String()),
|
||||
zap.String("trend_period", req.TrendPeriod),
|
||||
zap.Int("total_events", analysis.TotalEvents),
|
||||
zap.Int64("net_bytes_change", analysis.NetBytesChange))
|
||||
|
||||
return analysis, nil
|
||||
}
|
||||
|
||||
// analyzeEvents processes the events and generates trend analysis
|
||||
func (uc *getStorageUsageEventsTrendAnalysisUseCaseImpl) analyzeEvents(userID gocql.UUID, trendPeriod string, startDate, endDate time.Time, events []*storageusageevent.StorageUsageEvent) *StorageEventTrendAnalysis {
|
||||
analysis := &StorageEventTrendAnalysis{
|
||||
UserID: userID,
|
||||
TrendPeriod: trendPeriod,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
return analysis
|
||||
}
|
||||
|
||||
// Daily breakdown map
|
||||
dailyMap := make(map[string]*DailyStats)
|
||||
|
||||
// Process each event
|
||||
for _, event := range events {
|
||||
analysis.TotalEvents++
|
||||
|
||||
if event.Operation == "add" {
|
||||
analysis.AddEvents++
|
||||
analysis.TotalBytesAdded += event.FileSize
|
||||
if event.FileSize > analysis.LargestAddEvent {
|
||||
analysis.LargestAddEvent = event.FileSize
|
||||
}
|
||||
} else if event.Operation == "remove" {
|
||||
analysis.RemoveEvents++
|
||||
analysis.TotalBytesRemoved += event.FileSize
|
||||
if event.FileSize > analysis.LargestRemoveEvent {
|
||||
analysis.LargestRemoveEvent = event.FileSize
|
||||
}
|
||||
}
|
||||
|
||||
// Daily breakdown
|
||||
dayKey := event.EventDay.Format("2006-01-02")
|
||||
if dailyMap[dayKey] == nil {
|
||||
dailyMap[dayKey] = &DailyStats{
|
||||
Date: event.EventDay,
|
||||
}
|
||||
}
|
||||
|
||||
daily := dailyMap[dayKey]
|
||||
if event.Operation == "add" {
|
||||
daily.AddEvents++
|
||||
daily.BytesAdded += event.FileSize
|
||||
} else if event.Operation == "remove" {
|
||||
daily.RemoveEvents++
|
||||
daily.BytesRemoved += event.FileSize
|
||||
}
|
||||
daily.NetChange = daily.BytesAdded - daily.BytesRemoved
|
||||
}
|
||||
|
||||
// Calculate derived metrics
|
||||
analysis.NetBytesChange = analysis.TotalBytesAdded - analysis.TotalBytesRemoved
|
||||
|
||||
if analysis.AddEvents > 0 {
|
||||
analysis.AverageBytesPerAdd = float64(analysis.TotalBytesAdded) / float64(analysis.AddEvents)
|
||||
}
|
||||
|
||||
if analysis.RemoveEvents > 0 {
|
||||
analysis.AverageBytesPerRemove = float64(analysis.TotalBytesRemoved) / float64(analysis.RemoveEvents)
|
||||
}
|
||||
|
||||
// Convert daily map to slice and sort by date
|
||||
for _, daily := range dailyMap {
|
||||
analysis.DailyBreakdown = append(analysis.DailyBreakdown, *daily)
|
||||
}
|
||||
|
||||
// Sort daily breakdown by date
|
||||
for i := 0; i < len(analysis.DailyBreakdown)-1; i++ {
|
||||
for j := i + 1; j < len(analysis.DailyBreakdown); j++ {
|
||||
if analysis.DailyBreakdown[i].Date.After(analysis.DailyBreakdown[j].Date) {
|
||||
analysis.DailyBreakdown[i], analysis.DailyBreakdown[j] = analysis.DailyBreakdown[j], analysis.DailyBreakdown[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return analysis
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package storageusageevent
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
|
||||
)
|
||||
|
||||
// Wire providers for storage usage event use cases
|
||||
|
||||
func ProvideCreateStorageUsageEventUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) CreateStorageUsageEventUseCase {
|
||||
return NewCreateStorageUsageEventUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetStorageUsageEventsUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) GetStorageUsageEventsUseCase {
|
||||
return NewGetStorageUsageEventsUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideGetStorageUsageEventsTrendAnalysisUseCase(
|
||||
cfg *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) GetStorageUsageEventsTrendAnalysisUseCase {
|
||||
return NewGetStorageUsageEventsTrendAnalysisUseCase(cfg, logger, repo)
|
||||
}
|
||||
|
||||
func ProvideDeleteByUserUseCase(
|
||||
logger *zap.Logger,
|
||||
repo storageusageevent.StorageUsageEventRepository,
|
||||
) DeleteByUserUseCase {
|
||||
return NewDeleteByUserUseCase(logger, repo)
|
||||
}
|
||||
140
cloud/maplefile-backend/internal/usecase/tag/assigntag.go
Normal file
140
cloud/maplefile-backend/internal/usecase/tag/assigntag.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/assigntag.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type AssignTagUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewAssignTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) *AssignTagUseCase {
|
||||
return &AssignTagUseCase{
|
||||
tagRepo: tagRepo,
|
||||
collectionRepo: collectionRepo,
|
||||
fileRepo: fileRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AssignTagUseCase) Execute(ctx context.Context, userID, tagID, entityID gocql.UUID, entityType string) error {
|
||||
// Validate entity type
|
||||
if entityType != dom_tag.EntityTypeCollection && entityType != dom_tag.EntityTypeFile {
|
||||
return fmt.Errorf("invalid entity type: %s", entityType)
|
||||
}
|
||||
|
||||
// Verify tag exists and belongs to user
|
||||
tag, err := uc.tagRepo.GetByID(ctx, tagID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tag not found: %w", err)
|
||||
}
|
||||
if tag.UserID != userID {
|
||||
return fmt.Errorf("unauthorized: tag does not belong to user")
|
||||
}
|
||||
|
||||
// Handle collection or file assignment
|
||||
switch entityType {
|
||||
case dom_tag.EntityTypeCollection:
|
||||
return uc.assignToCollection(ctx, userID, tag, entityID)
|
||||
case dom_tag.EntityTypeFile:
|
||||
return uc.assignToFile(ctx, userID, tag, entityID)
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AssignTagUseCase) assignToCollection(ctx context.Context, userID gocql.UUID, tag *dom_tag.Tag, collectionID gocql.UUID) error {
|
||||
// Get collection
|
||||
collection, err := uc.collectionRepo.Get(ctx, collectionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("collection not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if tag is already assigned
|
||||
for _, existingTag := range collection.Tags {
|
||||
if existingTag.ID == tag.ID {
|
||||
return nil // Already assigned, idempotent
|
||||
}
|
||||
}
|
||||
|
||||
// Add embedded tag to collection's tag list
|
||||
embeddedTag := tag.ToEmbeddedTag()
|
||||
collection.Tags = append(collection.Tags, *embeddedTag)
|
||||
collection.ModifiedAt = time.Now()
|
||||
|
||||
// Update collection (this will trigger denormalized table maintenance in the repository)
|
||||
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
|
||||
return fmt.Errorf("failed to update collection: %w", err)
|
||||
}
|
||||
|
||||
// Create lightweight assignment tracking
|
||||
assignment := &dom_tag.TagAssignment{
|
||||
ID: gocql.TimeUUID(),
|
||||
UserID: userID,
|
||||
TagID: tag.ID,
|
||||
EntityID: collectionID,
|
||||
EntityType: dom_tag.EntityTypeCollection,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := uc.tagRepo.AssignTag(ctx, assignment); err != nil {
|
||||
return fmt.Errorf("failed to create tag assignment: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *AssignTagUseCase) assignToFile(ctx context.Context, userID gocql.UUID, tag *dom_tag.Tag, fileID gocql.UUID) error {
|
||||
// Get file metadata
|
||||
file, err := uc.fileRepo.Get(fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if tag is already assigned
|
||||
for _, existingTag := range file.Tags {
|
||||
if existingTag.ID == tag.ID {
|
||||
return nil // Already assigned, idempotent
|
||||
}
|
||||
}
|
||||
|
||||
// Add embedded tag to file's tag list
|
||||
embeddedTag := tag.ToEmbeddedTag()
|
||||
file.Tags = append(file.Tags, *embeddedTag)
|
||||
file.ModifiedAt = time.Now()
|
||||
|
||||
// Update file (this will trigger denormalized table maintenance in the repository)
|
||||
if err := uc.fileRepo.Update(file); err != nil {
|
||||
return fmt.Errorf("failed to update file: %w", err)
|
||||
}
|
||||
|
||||
// Create lightweight assignment tracking
|
||||
assignment := &dom_tag.TagAssignment{
|
||||
ID: gocql.TimeUUID(),
|
||||
UserID: userID,
|
||||
TagID: tag.ID,
|
||||
EntityID: fileID,
|
||||
EntityType: dom_tag.EntityTypeFile,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := uc.tagRepo.AssignTag(ctx, assignment); err != nil {
|
||||
return fmt.Errorf("failed to create tag assignment: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
48
cloud/maplefile-backend/internal/usecase/tag/create.go
Normal file
48
cloud/maplefile-backend/internal/usecase/tag/create.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/create.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type CreateTagUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
}
|
||||
|
||||
func NewCreateTagUseCase(tagRepo dom_tag.Repository) *CreateTagUseCase {
|
||||
return &CreateTagUseCase{
|
||||
tagRepo: tagRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute creates a new tag with encrypted data (E2EE)
|
||||
// The client is responsible for:
|
||||
// 1. Generating a random tag key
|
||||
// 2. Encrypting name and color with the tag key
|
||||
// 3. Encrypting the tag key with the user's master key
|
||||
// 4. Sending all encrypted data to the backend
|
||||
func (uc *CreateTagUseCase) Execute(ctx context.Context, tag *dom_tag.Tag) error {
|
||||
// Validate encrypted data is present
|
||||
if tag.EncryptedName == "" {
|
||||
return fmt.Errorf("encrypted tag name is required")
|
||||
}
|
||||
if tag.EncryptedColor == "" {
|
||||
return fmt.Errorf("encrypted tag color is required")
|
||||
}
|
||||
if tag.EncryptedTagKey == nil || len(tag.EncryptedTagKey.Ciphertext) == 0 {
|
||||
return fmt.Errorf("encrypted tag key is required")
|
||||
}
|
||||
if tag.State == "" {
|
||||
return fmt.Errorf("tag state is required")
|
||||
}
|
||||
|
||||
// Backend never sees plaintext - only validates encrypted data exists
|
||||
if err := uc.tagRepo.Create(ctx, tag); err != nil {
|
||||
return fmt.Errorf("failed to create tag: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
128
cloud/maplefile-backend/internal/usecase/tag/delete.go
Normal file
128
cloud/maplefile-backend/internal/usecase/tag/delete.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/delete.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type DeleteTagUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewDeleteTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
logger *zap.Logger,
|
||||
) *DeleteTagUseCase {
|
||||
return &DeleteTagUseCase{
|
||||
tagRepo: tagRepo,
|
||||
collectionRepo: collectionRepo,
|
||||
fileRepo: fileRepo,
|
||||
logger: logger.Named("DeleteTagUseCase"),
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *DeleteTagUseCase) Execute(ctx context.Context, userID, id gocql.UUID) error {
|
||||
// Remove tag from all collections and files before deleting
|
||||
// This runs synchronously to ensure data consistency
|
||||
uc.logger.Info("🏷️ TAG DELETE: Starting tag deletion",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
// Remove from all collections
|
||||
collections, err := uc.collectionRepo.ListByTagID(ctx, id)
|
||||
if err != nil {
|
||||
uc.logger.Error("🏷️ TAG DELETE: Failed to list collections by tag",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Info("🏷️ TAG DELETE: Found collections to clean up",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.Int("count", len(collections)))
|
||||
|
||||
for _, collection := range collections {
|
||||
// Remove the tag from the collection's Tags array
|
||||
newTags := make([]dom_tag.EmbeddedTag, 0, len(collection.Tags))
|
||||
for _, embeddedTag := range collection.Tags {
|
||||
if embeddedTag.ID != id {
|
||||
newTags = append(newTags, embeddedTag)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newTags) != len(collection.Tags) {
|
||||
collection.Tags = newTags
|
||||
collection.ModifiedAt = time.Now()
|
||||
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
|
||||
uc.logger.Error("🏷️ TAG DELETE: Failed to update collection",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.String("collection_id", collection.ID.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Debug("🏷️ TAG DELETE: Removed tag from collection",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.String("collection_id", collection.ID.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from all files
|
||||
files, err := uc.fileRepo.ListByTagID(ctx, id)
|
||||
if err != nil {
|
||||
uc.logger.Error("🏷️ TAG DELETE: Failed to list files by tag",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Info("🏷️ TAG DELETE: Found files to clean up",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.Int("count", len(files)))
|
||||
|
||||
for _, file := range files {
|
||||
// Remove the tag from the file's Tags array
|
||||
newTags := make([]dom_tag.EmbeddedTag, 0, len(file.Tags))
|
||||
for _, embeddedTag := range file.Tags {
|
||||
if embeddedTag.ID != id {
|
||||
newTags = append(newTags, embeddedTag)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newTags) != len(file.Tags) {
|
||||
file.Tags = newTags
|
||||
file.ModifiedAt = time.Now()
|
||||
if err := uc.fileRepo.Update(file); err != nil {
|
||||
uc.logger.Error("🏷️ TAG DELETE: Failed to update file",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.String("file_id", file.ID.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Debug("🏷️ TAG DELETE: Removed tag from file",
|
||||
zap.String("tag_id", id.String()),
|
||||
zap.String("file_id", file.ID.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, delete the tag itself
|
||||
if err := uc.tagRepo.DeleteByID(ctx, userID, id); err != nil {
|
||||
return fmt.Errorf("failed to delete tag: %w", err)
|
||||
}
|
||||
|
||||
uc.logger.Info("🏷️ TAG DELETE: Completed tag deletion",
|
||||
zap.String("tag_id", id.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
30
cloud/maplefile-backend/internal/usecase/tag/getbyid.go
Normal file
30
cloud/maplefile-backend/internal/usecase/tag/getbyid.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/getbyid.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type GetTagByIDUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
}
|
||||
|
||||
func NewGetTagByIDUseCase(tagRepo dom_tag.Repository) *GetTagByIDUseCase {
|
||||
return &GetTagByIDUseCase{
|
||||
tagRepo: tagRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *GetTagByIDUseCase) Execute(ctx context.Context, id gocql.UUID) (*dom_tag.Tag, error) {
|
||||
tag, err := uc.tagRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tag: %w", err)
|
||||
}
|
||||
|
||||
return tag, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/gettagsforentity.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type GetTagsForEntityUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
}
|
||||
|
||||
func NewGetTagsForEntityUseCase(tagRepo dom_tag.Repository) *GetTagsForEntityUseCase {
|
||||
return &GetTagsForEntityUseCase{
|
||||
tagRepo: tagRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *GetTagsForEntityUseCase) Execute(ctx context.Context, entityID gocql.UUID, entityType string) ([]*dom_tag.Tag, error) {
|
||||
// Validate entity type
|
||||
if entityType != dom_tag.EntityTypeCollection && entityType != dom_tag.EntityTypeFile {
|
||||
return nil, fmt.Errorf("invalid entity type: %s", entityType)
|
||||
}
|
||||
|
||||
tags, err := uc.tagRepo.GetTagsForEntity(ctx, entityID, entityType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tags for entity: %w", err)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
30
cloud/maplefile-backend/internal/usecase/tag/listbyuser.go
Normal file
30
cloud/maplefile-backend/internal/usecase/tag/listbyuser.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/listbyuser.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type ListTagsByUserUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
}
|
||||
|
||||
func NewListTagsByUserUseCase(tagRepo dom_tag.Repository) *ListTagsByUserUseCase {
|
||||
return &ListTagsByUserUseCase{
|
||||
tagRepo: tagRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ListTagsByUserUseCase) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_tag.Tag, error) {
|
||||
tags, err := uc.tagRepo.ListByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list tags: %w", err)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/listcollectionsbytag.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type ListCollectionsByTagUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
}
|
||||
|
||||
func NewListCollectionsByTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) *ListCollectionsByTagUseCase {
|
||||
return &ListCollectionsByTagUseCase{
|
||||
tagRepo: tagRepo,
|
||||
collectionRepo: collectionRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute returns collections that have ALL specified tags (AND logic)
|
||||
func (uc *ListCollectionsByTagUseCase) Execute(
|
||||
ctx context.Context,
|
||||
userID gocql.UUID,
|
||||
tagIDs []gocql.UUID,
|
||||
limit int,
|
||||
cursor string,
|
||||
) ([]*dom_collection.Collection, string, error) {
|
||||
if len(tagIDs) == 0 {
|
||||
return []*dom_collection.Collection{}, "", nil
|
||||
}
|
||||
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
// Verify all tags exist and belong to user
|
||||
for _, tagID := range tagIDs {
|
||||
tag, err := uc.tagRepo.GetByID(ctx, tagID)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("tag %s not found: %w", tagID.String(), err)
|
||||
}
|
||||
if tag.UserID != userID {
|
||||
return nil, "", fmt.Errorf("unauthorized: tag %s does not belong to user", tagID.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Query each tag and build collection ID -> count map
|
||||
collectionCounts := make(map[gocql.UUID]int)
|
||||
collectionData := make(map[gocql.UUID]*dom_collection.Collection)
|
||||
|
||||
for _, tagID := range tagIDs {
|
||||
// Get collections for this tag
|
||||
collections, err := uc.collectionRepo.ListByTagID(ctx, tagID)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to list collections by tag: %w", err)
|
||||
}
|
||||
|
||||
for _, collection := range collections {
|
||||
// Filter by user ownership
|
||||
if collection.OwnerID != userID {
|
||||
continue
|
||||
}
|
||||
|
||||
collectionCounts[collection.ID]++
|
||||
|
||||
// Store collection data on first occurrence
|
||||
if _, exists := collectionData[collection.ID]; !exists {
|
||||
collectionData[collection.ID] = collection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to collections that have ALL tags (AND logic)
|
||||
var intersectionCollections []*dom_collection.Collection
|
||||
for collectionID, count := range collectionCounts {
|
||||
if count == len(tagIDs) {
|
||||
intersectionCollections = append(intersectionCollections, collectionData[collectionID])
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by ID for consistent ordering
|
||||
sort.Slice(intersectionCollections, func(i, j int) bool {
|
||||
return intersectionCollections[i].ID.String() < intersectionCollections[j].ID.String()
|
||||
})
|
||||
|
||||
// Apply cursor pagination
|
||||
var cursorCollectionID gocql.UUID
|
||||
if cursor != "" {
|
||||
decoded, err := base64.StdEncoding.DecodeString(cursor)
|
||||
if err == nil {
|
||||
cursorCollectionID, _ = gocql.ParseUUID(string(decoded))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by cursor
|
||||
filteredCollections := make([]*dom_collection.Collection, 0, limit)
|
||||
foundCursor := cursorCollectionID.String() == "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
for _, collection := range intersectionCollections {
|
||||
if !foundCursor {
|
||||
if collection.ID.String() > cursorCollectionID.String() {
|
||||
foundCursor = true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filteredCollections = append(filteredCollections, collection)
|
||||
if len(filteredCollections) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Generate next cursor
|
||||
var nextCursor string
|
||||
if len(filteredCollections) == limit && len(intersectionCollections) > len(filteredCollections) {
|
||||
lastCollectionID := filteredCollections[len(filteredCollections)-1].ID
|
||||
nextCursor = base64.StdEncoding.EncodeToString([]byte(lastCollectionID.String()))
|
||||
}
|
||||
|
||||
return filteredCollections, nextCursor, nil
|
||||
}
|
||||
132
cloud/maplefile-backend/internal/usecase/tag/listfilesbytag.go
Normal file
132
cloud/maplefile-backend/internal/usecase/tag/listfilesbytag.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/listfilesbytag.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type ListFilesByTagUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewListFilesByTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) *ListFilesByTagUseCase {
|
||||
return &ListFilesByTagUseCase{
|
||||
tagRepo: tagRepo,
|
||||
fileRepo: fileRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute returns files that have ALL specified tags (AND logic)
|
||||
func (uc *ListFilesByTagUseCase) Execute(
|
||||
ctx context.Context,
|
||||
userID gocql.UUID,
|
||||
tagIDs []gocql.UUID,
|
||||
limit int,
|
||||
cursor string,
|
||||
) ([]*dom_file.File, string, error) {
|
||||
if len(tagIDs) == 0 {
|
||||
return []*dom_file.File{}, "", nil
|
||||
}
|
||||
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
// Verify all tags exist and belong to user
|
||||
for _, tagID := range tagIDs {
|
||||
tag, err := uc.tagRepo.GetByID(ctx, tagID)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("tag %s not found: %w", tagID.String(), err)
|
||||
}
|
||||
if tag.UserID != userID {
|
||||
return nil, "", fmt.Errorf("unauthorized: tag %s does not belong to user", tagID.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Query each tag and build file ID -> count map
|
||||
fileCounts := make(map[gocql.UUID]int)
|
||||
fileData := make(map[gocql.UUID]*dom_file.File)
|
||||
|
||||
for _, tagID := range tagIDs {
|
||||
// Get files for this tag
|
||||
files, err := uc.fileRepo.ListByTagID(ctx, tagID)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to list files by tag: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
// Filter by user ownership
|
||||
if file.OwnerID != userID {
|
||||
continue
|
||||
}
|
||||
|
||||
fileCounts[file.ID]++
|
||||
|
||||
// Store file data on first occurrence
|
||||
if _, exists := fileData[file.ID]; !exists {
|
||||
fileData[file.ID] = file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to files that have ALL tags (AND logic)
|
||||
var intersectionFiles []*dom_file.File
|
||||
for fileID, count := range fileCounts {
|
||||
if count == len(tagIDs) {
|
||||
intersectionFiles = append(intersectionFiles, fileData[fileID])
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by ID for consistent ordering
|
||||
sort.Slice(intersectionFiles, func(i, j int) bool {
|
||||
return intersectionFiles[i].ID.String() < intersectionFiles[j].ID.String()
|
||||
})
|
||||
|
||||
// Apply cursor pagination
|
||||
var cursorFileID gocql.UUID
|
||||
if cursor != "" {
|
||||
decoded, err := base64.StdEncoding.DecodeString(cursor)
|
||||
if err == nil {
|
||||
cursorFileID, _ = gocql.ParseUUID(string(decoded))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by cursor
|
||||
filteredFiles := make([]*dom_file.File, 0, limit)
|
||||
foundCursor := cursorFileID.String() == "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
for _, file := range intersectionFiles {
|
||||
if !foundCursor {
|
||||
if file.ID.String() > cursorFileID.String() {
|
||||
foundCursor = true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
if len(filteredFiles) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Generate next cursor
|
||||
var nextCursor string
|
||||
if len(filteredFiles) == limit && len(intersectionFiles) > len(filteredFiles) {
|
||||
lastFileID := filteredFiles[len(filteredFiles)-1].ID
|
||||
nextCursor = base64.StdEncoding.EncodeToString([]byte(lastFileID.String()))
|
||||
}
|
||||
|
||||
return filteredFiles, nextCursor, nil
|
||||
}
|
||||
86
cloud/maplefile-backend/internal/usecase/tag/provider.go
Normal file
86
cloud/maplefile-backend/internal/usecase/tag/provider.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package tag
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
// Wire providers for tag use cases
|
||||
|
||||
func ProvideCreateTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
) *CreateTagUseCase {
|
||||
return NewCreateTagUseCase(tagRepo)
|
||||
}
|
||||
|
||||
func ProvideGetTagByIDUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
) *GetTagByIDUseCase {
|
||||
return NewGetTagByIDUseCase(tagRepo)
|
||||
}
|
||||
|
||||
func ProvideListTagsByUserUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
) *ListTagsByUserUseCase {
|
||||
return NewListTagsByUserUseCase(tagRepo)
|
||||
}
|
||||
|
||||
func ProvideUpdateTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
logger *zap.Logger,
|
||||
) *UpdateTagUseCase {
|
||||
return NewUpdateTagUseCase(tagRepo, collectionRepo, fileRepo, logger)
|
||||
}
|
||||
|
||||
func ProvideDeleteTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
logger *zap.Logger,
|
||||
) *DeleteTagUseCase {
|
||||
return NewDeleteTagUseCase(tagRepo, collectionRepo, fileRepo, logger)
|
||||
}
|
||||
|
||||
func ProvideAssignTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) *AssignTagUseCase {
|
||||
return NewAssignTagUseCase(tagRepo, collectionRepo, fileRepo)
|
||||
}
|
||||
|
||||
func ProvideUnassignTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) *UnassignTagUseCase {
|
||||
return NewUnassignTagUseCase(tagRepo, collectionRepo, fileRepo)
|
||||
}
|
||||
|
||||
func ProvideGetTagsForEntityUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
) *GetTagsForEntityUseCase {
|
||||
return NewGetTagsForEntityUseCase(tagRepo)
|
||||
}
|
||||
|
||||
func ProvideListCollectionsByTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
) *ListCollectionsByTagUseCase {
|
||||
return NewListCollectionsByTagUseCase(tagRepo, collectionRepo)
|
||||
}
|
||||
|
||||
func ProvideListFilesByTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) *ListFilesByTagUseCase {
|
||||
return NewListFilesByTagUseCase(tagRepo, fileRepo)
|
||||
}
|
||||
|
||||
// NOTE: CreateDefaultTagsUseCase removed - default tags must be created client-side
|
||||
// due to E2EE. The client creates default tags after first login using the user's master key.
|
||||
125
cloud/maplefile-backend/internal/usecase/tag/unassigntag.go
Normal file
125
cloud/maplefile-backend/internal/usecase/tag/unassigntag.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/unassigntag.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type UnassignTagUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewUnassignTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) *UnassignTagUseCase {
|
||||
return &UnassignTagUseCase{
|
||||
tagRepo: tagRepo,
|
||||
collectionRepo: collectionRepo,
|
||||
fileRepo: fileRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *UnassignTagUseCase) Execute(ctx context.Context, tagID, entityID gocql.UUID, entityType string) error {
|
||||
// Validate entity type
|
||||
if entityType != dom_tag.EntityTypeCollection && entityType != dom_tag.EntityTypeFile {
|
||||
return fmt.Errorf("invalid entity type: %s", entityType)
|
||||
}
|
||||
|
||||
// Handle collection or file unassignment
|
||||
switch entityType {
|
||||
case dom_tag.EntityTypeCollection:
|
||||
return uc.unassignFromCollection(ctx, tagID, entityID)
|
||||
case dom_tag.EntityTypeFile:
|
||||
return uc.unassignFromFile(ctx, tagID, entityID)
|
||||
default:
|
||||
return fmt.Errorf("unsupported entity type: %s", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *UnassignTagUseCase) unassignFromCollection(ctx context.Context, tagID, collectionID gocql.UUID) error {
|
||||
// Get collection
|
||||
collection, err := uc.collectionRepo.Get(ctx, collectionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("collection not found: %w", err)
|
||||
}
|
||||
|
||||
// Remove tag from collection's tag list
|
||||
newTags := make([]dom_tag.EmbeddedTag, 0, len(collection.Tags))
|
||||
found := false
|
||||
for _, existingTag := range collection.Tags {
|
||||
if existingTag.ID != tagID {
|
||||
newTags = append(newTags, existingTag)
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil // Tag wasn't assigned, idempotent
|
||||
}
|
||||
|
||||
collection.Tags = newTags
|
||||
collection.ModifiedAt = time.Now()
|
||||
|
||||
// Update collection (this will trigger denormalized table maintenance in the repository)
|
||||
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
|
||||
return fmt.Errorf("failed to update collection: %w", err)
|
||||
}
|
||||
|
||||
// Remove lightweight assignment tracking
|
||||
if err := uc.tagRepo.UnassignTag(ctx, tagID, collectionID, dom_tag.EntityTypeCollection); err != nil {
|
||||
return fmt.Errorf("failed to remove tag assignment: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *UnassignTagUseCase) unassignFromFile(ctx context.Context, tagID, fileID gocql.UUID) error {
|
||||
// Get file metadata
|
||||
file, err := uc.fileRepo.Get(fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
// Remove tag from file's tag list
|
||||
newTags := make([]dom_tag.EmbeddedTag, 0, len(file.Tags))
|
||||
found := false
|
||||
for _, existingTag := range file.Tags {
|
||||
if existingTag.ID != tagID {
|
||||
newTags = append(newTags, existingTag)
|
||||
} else {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil // Tag wasn't assigned, idempotent
|
||||
}
|
||||
|
||||
file.Tags = newTags
|
||||
file.ModifiedAt = time.Now()
|
||||
|
||||
// Update file (this will trigger denormalized table maintenance in the repository)
|
||||
if err := uc.fileRepo.Update(file); err != nil {
|
||||
return fmt.Errorf("failed to update file: %w", err)
|
||||
}
|
||||
|
||||
// Remove lightweight assignment tracking
|
||||
if err := uc.tagRepo.UnassignTag(ctx, tagID, fileID, dom_tag.EntityTypeFile); err != nil {
|
||||
return fmt.Errorf("failed to remove tag assignment: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
154
cloud/maplefile-backend/internal/usecase/tag/update.go
Normal file
154
cloud/maplefile-backend/internal/usecase/tag/update.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/update.go
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
)
|
||||
|
||||
type UpdateTagUseCase struct {
|
||||
tagRepo dom_tag.Repository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewUpdateTagUseCase(
|
||||
tagRepo dom_tag.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
logger *zap.Logger,
|
||||
) *UpdateTagUseCase {
|
||||
return &UpdateTagUseCase{
|
||||
tagRepo: tagRepo,
|
||||
collectionRepo: collectionRepo,
|
||||
fileRepo: fileRepo,
|
||||
logger: logger.Named("UpdateTagUseCase"),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute updates a tag with new encrypted data (E2EE)
|
||||
// The client must provide the complete updated tag with newly encrypted fields
|
||||
// This will propagate the tag update to all collections and files that have this tag embedded
|
||||
func (uc *UpdateTagUseCase) Execute(ctx context.Context, tag *dom_tag.Tag) error {
|
||||
// Validate encrypted data is present
|
||||
if tag.EncryptedName == "" {
|
||||
return fmt.Errorf("encrypted tag name is required")
|
||||
}
|
||||
if tag.EncryptedColor == "" {
|
||||
return fmt.Errorf("encrypted tag color is required")
|
||||
}
|
||||
if tag.EncryptedTagKey == nil || len(tag.EncryptedTagKey.Ciphertext) == 0 {
|
||||
return fmt.Errorf("encrypted tag key is required")
|
||||
}
|
||||
|
||||
// Update modified timestamp
|
||||
tag.ModifiedAt = time.Now()
|
||||
|
||||
// Backend never sees plaintext - only validates encrypted data exists and updates
|
||||
if err := uc.tagRepo.Update(ctx, tag); err != nil {
|
||||
return fmt.Errorf("failed to update tag: %w", err)
|
||||
}
|
||||
|
||||
// Propagate tag updates to all collections and files that have this tag embedded
|
||||
// This runs asynchronously in the background to avoid blocking the API response
|
||||
go uc.propagateTagUpdate(context.Background(), tag)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// propagateTagUpdate updates all collections and files that have this tag embedded
|
||||
// This is called asynchronously after the tag is updated
|
||||
func (uc *UpdateTagUseCase) propagateTagUpdate(ctx context.Context, tag *dom_tag.Tag) {
|
||||
uc.logger.Info("🏷️ TAG PROPAGATION: Starting tag update propagation",
|
||||
zap.String("tag_id", tag.ID.String()))
|
||||
|
||||
// Create the updated embedded tag
|
||||
updatedEmbeddedTag := tag.ToEmbeddedTag()
|
||||
|
||||
// Update all collections with this tag
|
||||
collections, err := uc.collectionRepo.ListByTagID(ctx, tag.ID)
|
||||
if err != nil {
|
||||
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to list collections by tag",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Info("🏷️ TAG PROPAGATION: Found collections to update",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.Int("count", len(collections)))
|
||||
|
||||
for _, collection := range collections {
|
||||
// Update the embedded tag in the collection
|
||||
updated := false
|
||||
for i, embeddedTag := range collection.Tags {
|
||||
if embeddedTag.ID == tag.ID {
|
||||
collection.Tags[i] = *updatedEmbeddedTag
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
collection.ModifiedAt = time.Now()
|
||||
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
|
||||
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to update collection",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.String("collection_id", collection.ID.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Debug("🏷️ TAG PROPAGATION: Updated collection",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.String("collection_id", collection.ID.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update all files with this tag
|
||||
files, err := uc.fileRepo.ListByTagID(ctx, tag.ID)
|
||||
if err != nil {
|
||||
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to list files by tag",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Info("🏷️ TAG PROPAGATION: Found files to update",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.Int("count", len(files)))
|
||||
|
||||
for _, file := range files {
|
||||
// Update the embedded tag in the file
|
||||
updated := false
|
||||
for i, embeddedTag := range file.Tags {
|
||||
if embeddedTag.ID == tag.ID {
|
||||
file.Tags[i] = *updatedEmbeddedTag
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
file.ModifiedAt = time.Now()
|
||||
if err := uc.fileRepo.Update(file); err != nil {
|
||||
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to update file",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.String("file_id", file.ID.String()),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
uc.logger.Debug("🏷️ TAG PROPAGATION: Updated file",
|
||||
zap.String("tag_id", tag.ID.String()),
|
||||
zap.String("file_id", file.ID.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uc.logger.Info("🏷️ TAG PROPAGATION: Completed tag update propagation",
|
||||
zap.String("tag_id", tag.ID.String()))
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/anonymize_old_ips.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
)
|
||||
|
||||
type AnonymizeOldIPsUseCase interface {
|
||||
Execute(ctx context.Context, cutoffDate time.Time) (int, error)
|
||||
}
|
||||
|
||||
type anonymizeOldIPsUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_user.Repository
|
||||
}
|
||||
|
||||
func NewAnonymizeOldIPsUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) AnonymizeOldIPsUseCase {
|
||||
logger = logger.Named("UserAnonymizeOldIPsUseCase")
|
||||
return &anonymizeOldIPsUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *anonymizeOldIPsUseCaseImpl) Execute(ctx context.Context, cutoffDate time.Time) (int, error) {
|
||||
uc.logger.Debug("Anonymizing old IPs in user tables",
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
|
||||
count, err := uc.repo.AnonymizeOldIPs(ctx, cutoffDate)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to anonymize old IPs in user tables",
|
||||
zap.Error(err),
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
uc.logger.Info("Successfully anonymized old IPs in user tables",
|
||||
zap.Int("count", count),
|
||||
zap.Time("cutoff_date", cutoffDate))
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/usecase/user/anonymize_user_ips_immediately.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
||||
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
|
||||
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// AnonymizeUserIPsImmediatelyUseCase immediately anonymizes all IP addresses for a user
|
||||
// Used for GDPR right-to-be-forgotten implementation
|
||||
type AnonymizeUserIPsImmediatelyUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID) error
|
||||
}
|
||||
|
||||
type anonymizeUserIPsImmediatelyUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
userRepo dom_user.Repository
|
||||
collectionRepo dom_collection.CollectionRepository
|
||||
fileRepo dom_file.FileMetadataRepository
|
||||
}
|
||||
|
||||
func NewAnonymizeUserIPsImmediatelyUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
userRepo dom_user.Repository,
|
||||
collectionRepo dom_collection.CollectionRepository,
|
||||
fileRepo dom_file.FileMetadataRepository,
|
||||
) AnonymizeUserIPsImmediatelyUseCase {
|
||||
logger = logger.Named("AnonymizeUserIPsImmediatelyUseCase")
|
||||
return &anonymizeUserIPsImmediatelyUseCaseImpl{config, logger, userRepo, collectionRepo, fileRepo}
|
||||
}
|
||||
|
||||
func (uc *anonymizeUserIPsImmediatelyUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating immediate IP anonymization",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
uc.logger.Info("Starting immediate IP anonymization for user (GDPR mode)",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
//
|
||||
// STEP 2: Anonymize user metadata IPs
|
||||
//
|
||||
|
||||
uc.logger.Debug("Anonymizing user metadata IPs",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
err := uc.userRepo.AnonymizeUserIPs(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to anonymize user metadata IPs",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Debug("✅ User metadata IPs anonymized")
|
||||
|
||||
//
|
||||
// STEP 3: Anonymize collection IPs for all user's collections
|
||||
//
|
||||
|
||||
uc.logger.Debug("Anonymizing collection IPs for user's collections",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
collectionCount, err := uc.collectionRepo.AnonymizeCollectionIPsByOwner(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to anonymize collection IPs",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Debug("✅ Collection IPs anonymized",
|
||||
zap.Int("collection_count", collectionCount))
|
||||
|
||||
//
|
||||
// STEP 4: Anonymize file IPs for all user's files
|
||||
//
|
||||
|
||||
uc.logger.Debug("Anonymizing file IPs for user's files",
|
||||
zap.String("user_id", userID.String()))
|
||||
|
||||
fileCount, err := uc.fileRepo.AnonymizeFileIPsByOwner(ctx, userID)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to anonymize file IPs",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Debug("✅ File IPs anonymized",
|
||||
zap.Int("file_count", fileCount))
|
||||
|
||||
//
|
||||
// SUCCESS
|
||||
//
|
||||
|
||||
uc.logger.Info("✅ Successfully anonymized all IPs for user",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Int("collections_anonymized", collectionCount),
|
||||
zap.Int("files_anonymized", fileCount))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
// NOTE: Unit tests for AnonymizeUserIPsImmediatelyUseCase would require mocks.
|
||||
// For now, this use case will be tested via integration tests.
|
||||
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
|
||||
|
||||
func TestAnonymizeUserIPsImmediatelyUseCase_Constructor(t *testing.T) {
|
||||
// Test that constructor creates use case successfully
|
||||
cfg := &config.Configuration{}
|
||||
logger := zap.NewNop()
|
||||
|
||||
useCase := NewAnonymizeUserIPsImmediatelyUseCase(cfg, logger, nil, nil, nil)
|
||||
|
||||
if useCase == nil {
|
||||
t.Error("Expected use case to be created, got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// monorepo/cloud/backend/internal/usecase/user/clear_user_cache.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
// ClearUserCacheUseCase clears all cache entries for a user
|
||||
// Used for GDPR right-to-be-forgotten implementation
|
||||
type ClearUserCacheUseCase interface {
|
||||
Execute(ctx context.Context, userID gocql.UUID, email string) error
|
||||
}
|
||||
|
||||
type clearUserCacheUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewClearUserCacheUseCase(
|
||||
config *config.Configuration,
|
||||
logger *zap.Logger,
|
||||
) ClearUserCacheUseCase {
|
||||
logger = logger.Named("ClearUserCacheUseCase")
|
||||
return &clearUserCacheUseCaseImpl{config, logger}
|
||||
}
|
||||
|
||||
func (uc *clearUserCacheUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if userID.String() == "" {
|
||||
e["user_id"] = "User ID is required"
|
||||
}
|
||||
if email == "" {
|
||||
e["email"] = "Email is required"
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Failed validating clear user cache",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
uc.logger.Info("Clearing user cache for GDPR deletion",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.String("email", email))
|
||||
|
||||
//
|
||||
// STEP 2: Clear cache entries
|
||||
//
|
||||
|
||||
// LIMITATION: The current cache implementation (Cassandra-based) stores sessions
|
||||
// keyed by refresh token (format: "refresh:{token}"), not by user ID.
|
||||
// This means we cannot efficiently query and delete all sessions for a specific user.
|
||||
//
|
||||
// CURRENT APPROACH:
|
||||
// - All cache entries have TTL (Time To Live)
|
||||
// - Sessions expire automatically based on JWT refresh token duration
|
||||
// - No user data is permanently stored in cache
|
||||
//
|
||||
// GDPR COMPLIANCE:
|
||||
// - Cache data is transient and automatically expires
|
||||
// - No PII is stored permanently in cache
|
||||
// - User deletion still complies with GDPR right-to-erasure
|
||||
//
|
||||
// FUTURE ENHANCEMENT OPTIONS:
|
||||
// 1. Add a secondary index/table: user_id → [session_keys]
|
||||
// 2. Switch to Redis and use SCAN with pattern: "refresh:*" + check user_id
|
||||
// 3. Implement a logout-all-sessions endpoint that users can call before deletion
|
||||
// 4. Store session keys in user metadata for easy cleanup
|
||||
//
|
||||
// For now, we log this operation and rely on TTL expiration.
|
||||
|
||||
uc.logger.Info("✅ User cache cleared (sessions will expire via TTL)",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.String("note", "Active sessions expire based on JWT refresh token duration"))
|
||||
|
||||
// TODO: Implement actual cache cleanup when we have a user_id → session_key mapping
|
||||
// For now, this is a placeholder that documents the limitation
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
func TestClearUserCacheUseCase_Constructor(t *testing.T) {
|
||||
// Test that constructor creates use case successfully
|
||||
cfg := &config.Configuration{}
|
||||
logger := zap.NewNop()
|
||||
|
||||
useCase := NewClearUserCacheUseCase(cfg, logger)
|
||||
|
||||
if useCase == nil {
|
||||
t.Error("Expected use case to be created, got nil")
|
||||
}
|
||||
}
|
||||
50
cloud/maplefile-backend/internal/usecase/user/create.go
Normal file
50
cloud/maplefile-backend/internal/usecase/user/create.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/create.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UserCreateUseCase interface {
|
||||
Execute(ctx context.Context, user *dom_user.User) error
|
||||
}
|
||||
|
||||
type userCreateUseCaseImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_user.Repository
|
||||
}
|
||||
|
||||
func NewUserCreateUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserCreateUseCase {
|
||||
logger = logger.Named("UserCreateUseCase")
|
||||
return &userCreateUseCaseImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *userCreateUseCaseImpl) Execute(ctx context.Context, user *dom_user.User) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if user == nil {
|
||||
e["user"] = "missing value"
|
||||
} else {
|
||||
//TODO: IMPL.
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Validation failed for upsert",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
//
|
||||
// STEP 2: Insert into database.
|
||||
//
|
||||
|
||||
return uc.repo.Create(ctx, user)
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/getbyid.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
type UserDeleteUserByEmailUseCase interface {
|
||||
Execute(ctx context.Context, email string) error
|
||||
}
|
||||
|
||||
type userDeleteUserByEmailImpl struct {
|
||||
config *config.Configuration
|
||||
logger *zap.Logger
|
||||
repo dom_user.Repository
|
||||
}
|
||||
|
||||
func NewUserDeleteUserByEmailUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserDeleteUserByEmailUseCase {
|
||||
logger = logger.Named("UserDeleteUserByEmailUseCase")
|
||||
return &userDeleteUserByEmailImpl{config, logger, repo}
|
||||
}
|
||||
|
||||
func (uc *userDeleteUserByEmailImpl) Execute(ctx context.Context, email string) error {
|
||||
//
|
||||
// STEP 1: Validation.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if email == "" {
|
||||
e["email"] = "missing value"
|
||||
} else {
|
||||
//TODO: IMPL.
|
||||
}
|
||||
if len(e) != 0 {
|
||||
uc.logger.Warn("Validation failed for upsert",
|
||||
zap.Any("error", e))
|
||||
return httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 2: Get from database.
|
||||
//
|
||||
|
||||
return uc.repo.DeleteByEmail(ctx, email)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue