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,148 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
)
// DeletePagesService handles page deletion operations
type DeletePagesService interface {
DeletePages(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.DeletePagesInput) (*pageusecase.DeletePagesOutput, error)
DeleteAllPages(ctx context.Context, tenantID, siteID gocql.UUID) (*pageusecase.DeletePagesOutput, error)
}
type deletePagesService struct {
// Focused usecases
validateSiteUC *pageusecase.ValidateSiteForDeletionUseCase
deletePagesRepoUC *pageusecase.DeletePagesFromRepoUseCase
deletePagesSearchUC *pageusecase.DeletePagesFromSearchUseCase
logger *zap.Logger
}
// NewDeletePagesService creates a new DeletePagesService
func NewDeletePagesService(
validateSiteUC *pageusecase.ValidateSiteForDeletionUseCase,
deletePagesRepoUC *pageusecase.DeletePagesFromRepoUseCase,
deletePagesSearchUC *pageusecase.DeletePagesFromSearchUseCase,
logger *zap.Logger,
) DeletePagesService {
return &deletePagesService{
validateSiteUC: validateSiteUC,
deletePagesRepoUC: deletePagesRepoUC,
deletePagesSearchUC: deletePagesSearchUC,
logger: logger.Named("delete-pages-service"),
}
}
// DeletePages orchestrates the deletion of specific pages
func (s *deletePagesService) DeletePages(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.DeletePagesInput) (*pageusecase.DeletePagesOutput, error) {
s.logger.Info("deleting pages",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.Int("page_count", len(input.PageIDs)))
// Step 1: Validate site
_, err := s.validateSiteUC.Execute(ctx, tenantID, siteID)
if err != nil {
s.logger.Error("failed to validate site", zap.Error(err))
return nil, err
}
// Step 2: Delete pages from database
deleteResult, err := s.deletePagesRepoUC.Execute(ctx, siteID, input.PageIDs)
if err != nil {
s.logger.Error("failed to delete pages from database", zap.Error(err))
return nil, err
}
// Step 3: Delete pages from search index (only if database delete succeeded)
deindexedCount := 0
if deleteResult.DeletedCount > 0 {
// Only delete pages that were successfully deleted from database
successfulPageIDs := s.getSuccessfulPageIDs(input.PageIDs, deleteResult.FailedPages)
if len(successfulPageIDs) > 0 {
deindexedCount, _ = s.deletePagesSearchUC.Execute(ctx, siteID, successfulPageIDs)
}
}
// Step 4: Build output
message := fmt.Sprintf("Successfully deleted %d pages from database, removed %d from search index",
deleteResult.DeletedCount, deindexedCount)
if len(deleteResult.FailedPages) > 0 {
message += fmt.Sprintf(", failed %d pages", len(deleteResult.FailedPages))
}
s.logger.Info("pages deleted successfully",
zap.String("site_id", siteID.String()),
zap.Int("deleted", deleteResult.DeletedCount),
zap.Int("deindexed", deindexedCount),
zap.Int("failed", len(deleteResult.FailedPages)))
return &pageusecase.DeletePagesOutput{
DeletedCount: deleteResult.DeletedCount,
DeindexedCount: deindexedCount,
FailedPages: deleteResult.FailedPages,
Message: message,
}, nil
}
// DeleteAllPages orchestrates the deletion of all pages for a site
func (s *deletePagesService) DeleteAllPages(ctx context.Context, tenantID, siteID gocql.UUID) (*pageusecase.DeletePagesOutput, error) {
s.logger.Info("deleting all pages",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()))
// Step 1: Validate site
_, err := s.validateSiteUC.Execute(ctx, tenantID, siteID)
if err != nil {
s.logger.Error("failed to validate site", zap.Error(err))
return nil, err
}
// Step 2: Delete all pages from database
count, err := s.deletePagesRepoUC.ExecuteDeleteAll(ctx, siteID)
if err != nil {
s.logger.Error("failed to delete all pages from database", zap.Error(err))
return nil, err
}
// Step 3: Delete all documents from search index
_ = s.deletePagesSearchUC.ExecuteDeleteAll(ctx, siteID)
s.logger.Info("all pages deleted successfully",
zap.String("site_id", siteID.String()),
zap.Int64("count", count))
return &pageusecase.DeletePagesOutput{
DeletedCount: int(count),
DeindexedCount: int(count),
Message: fmt.Sprintf("Successfully deleted all %d pages", count),
}, nil
}
// Helper: Get list of page IDs that were successfully deleted (exclude failed ones)
func (s *deletePagesService) getSuccessfulPageIDs(allPageIDs, failedPageIDs []string) []string {
if len(failedPageIDs) == 0 {
return allPageIDs
}
failedMap := make(map[string]bool, len(failedPageIDs))
for _, id := range failedPageIDs {
failedMap[id] = true
}
successful := make([]string, 0, len(allPageIDs)-len(failedPageIDs))
for _, id := range allPageIDs {
if !failedMap[id] {
successful = append(successful, id)
}
}
return successful
}

