Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}