View file

@ -0,0 +1,80 @@
package page
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
)
// SearchPagesService handles page search operations
type SearchPagesService interface {
SearchPages(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.SearchPagesInput) (*pageusecase.SearchPagesOutput, error)
}
type searchPagesService struct {
// Focused usecases
validateSiteUC *pageusecase.ValidateSiteForSearchUseCase
executeSearchUC *pageusecase.ExecuteSearchQueryUseCase
incrementCountUC *pageusecase.IncrementSearchCountUseCase
logger *zap.Logger
}
// NewSearchPagesService creates a new SearchPagesService
func NewSearchPagesService(
validateSiteUC *pageusecase.ValidateSiteForSearchUseCase,
executeSearchUC *pageusecase.ExecuteSearchQueryUseCase,
incrementCountUC *pageusecase.IncrementSearchCountUseCase,
logger *zap.Logger,
) SearchPagesService {
return &searchPagesService{
validateSiteUC: validateSiteUC,
executeSearchUC: executeSearchUC,
incrementCountUC: incrementCountUC,
logger: logger.Named("search-pages-service"),
}
}
// SearchPages orchestrates the page search workflow
func (s *searchPagesService) SearchPages(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.SearchPagesInput) (*pageusecase.SearchPagesOutput, error) {
s.logger.Info("searching pages",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.String("query", input.Query))
// Step 1: Validate site (no quota check - usage-based billing)
site, err := s.validateSiteUC.Execute(ctx, tenantID, siteID)
if err != nil {
s.logger.Error("failed to validate site", zap.Error(err))
return nil, err
}
// Step 2: Execute search query
result, err := s.executeSearchUC.Execute(ctx, siteID, input.Query, input.Limit, input.Offset, input.Filter)
if err != nil {
s.logger.Error("failed to execute search", zap.Error(err))
return nil, err
}
// Step 3: Increment search count (for billing tracking)
if err := s.incrementCountUC.Execute(ctx, site); err != nil {
s.logger.Warn("failed to increment search count (non-fatal)", zap.Error(err))
// Don't fail the search operation
}
s.logger.Info("pages searched successfully",
zap.String("site_id", siteID.String()),
zap.Int64("total_hits", result.TotalHits))
return &pageusecase.SearchPagesOutput{
Hits: result.Hits,
Query: result.Query,
ProcessingTimeMs: result.ProcessingTimeMs,
TotalHits: result.TotalHits,
Limit: result.Limit,
Offset: result.Offset,
}, nil
}

View file

@ -0,0 +1,133 @@
package page
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
)
// SyncStatusService handles sync status operations
type SyncStatusService interface {
GetSyncStatus(ctx context.Context, tenantID, siteID gocql.UUID) (*pageusecase.SyncStatusOutput, error)
GetPageDetails(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.GetPageDetailsInput) (*pageusecase.PageDetailsOutput, error)
}
type syncStatusService struct {
// Focused usecases
validateSiteUC *pageusecase.ValidateSiteForStatusUseCase
getStatsUC *pageusecase.GetPageStatisticsUseCase
getIndexStatusUC *pageusecase.GetSearchIndexStatusUseCase
getPageByIDUC *pageusecase.GetPageByIDUseCase
logger *zap.Logger
}
// NewSyncStatusService creates a new SyncStatusService
func NewSyncStatusService(
validateSiteUC *pageusecase.ValidateSiteForStatusUseCase,
getStatsUC *pageusecase.GetPageStatisticsUseCase,
getIndexStatusUC *pageusecase.GetSearchIndexStatusUseCase,
getPageByIDUC *pageusecase.GetPageByIDUseCase,
logger *zap.Logger,
) SyncStatusService {
return &syncStatusService{
validateSiteUC: validateSiteUC,
getStatsUC: getStatsUC,
getIndexStatusUC: getIndexStatusUC,
getPageByIDUC: getPageByIDUC,
logger: logger.Named("sync-status-service"),
}
}
// GetSyncStatus orchestrates retrieving sync status for a site
func (s *syncStatusService) GetSyncStatus(ctx context.Context, tenantID, siteID gocql.UUID) (*pageusecase.SyncStatusOutput, error) {
s.logger.Info("getting sync status",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()))
// Step 1: Validate site
site, err := s.validateSiteUC.Execute(ctx, tenantID, siteID)
if err != nil {
s.logger.Error("failed to validate site", zap.Error(err))
return nil, err
}
// Step 2: Get page statistics
stats, err := s.getStatsUC.Execute(ctx, siteID)
if err != nil {
s.logger.Error("failed to get page statistics", zap.Error(err))
return nil, err
}
// Step 3: Get search index status
indexStatus, err := s.getIndexStatusUC.Execute(ctx, siteID)
if err != nil {
s.logger.Error("failed to get search index status", zap.Error(err))
return nil, err
}
s.logger.Info("sync status retrieved successfully",
zap.String("site_id", siteID.String()),
zap.Int64("total_pages", stats.TotalPages))
// Step 4: Build output
return &pageusecase.SyncStatusOutput{
SiteID: siteID.String(),
TotalPages: stats.TotalPages,
PublishedPages: stats.PublishedPages,
DraftPages: stats.DraftPages,
LastSyncedAt: site.LastIndexedAt,
PagesIndexedMonth: site.MonthlyPagesIndexed,
SearchRequestsMonth: site.SearchRequestsCount,
LastResetAt: site.LastResetAt,
SearchIndexStatus: indexStatus.Status,
SearchIndexDocCount: indexStatus.DocumentCount,
}, nil
}
// GetPageDetails orchestrates retrieving details for a specific page
func (s *syncStatusService) GetPageDetails(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.GetPageDetailsInput) (*pageusecase.PageDetailsOutput, error) {
s.logger.Info("getting page details",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.String("page_id", input.PageID))
// Step 1: Validate site
_, err := s.validateSiteUC.Execute(ctx, tenantID, siteID)
if err != nil {
s.logger.Error("failed to validate site", zap.Error(err))
return nil, err
}
// Step 2: Get page by ID
page, err := s.getPageByIDUC.Execute(ctx, siteID, input.PageID)
if err != nil {
s.logger.Error("failed to get page", zap.Error(err))
return nil, err
}
s.logger.Info("page details retrieved successfully",
zap.String("site_id", siteID.String()),
zap.String("page_id", input.PageID))
// Step 3: Build output
isIndexed := !page.IndexedAt.IsZero()
return &pageusecase.PageDetailsOutput{
PageID: page.PageID,
Title: page.Title,
Excerpt: page.Excerpt,
URL: page.URL,
Status: page.Status,
PostType: page.PostType,
Author: page.Author,
PublishedAt: page.PublishedAt,
ModifiedAt: page.ModifiedAt,
IndexedAt: page.IndexedAt,
MeilisearchDocID: page.MeilisearchDocID,
IsIndexed: isIndexed,
}, nil
}

View file

@ -0,0 +1,143 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
)
// SyncPagesService handles page synchronization operations
type SyncPagesService interface {
SyncPages(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.SyncPagesInput) (*pageusecase.SyncPagesOutput, error)
}
type syncPagesService struct {
// Focused usecases
validateSiteUC *pageusecase.ValidateSiteUseCase
ensureIndexUC *pageusecase.EnsureSearchIndexUseCase
createPageUC *pageusecase.CreatePageEntityUseCase
upsertPageUC *pageusecase.UpsertPageUseCase
indexPageUC *pageusecase.IndexPageToSearchUseCase
updateUsageUC *pageusecase.UpdateSiteUsageUseCase
logger *zap.Logger
}
// NewSyncPagesService creates a new SyncPagesService
func NewSyncPagesService(
validateSiteUC *pageusecase.ValidateSiteUseCase,
ensureIndexUC *pageusecase.EnsureSearchIndexUseCase,
createPageUC *pageusecase.CreatePageEntityUseCase,
upsertPageUC *pageusecase.UpsertPageUseCase,
indexPageUC *pageusecase.IndexPageToSearchUseCase,
updateUsageUC *pageusecase.UpdateSiteUsageUseCase,
logger *zap.Logger,
) SyncPagesService {
return &syncPagesService{
validateSiteUC: validateSiteUC,
ensureIndexUC: ensureIndexUC,
createPageUC: createPageUC,
upsertPageUC: upsertPageUC,
indexPageUC: indexPageUC,
updateUsageUC: updateUsageUC,
logger: logger.Named("sync-pages-service"),
}
}
// SyncPages orchestrates the page synchronization workflow
func (s *syncPagesService) SyncPages(ctx context.Context, tenantID, siteID gocql.UUID, input *pageusecase.SyncPagesInput) (*pageusecase.SyncPagesOutput, error) {
s.logger.Info("syncing pages",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.Int("page_count", len(input.Pages)))
// Step 1: Validate site (no quota check - usage-based billing)
site, err := s.validateSiteUC.Execute(ctx, tenantID, siteID)
if err != nil {
s.logger.Error("failed to validate site", zap.Error(err))
return nil, err
}
// Step 2: Ensure search index exists
if err := s.ensureIndexUC.Execute(ctx, siteID); err != nil {
s.logger.Error("failed to ensure search index", zap.Error(err))
return nil, err
}
// Step 3: Process pages (create, save, prepare for indexing)
syncedCount, failedPages, pagesToIndex := s.processPages(ctx, siteID, site.TenantID, input.Pages)
// Step 4: Bulk index pages to search
indexedCount, err := s.indexPageUC.Execute(ctx, siteID, pagesToIndex)
if err != nil {
s.logger.Error("failed to index pages", zap.Error(err))
return nil, err
}
// Step 5: Update site usage tracking (for billing)
if indexedCount > 0 {
if err := s.updateUsageUC.Execute(ctx, site, indexedCount); err != nil {
s.logger.Warn("failed to update usage (non-fatal)", zap.Error(err))
// Don't fail the whole operation
}
}
// Step 6: Build output
message := fmt.Sprintf("Successfully synced %d pages, indexed %d pages", syncedCount, indexedCount)
if len(failedPages) > 0 {
message += fmt.Sprintf(", failed %d pages", len(failedPages))
}
s.logger.Info("pages synced successfully",
zap.String("site_id", siteID.String()),
zap.Int("synced", syncedCount),
zap.Int("indexed", indexedCount),
zap.Int("failed", len(failedPages)))
return &pageusecase.SyncPagesOutput{
SyncedCount: syncedCount,
IndexedCount: indexedCount,
FailedPages: failedPages,
Message: message,
}, nil
}
// Helper: Process pages - create entities, save to DB, collect pages to index
func (s *syncPagesService) processPages(
ctx context.Context,
siteID, tenantID gocql.UUID,
pages []pageusecase.SyncPageInput,
) (int, []string, []*domainpage.Page) {
syncedCount := 0
var failedPages []string
var pagesToIndex []*domainpage.Page
for _, pageInput := range pages {
// Create page entity (usecase)
page, err := s.createPageUC.Execute(siteID, tenantID, pageInput)
if err != nil {
failedPages = append(failedPages, pageInput.PageID)
continue
}
// Save to database (usecase)
if err := s.upsertPageUC.Execute(ctx, page); err != nil {
failedPages = append(failedPages, pageInput.PageID)
continue
}
syncedCount++
// Collect pages that should be indexed
if page.ShouldIndex() {
pagesToIndex = append(pagesToIndex, page)
}
}
return syncedCount, failedPages, pagesToIndex
}