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,57 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcrypt"
|
||||
)
|
||||
|
||||
// CreatePageEntityUseCase creates a domain page entity from input
|
||||
type CreatePageEntityUseCase struct {
|
||||
ipEncryptor *ipcrypt.IPEncryptor
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideCreatePageEntityUseCase creates a new CreatePageEntityUseCase
|
||||
func ProvideCreatePageEntityUseCase(
|
||||
ipEncryptor *ipcrypt.IPEncryptor,
|
||||
logger *zap.Logger,
|
||||
) *CreatePageEntityUseCase {
|
||||
return &CreatePageEntityUseCase{
|
||||
ipEncryptor: ipEncryptor,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute converts SyncPageInput to a domain Page entity
|
||||
func (uc *CreatePageEntityUseCase) Execute(
|
||||
siteID, tenantID gocql.UUID,
|
||||
input SyncPageInput,
|
||||
) (*domainpage.Page, error) {
|
||||
// Encrypt IP address (CWE-359: GDPR compliance)
|
||||
encryptedIP, err := uc.ipEncryptor.Encrypt(input.IPAddress)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to encrypt IP address",
|
||||
zap.String("page_id", input.PageID),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return domainpage.NewPage(
|
||||
siteID,
|
||||
tenantID,
|
||||
input.PageID,
|
||||
input.Title,
|
||||
input.Content,
|
||||
input.Excerpt,
|
||||
input.URL,
|
||||
input.Status,
|
||||
input.PostType,
|
||||
input.Author,
|
||||
input.PublishedAt,
|
||||
input.ModifiedAt,
|
||||
encryptedIP,
|
||||
), nil
|
||||
}
|
||||
190
cloud/maplepress-backend/internal/usecase/page/delete.go
Normal file
190
cloud/maplepress-backend/internal/usecase/page/delete.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// DeletePagesUseCase handles page deletion
|
||||
type DeletePagesUseCase struct {
|
||||
pageRepo domainpage.Repository
|
||||
siteRepo domainsite.Repository
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideDeletePagesUseCase creates a new DeletePagesUseCase
|
||||
func ProvideDeletePagesUseCase(
|
||||
pageRepo domainpage.Repository,
|
||||
siteRepo domainsite.Repository,
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *DeletePagesUseCase {
|
||||
return &DeletePagesUseCase{
|
||||
pageRepo: pageRepo,
|
||||
siteRepo: siteRepo,
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// DeletePagesInput is the input for deleting pages
|
||||
type DeletePagesInput struct {
|
||||
PageIDs []string `json:"page_ids"`
|
||||
}
|
||||
|
||||
// DeletePagesOutput is the output after deleting pages
|
||||
type DeletePagesOutput struct {
|
||||
DeletedCount int `json:"deleted_count"`
|
||||
DeindexedCount int `json:"deindexed_count"`
|
||||
FailedPages []string `json:"failed_pages,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Execute deletes pages from both database and search index
|
||||
func (uc *DeletePagesUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID, input *DeletePagesInput) (*DeletePagesOutput, error) {
|
||||
uc.logger.Info("executing delete pages use case",
|
||||
zap.String("tenant_id", tenantID.String()),
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int("page_count", len(input.PageIDs)))
|
||||
|
||||
// Get site to validate
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
deletedCount := 0
|
||||
deindexedCount := 0
|
||||
var failedPages []string
|
||||
|
||||
// Delete pages from database
|
||||
if len(input.PageIDs) > 1 {
|
||||
// Use batch delete for multiple pages
|
||||
if err := uc.pageRepo.DeleteMultiple(ctx, siteID, input.PageIDs); err != nil {
|
||||
uc.logger.Error("failed to batch delete pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to delete pages: %w", err)
|
||||
}
|
||||
deletedCount = len(input.PageIDs)
|
||||
} else if len(input.PageIDs) == 1 {
|
||||
// Single page delete
|
||||
if err := uc.pageRepo.Delete(ctx, siteID, input.PageIDs[0]); err != nil {
|
||||
uc.logger.Error("failed to delete page",
|
||||
zap.String("page_id", input.PageIDs[0]),
|
||||
zap.Error(err))
|
||||
failedPages = append(failedPages, input.PageIDs[0])
|
||||
} else {
|
||||
deletedCount = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from search index
|
||||
if deletedCount > 0 {
|
||||
if len(input.PageIDs) > 1 {
|
||||
// Batch delete from Meilisearch
|
||||
_, err := uc.searchClient.DeleteDocuments(siteID.String(), input.PageIDs)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to delete documents from search index", zap.Error(err))
|
||||
// Don't fail the whole operation since database delete succeeded
|
||||
} else {
|
||||
deindexedCount = len(input.PageIDs)
|
||||
}
|
||||
} else if len(input.PageIDs) == 1 && len(failedPages) == 0 {
|
||||
// Single document delete
|
||||
_, err := uc.searchClient.DeleteDocument(siteID.String(), input.PageIDs[0])
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to delete document from search index",
|
||||
zap.String("page_id", input.PageIDs[0]),
|
||||
zap.Error(err))
|
||||
// Don't fail the whole operation since database delete succeeded
|
||||
} else {
|
||||
deindexedCount = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uc.logger.Info("pages deleted successfully",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int("deleted", deletedCount),
|
||||
zap.Int("deindexed", deindexedCount),
|
||||
zap.Int("failed", len(failedPages)))
|
||||
|
||||
message := fmt.Sprintf("Successfully deleted %d pages from database, removed %d from search index", deletedCount, deindexedCount)
|
||||
if len(failedPages) > 0 {
|
||||
message += fmt.Sprintf(", failed %d pages", len(failedPages))
|
||||
}
|
||||
|
||||
return &DeletePagesOutput{
|
||||
DeletedCount: deletedCount,
|
||||
DeindexedCount: deindexedCount,
|
||||
FailedPages: failedPages,
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteAllPagesInput is the input for deleting all pages for a site
|
||||
type DeleteAllPagesInput struct{}
|
||||
|
||||
// ExecuteDeleteAll deletes all pages for a site
|
||||
func (uc *DeletePagesUseCase) ExecuteDeleteAll(ctx context.Context, tenantID, siteID gocql.UUID) (*DeletePagesOutput, error) {
|
||||
uc.logger.Info("executing delete all pages use case",
|
||||
zap.String("tenant_id", tenantID.String()),
|
||||
zap.String("site_id", siteID.String()))
|
||||
|
||||
// Get site to validate
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
// Count pages before deletion
|
||||
count, err := uc.pageRepo.CountBySiteID(ctx, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to count pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to count pages: %w", err)
|
||||
}
|
||||
|
||||
// Delete all pages from database
|
||||
if err := uc.pageRepo.DeleteBySiteID(ctx, siteID); err != nil {
|
||||
uc.logger.Error("failed to delete all pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to delete pages: %w", err)
|
||||
}
|
||||
|
||||
// Delete all documents from search index
|
||||
_, err = uc.searchClient.DeleteAllDocuments(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to delete all documents from search index", zap.Error(err))
|
||||
// Don't fail the whole operation since database delete succeeded
|
||||
}
|
||||
|
||||
uc.logger.Info("all pages deleted successfully",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int64("count", count))
|
||||
|
||||
return &DeletePagesOutput{
|
||||
DeletedCount: int(count),
|
||||
DeindexedCount: int(count),
|
||||
Message: fmt.Sprintf("Successfully deleted all %d pages", count),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
)
|
||||
|
||||
// DeletePagesFromRepoUseCase deletes pages from the database repository
|
||||
type DeletePagesFromRepoUseCase struct {
|
||||
pageRepo domainpage.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideDeletePagesFromRepoUseCase creates a new DeletePagesFromRepoUseCase
|
||||
func ProvideDeletePagesFromRepoUseCase(
|
||||
pageRepo domainpage.Repository,
|
||||
logger *zap.Logger,
|
||||
) *DeletePagesFromRepoUseCase {
|
||||
return &DeletePagesFromRepoUseCase{
|
||||
pageRepo: pageRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// DeletePagesResult contains the result of page deletion
|
||||
type DeletePagesResult struct {
|
||||
DeletedCount int
|
||||
FailedPages []string
|
||||
}
|
||||
|
||||
// Execute deletes specific pages from the database
|
||||
func (uc *DeletePagesFromRepoUseCase) Execute(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
pageIDs []string,
|
||||
) (*DeletePagesResult, error) {
|
||||
result := &DeletePagesResult{
|
||||
DeletedCount: 0,
|
||||
FailedPages: []string{},
|
||||
}
|
||||
|
||||
if len(pageIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Use batch delete for multiple pages
|
||||
if len(pageIDs) > 1 {
|
||||
if err := uc.pageRepo.DeleteMultiple(ctx, siteID, pageIDs); err != nil {
|
||||
uc.logger.Error("failed to batch delete pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to delete pages: %w", err)
|
||||
}
|
||||
result.DeletedCount = len(pageIDs)
|
||||
} else {
|
||||
// Single page delete
|
||||
if err := uc.pageRepo.Delete(ctx, siteID, pageIDs[0]); err != nil {
|
||||
uc.logger.Error("failed to delete page",
|
||||
zap.String("page_id", pageIDs[0]),
|
||||
zap.Error(err))
|
||||
result.FailedPages = append(result.FailedPages, pageIDs[0])
|
||||
} else {
|
||||
result.DeletedCount = 1
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ExecuteDeleteAll deletes all pages for a site from the database
|
||||
func (uc *DeletePagesFromRepoUseCase) ExecuteDeleteAll(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
) (int64, error) {
|
||||
// Count pages before deletion
|
||||
count, err := uc.pageRepo.CountBySiteID(ctx, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to count pages", zap.Error(err))
|
||||
return 0, fmt.Errorf("failed to count pages: %w", err)
|
||||
}
|
||||
|
||||
// Delete all pages from database
|
||||
if err := uc.pageRepo.DeleteBySiteID(ctx, siteID); err != nil {
|
||||
uc.logger.Error("failed to delete all pages", zap.Error(err))
|
||||
return 0, fmt.Errorf("failed to delete pages: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// DeletePagesFromSearchUseCase deletes pages from the search index
|
||||
type DeletePagesFromSearchUseCase struct {
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideDeletePagesFromSearchUseCase creates a new DeletePagesFromSearchUseCase
|
||||
func ProvideDeletePagesFromSearchUseCase(
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *DeletePagesFromSearchUseCase {
|
||||
return &DeletePagesFromSearchUseCase{
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute deletes specific pages from the search index
|
||||
func (uc *DeletePagesFromSearchUseCase) Execute(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
pageIDs []string,
|
||||
) (int, error) {
|
||||
if len(pageIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
deindexedCount := 0
|
||||
|
||||
// Batch delete from Meilisearch
|
||||
if len(pageIDs) > 1 {
|
||||
_, err := uc.searchClient.DeleteDocuments(siteID.String(), pageIDs)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to delete documents from search index", zap.Error(err))
|
||||
// Don't fail the whole operation since database delete may have succeeded
|
||||
return 0, nil
|
||||
}
|
||||
deindexedCount = len(pageIDs)
|
||||
} else {
|
||||
// Single document delete
|
||||
_, err := uc.searchClient.DeleteDocument(siteID.String(), pageIDs[0])
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to delete document from search index",
|
||||
zap.String("page_id", pageIDs[0]),
|
||||
zap.Error(err))
|
||||
// Don't fail the whole operation since database delete may have succeeded
|
||||
return 0, nil
|
||||
}
|
||||
deindexedCount = 1
|
||||
}
|
||||
|
||||
return deindexedCount, nil
|
||||
}
|
||||
|
||||
// ExecuteDeleteAll deletes all documents for a site from the search index
|
||||
func (uc *DeletePagesFromSearchUseCase) ExecuteDeleteAll(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
) error {
|
||||
_, err := uc.searchClient.DeleteAllDocuments(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to delete all documents from search index", zap.Error(err))
|
||||
// Don't fail the whole operation since database delete may have succeeded
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// EnsureSearchIndexUseCase ensures search index exists for a site
|
||||
type EnsureSearchIndexUseCase struct {
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideEnsureSearchIndexUseCase creates a new EnsureSearchIndexUseCase
|
||||
func ProvideEnsureSearchIndexUseCase(
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *EnsureSearchIndexUseCase {
|
||||
return &EnsureSearchIndexUseCase{
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute ensures the search index exists, creating it if necessary
|
||||
func (uc *EnsureSearchIndexUseCase) Execute(ctx context.Context, siteID gocql.UUID) error {
|
||||
indexExists, err := uc.searchClient.IndexExists(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to check index existence", zap.Error(err))
|
||||
return fmt.Errorf("failed to check search index: %w", err)
|
||||
}
|
||||
|
||||
if !indexExists {
|
||||
uc.logger.Info("creating search index", zap.String("site_id", siteID.String()))
|
||||
if err := uc.searchClient.CreateIndex(siteID.String()); err != nil {
|
||||
uc.logger.Error("failed to create index", zap.Error(err))
|
||||
return fmt.Errorf("failed to create search index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// ExecuteSearchQueryUseCase performs the actual search query
|
||||
type ExecuteSearchQueryUseCase struct {
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideExecuteSearchQueryUseCase creates a new ExecuteSearchQueryUseCase
|
||||
func ProvideExecuteSearchQueryUseCase(
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *ExecuteSearchQueryUseCase {
|
||||
return &ExecuteSearchQueryUseCase{
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute performs the search query against Meilisearch
|
||||
func (uc *ExecuteSearchQueryUseCase) Execute(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
query string,
|
||||
limit, offset int64,
|
||||
filter string,
|
||||
) (*search.SearchResult, error) {
|
||||
// Set default limits if not provided
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20 // Default to 20 results
|
||||
}
|
||||
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// Build search request
|
||||
searchReq := search.SearchRequest{
|
||||
Query: query,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Filter: filter,
|
||||
}
|
||||
|
||||
// If no filter provided, default to only published pages
|
||||
if searchReq.Filter == "" {
|
||||
searchReq.Filter = "status = publish"
|
||||
}
|
||||
|
||||
// Perform search
|
||||
result, err := uc.searchClient.Search(siteID.String(), searchReq)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to search pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to search pages: %w", err)
|
||||
}
|
||||
|
||||
uc.logger.Info("search completed",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.String("query", query),
|
||||
zap.Int64("total_hits", result.TotalHits),
|
||||
zap.Int64("processing_time_ms", result.ProcessingTimeMs))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
)
|
||||
|
||||
// GetPageByIDUseCase retrieves a specific page by ID
|
||||
type GetPageByIDUseCase struct {
|
||||
pageRepo domainpage.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideGetPageByIDUseCase creates a new GetPageByIDUseCase
|
||||
func ProvideGetPageByIDUseCase(
|
||||
pageRepo domainpage.Repository,
|
||||
logger *zap.Logger,
|
||||
) *GetPageByIDUseCase {
|
||||
return &GetPageByIDUseCase{
|
||||
pageRepo: pageRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute retrieves a page by its ID
|
||||
func (uc *GetPageByIDUseCase) Execute(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
pageID string,
|
||||
) (*domainpage.Page, error) {
|
||||
// Get page from database
|
||||
page, err := uc.pageRepo.GetByID(ctx, siteID, pageID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get page",
|
||||
zap.String("page_id", pageID),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("page not found")
|
||||
}
|
||||
|
||||
uc.logger.Info("page retrieved",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.String("page_id", pageID))
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
)
|
||||
|
||||
// GetPageStatisticsUseCase retrieves page count statistics
|
||||
type GetPageStatisticsUseCase struct {
|
||||
pageRepo domainpage.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideGetPageStatisticsUseCase creates a new GetPageStatisticsUseCase
|
||||
func ProvideGetPageStatisticsUseCase(
|
||||
pageRepo domainpage.Repository,
|
||||
logger *zap.Logger,
|
||||
) *GetPageStatisticsUseCase {
|
||||
return &GetPageStatisticsUseCase{
|
||||
pageRepo: pageRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// PageStatistics contains page count statistics
|
||||
type PageStatistics struct {
|
||||
TotalPages int64
|
||||
PublishedPages int64
|
||||
DraftPages int64
|
||||
}
|
||||
|
||||
// Execute retrieves page statistics for a site
|
||||
func (uc *GetPageStatisticsUseCase) Execute(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
) (*PageStatistics, error) {
|
||||
// Count total pages in database
|
||||
totalPages, err := uc.pageRepo.CountBySiteID(ctx, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to count pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to count pages: %w", err)
|
||||
}
|
||||
|
||||
// Get all pages to count by status (this could be optimized with a dedicated query)
|
||||
pages, err := uc.pageRepo.GetBySiteID(ctx, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to get pages: %w", err)
|
||||
}
|
||||
|
||||
// Count pages by status
|
||||
var publishedPages, draftPages int64
|
||||
for _, page := range pages {
|
||||
if page.Status == "publish" {
|
||||
publishedPages++
|
||||
} else if page.Status == "draft" {
|
||||
draftPages++
|
||||
}
|
||||
}
|
||||
|
||||
uc.logger.Info("page statistics retrieved",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int64("total", totalPages),
|
||||
zap.Int64("published", publishedPages),
|
||||
zap.Int64("draft", draftPages))
|
||||
|
||||
return &PageStatistics{
|
||||
TotalPages: totalPages,
|
||||
PublishedPages: publishedPages,
|
||||
DraftPages: draftPages,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// GetSearchIndexStatusUseCase retrieves search index status information
|
||||
type GetSearchIndexStatusUseCase struct {
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideGetSearchIndexStatusUseCase creates a new GetSearchIndexStatusUseCase
|
||||
func ProvideGetSearchIndexStatusUseCase(
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *GetSearchIndexStatusUseCase {
|
||||
return &GetSearchIndexStatusUseCase{
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SearchIndexStatus contains search index status information
|
||||
type SearchIndexStatus struct {
|
||||
Status string // "not_created", "active", "error"
|
||||
DocumentCount int64
|
||||
}
|
||||
|
||||
// Execute retrieves search index status for a site
|
||||
func (uc *GetSearchIndexStatusUseCase) Execute(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
) (*SearchIndexStatus, error) {
|
||||
status := &SearchIndexStatus{
|
||||
Status: "not_created",
|
||||
DocumentCount: 0,
|
||||
}
|
||||
|
||||
// Check if index exists
|
||||
indexExists, err := uc.searchClient.IndexExists(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to check index existence", zap.Error(err))
|
||||
status.Status = "error"
|
||||
return status, nil
|
||||
}
|
||||
|
||||
if !indexExists {
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// Index exists, mark as active
|
||||
status.Status = "active"
|
||||
|
||||
// Get index stats
|
||||
stats, err := uc.searchClient.GetStats(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get index stats", zap.Error(err))
|
||||
// Don't change status to error, index is still active
|
||||
} else {
|
||||
status.DocumentCount = stats.NumberOfDocuments
|
||||
}
|
||||
|
||||
uc.logger.Info("search index status retrieved",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.String("status", status.Status),
|
||||
zap.Int64("doc_count", status.DocumentCount))
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
)
|
||||
|
||||
// IncrementSearchCountUseCase increments the search request counter for a site
|
||||
type IncrementSearchCountUseCase struct {
|
||||
siteRepo domainsite.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideIncrementSearchCountUseCase creates a new IncrementSearchCountUseCase
|
||||
func ProvideIncrementSearchCountUseCase(
|
||||
siteRepo domainsite.Repository,
|
||||
logger *zap.Logger,
|
||||
) *IncrementSearchCountUseCase {
|
||||
return &IncrementSearchCountUseCase{
|
||||
siteRepo: siteRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute increments the search count and updates the site usage tracking
|
||||
func (uc *IncrementSearchCountUseCase) Execute(
|
||||
ctx context.Context,
|
||||
site *domainsite.Site,
|
||||
) error {
|
||||
// Increment search request count
|
||||
site.IncrementSearchCount()
|
||||
|
||||
uc.logger.Info("incremented search count",
|
||||
zap.String("site_id", site.ID.String()),
|
||||
zap.Int64("new_count", site.SearchRequestsCount))
|
||||
|
||||
// Update usage tracking in database
|
||||
if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil {
|
||||
uc.logger.Error("failed to update search usage", zap.Error(err))
|
||||
// Don't fail the search, just log the error and return
|
||||
return err
|
||||
}
|
||||
|
||||
uc.logger.Info("search usage updated successfully",
|
||||
zap.String("site_id", site.ID.String()),
|
||||
zap.Int64("search_count", site.SearchRequestsCount))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// IndexPageToSearchUseCase indexes pages to the search engine
|
||||
type IndexPageToSearchUseCase struct {
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideIndexPageToSearchUseCase creates a new IndexPageToSearchUseCase
|
||||
func ProvideIndexPageToSearchUseCase(
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *IndexPageToSearchUseCase {
|
||||
return &IndexPageToSearchUseCase{
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute indexes a batch of pages to Meilisearch
|
||||
func (uc *IndexPageToSearchUseCase) Execute(
|
||||
ctx context.Context,
|
||||
siteID gocql.UUID,
|
||||
pages []*domainpage.Page,
|
||||
) (int, error) {
|
||||
if len(pages) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Convert pages to search documents
|
||||
documents := make([]search.PageDocument, 0, len(pages))
|
||||
for _, page := range pages {
|
||||
if page.ShouldIndex() {
|
||||
page.MarkIndexed()
|
||||
|
||||
doc := search.PageDocument{
|
||||
ID: page.PageID,
|
||||
SiteID: page.SiteID.String(),
|
||||
TenantID: page.TenantID.String(),
|
||||
Title: page.Title,
|
||||
Content: page.Content,
|
||||
Excerpt: page.Excerpt,
|
||||
URL: page.URL,
|
||||
Status: page.Status,
|
||||
PostType: page.PostType,
|
||||
Author: page.Author,
|
||||
PublishedAt: page.PublishedAt.Unix(),
|
||||
ModifiedAt: page.ModifiedAt.Unix(),
|
||||
}
|
||||
|
||||
documents = append(documents, doc)
|
||||
}
|
||||
}
|
||||
|
||||
if len(documents) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Bulk index to Meilisearch
|
||||
_, err := uc.searchClient.AddDocuments(siteID.String(), documents)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to index documents", zap.Error(err))
|
||||
return 0, fmt.Errorf("failed to index documents: %w", err)
|
||||
}
|
||||
|
||||
return len(documents), nil
|
||||
}
|
||||
134
cloud/maplepress-backend/internal/usecase/page/search.go
Normal file
134
cloud/maplepress-backend/internal/usecase/page/search.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// SearchPagesUseCase handles page search functionality
|
||||
type SearchPagesUseCase struct {
|
||||
siteRepo domainsite.Repository
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideSearchPagesUseCase creates a new SearchPagesUseCase
|
||||
func ProvideSearchPagesUseCase(
|
||||
siteRepo domainsite.Repository,
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *SearchPagesUseCase {
|
||||
return &SearchPagesUseCase{
|
||||
siteRepo: siteRepo,
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SearchPagesInput is the input for searching pages
|
||||
type SearchPagesInput struct {
|
||||
Query string `json:"query"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
Filter string `json:"filter,omitempty"` // e.g., "status = publish AND post_type = post"
|
||||
}
|
||||
|
||||
// SearchPagesOutput is the output after searching pages
|
||||
type SearchPagesOutput struct {
|
||||
Hits interface{} `json:"hits"` // meilisearch.Hits
|
||||
Query string `json:"query"`
|
||||
ProcessingTimeMs int64 `json:"processing_time_ms"`
|
||||
TotalHits int64 `json:"total_hits"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
// Execute performs a search on the site's indexed pages
|
||||
func (uc *SearchPagesUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID, input *SearchPagesInput) (*SearchPagesOutput, error) {
|
||||
uc.logger.Info("executing search pages use case",
|
||||
zap.String("tenant_id", tenantID.String()),
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.String("query", input.Query))
|
||||
|
||||
// Get site to validate and check quotas
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
// No quota checking - usage-based billing (anti-abuse via rate limiting only)
|
||||
|
||||
// Set default limits if not provided
|
||||
limit := input.Limit
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20 // Default to 20 results
|
||||
}
|
||||
|
||||
offset := input.Offset
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// Build search request
|
||||
searchReq := search.SearchRequest{
|
||||
Query: input.Query,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Filter: input.Filter,
|
||||
}
|
||||
|
||||
// If no filter provided, default to only published pages
|
||||
if searchReq.Filter == "" {
|
||||
searchReq.Filter = "status = publish"
|
||||
}
|
||||
|
||||
// Perform search
|
||||
result, err := uc.searchClient.Search(siteID.String(), searchReq)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to search pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to search pages: %w", err)
|
||||
}
|
||||
|
||||
// Increment search request count (for usage tracking/billing)
|
||||
site.IncrementSearchCount()
|
||||
uc.logger.Info("incremented search count",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int64("new_count", site.SearchRequestsCount))
|
||||
|
||||
if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil {
|
||||
uc.logger.Error("failed to update search usage", zap.Error(err))
|
||||
// Don't fail the search, just log the error
|
||||
} else {
|
||||
uc.logger.Info("search usage updated successfully",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int64("search_count", site.SearchRequestsCount))
|
||||
}
|
||||
|
||||
uc.logger.Info("search completed successfully",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.String("query", input.Query),
|
||||
zap.Int64("total_hits", result.TotalHits),
|
||||
zap.Int64("processing_time_ms", result.ProcessingTimeMs))
|
||||
|
||||
return &SearchPagesOutput{
|
||||
Hits: result.Hits,
|
||||
Query: result.Query,
|
||||
ProcessingTimeMs: result.ProcessingTimeMs,
|
||||
TotalHits: result.TotalHits,
|
||||
Limit: result.Limit,
|
||||
Offset: result.Offset,
|
||||
}, nil
|
||||
}
|
||||
199
cloud/maplepress-backend/internal/usecase/page/status.go
Normal file
199
cloud/maplepress-backend/internal/usecase/page/status.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
)
|
||||
|
||||
// GetSyncStatusUseCase handles retrieving synchronization status
|
||||
type GetSyncStatusUseCase struct {
|
||||
pageRepo domainpage.Repository
|
||||
siteRepo domainsite.Repository
|
||||
searchClient *search.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideGetSyncStatusUseCase creates a new GetSyncStatusUseCase
|
||||
func ProvideGetSyncStatusUseCase(
|
||||
pageRepo domainpage.Repository,
|
||||
siteRepo domainsite.Repository,
|
||||
searchClient *search.Client,
|
||||
logger *zap.Logger,
|
||||
) *GetSyncStatusUseCase {
|
||||
return &GetSyncStatusUseCase{
|
||||
pageRepo: pageRepo,
|
||||
siteRepo: siteRepo,
|
||||
searchClient: searchClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SyncStatusOutput provides synchronization status information
|
||||
type SyncStatusOutput struct {
|
||||
SiteID string `json:"site_id"`
|
||||
TotalPages int64 `json:"total_pages"`
|
||||
PublishedPages int64 `json:"published_pages"`
|
||||
DraftPages int64 `json:"draft_pages"`
|
||||
LastSyncedAt time.Time `json:"last_synced_at"`
|
||||
PagesIndexedMonth int64 `json:"pages_indexed_month"` // Usage tracking
|
||||
SearchRequestsMonth int64 `json:"search_requests_month"` // Usage tracking
|
||||
LastResetAt time.Time `json:"last_reset_at"` // Monthly billing cycle
|
||||
SearchIndexStatus string `json:"search_index_status"`
|
||||
SearchIndexDocCount int64 `json:"search_index_doc_count"`
|
||||
}
|
||||
|
||||
// Execute retrieves the current sync status for a site
|
||||
func (uc *GetSyncStatusUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID) (*SyncStatusOutput, error) {
|
||||
uc.logger.Info("executing get sync status use case",
|
||||
zap.String("tenant_id", tenantID.String()),
|
||||
zap.String("site_id", siteID.String()))
|
||||
|
||||
// Get site to validate and get quota information
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
// Count total pages in database
|
||||
totalPages, err := uc.pageRepo.CountBySiteID(ctx, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to count pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to count pages: %w", err)
|
||||
}
|
||||
|
||||
// Get all pages to count by status (this could be optimized with a dedicated query)
|
||||
pages, err := uc.pageRepo.GetBySiteID(ctx, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get pages", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to get pages: %w", err)
|
||||
}
|
||||
|
||||
// Count pages by status
|
||||
var publishedPages, draftPages int64
|
||||
for _, page := range pages {
|
||||
if page.Status == "publish" {
|
||||
publishedPages++
|
||||
} else if page.Status == "draft" {
|
||||
draftPages++
|
||||
}
|
||||
}
|
||||
|
||||
// Check search index status
|
||||
indexStatus := "not_created"
|
||||
var indexDocCount int64 = 0
|
||||
|
||||
indexExists, err := uc.searchClient.IndexExists(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to check index existence", zap.Error(err))
|
||||
indexStatus = "error"
|
||||
} else if indexExists {
|
||||
indexStatus = "active"
|
||||
|
||||
// Get index stats
|
||||
stats, err := uc.searchClient.GetStats(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get index stats", zap.Error(err))
|
||||
} else {
|
||||
indexDocCount = stats.NumberOfDocuments
|
||||
}
|
||||
}
|
||||
|
||||
uc.logger.Info("sync status retrieved successfully",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int64("total_pages", totalPages),
|
||||
zap.Int64("published", publishedPages),
|
||||
zap.Int64("draft", draftPages))
|
||||
|
||||
return &SyncStatusOutput{
|
||||
SiteID: siteID.String(),
|
||||
TotalPages: totalPages,
|
||||
PublishedPages: publishedPages,
|
||||
DraftPages: draftPages,
|
||||
LastSyncedAt: site.LastIndexedAt,
|
||||
PagesIndexedMonth: site.MonthlyPagesIndexed,
|
||||
SearchRequestsMonth: site.SearchRequestsCount,
|
||||
LastResetAt: site.LastResetAt,
|
||||
SearchIndexStatus: indexStatus,
|
||||
SearchIndexDocCount: indexDocCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPageDetailsInput is the input for getting page details
|
||||
type GetPageDetailsInput struct {
|
||||
PageID string `json:"page_id"`
|
||||
}
|
||||
|
||||
// PageDetailsOutput provides detailed information about a specific page
|
||||
type PageDetailsOutput struct {
|
||||
PageID string `json:"page_id"`
|
||||
Title string `json:"title"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
PostType string `json:"post_type"`
|
||||
Author string `json:"author"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
IndexedAt time.Time `json:"indexed_at"`
|
||||
MeilisearchDocID string `json:"meilisearch_doc_id"`
|
||||
IsIndexed bool `json:"is_indexed"`
|
||||
}
|
||||
|
||||
// ExecuteGetPageDetails retrieves detailed information about a specific page
|
||||
func (uc *GetSyncStatusUseCase) ExecuteGetPageDetails(ctx context.Context, tenantID, siteID gocql.UUID, input *GetPageDetailsInput) (*PageDetailsOutput, error) {
|
||||
uc.logger.Info("executing get page details use case",
|
||||
zap.String("tenant_id", tenantID.String()),
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.String("page_id", input.PageID))
|
||||
|
||||
// Get site to validate
|
||||
_, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Get page from database
|
||||
page, err := uc.pageRepo.GetByID(ctx, siteID, input.PageID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get page", zap.Error(err))
|
||||
return nil, fmt.Errorf("page not found")
|
||||
}
|
||||
|
||||
// Check if page is indexed in Meilisearch
|
||||
isIndexed := !page.IndexedAt.IsZero()
|
||||
|
||||
uc.logger.Info("page details retrieved successfully",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.String("page_id", input.PageID))
|
||||
|
||||
return &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
|
||||
}
|
||||
205
cloud/maplepress-backend/internal/usecase/page/sync.go
Normal file
205
cloud/maplepress-backend/internal/usecase/page/sync.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcrypt"
|
||||
)
|
||||
|
||||
// SyncPagesUseCase handles page synchronization from WordPress
|
||||
type SyncPagesUseCase struct {
|
||||
pageRepo domainpage.Repository
|
||||
siteRepo domainsite.Repository
|
||||
searchClient *search.Client
|
||||
ipEncryptor *ipcrypt.IPEncryptor
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideSyncPagesUseCase creates a new SyncPagesUseCase
|
||||
func ProvideSyncPagesUseCase(
|
||||
pageRepo domainpage.Repository,
|
||||
siteRepo domainsite.Repository,
|
||||
searchClient *search.Client,
|
||||
ipEncryptor *ipcrypt.IPEncryptor,
|
||||
logger *zap.Logger,
|
||||
) *SyncPagesUseCase {
|
||||
return &SyncPagesUseCase{
|
||||
pageRepo: pageRepo,
|
||||
siteRepo: siteRepo,
|
||||
searchClient: searchClient,
|
||||
ipEncryptor: ipEncryptor,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SyncPageInput represents a single page to sync
|
||||
type SyncPageInput struct {
|
||||
PageID string `json:"page_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"` // publish, draft, trash
|
||||
PostType string `json:"post_type"` // page, post
|
||||
Author string `json:"author"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
IPAddress string `json:"-"` // Plain IP address (will be encrypted before storage), never exposed in JSON
|
||||
}
|
||||
|
||||
// SyncPagesInput is the input for syncing pages
|
||||
type SyncPagesInput struct {
|
||||
Pages []SyncPageInput `json:"pages"`
|
||||
}
|
||||
|
||||
// SyncPagesOutput is the output after syncing pages
|
||||
type SyncPagesOutput struct {
|
||||
SyncedCount int `json:"synced_count"`
|
||||
IndexedCount int `json:"indexed_count"`
|
||||
FailedPages []string `json:"failed_pages,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SyncPages syncs a batch of pages for a site
|
||||
func (uc *SyncPagesUseCase) SyncPages(ctx context.Context, tenantID, siteID gocql.UUID, input *SyncPagesInput) (*SyncPagesOutput, error) {
|
||||
uc.logger.Info("syncing pages",
|
||||
zap.String("tenant_id", tenantID.String()),
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int("page_count", len(input.Pages)))
|
||||
|
||||
// Get site to validate and check quotas
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
// No quota limits - usage-based billing (anti-abuse via rate limiting only)
|
||||
|
||||
// Ensure search index exists
|
||||
indexExists, err := uc.searchClient.IndexExists(siteID.String())
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to check index existence", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to check search index: %w", err)
|
||||
}
|
||||
|
||||
if !indexExists {
|
||||
uc.logger.Info("creating search index", zap.String("site_id", siteID.String()))
|
||||
if err := uc.searchClient.CreateIndex(siteID.String()); err != nil {
|
||||
uc.logger.Error("failed to create index", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to create search index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process each page
|
||||
syncedCount := 0
|
||||
indexedCount := 0
|
||||
var failedPages []string
|
||||
var documentsToIndex []search.PageDocument
|
||||
|
||||
for _, pageInput := range input.Pages {
|
||||
// Encrypt IP address (CWE-359: GDPR compliance)
|
||||
encryptedIP, err := uc.ipEncryptor.Encrypt(pageInput.IPAddress)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to encrypt IP address",
|
||||
zap.String("page_id", pageInput.PageID),
|
||||
zap.Error(err))
|
||||
failedPages = append(failedPages, pageInput.PageID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create page entity
|
||||
page := domainpage.NewPage(
|
||||
siteID,
|
||||
site.TenantID,
|
||||
pageInput.PageID,
|
||||
pageInput.Title,
|
||||
pageInput.Content,
|
||||
pageInput.Excerpt,
|
||||
pageInput.URL,
|
||||
pageInput.Status,
|
||||
pageInput.PostType,
|
||||
pageInput.Author,
|
||||
pageInput.PublishedAt,
|
||||
pageInput.ModifiedAt,
|
||||
encryptedIP,
|
||||
)
|
||||
|
||||
// Upsert page to database
|
||||
if err := uc.pageRepo.Upsert(ctx, page); err != nil {
|
||||
uc.logger.Error("failed to upsert page",
|
||||
zap.String("page_id", pageInput.PageID),
|
||||
zap.Error(err))
|
||||
failedPages = append(failedPages, pageInput.PageID)
|
||||
continue
|
||||
}
|
||||
|
||||
syncedCount++
|
||||
|
||||
// Only index published pages
|
||||
if page.ShouldIndex() {
|
||||
page.MarkIndexed()
|
||||
|
||||
// Prepare document for Meilisearch
|
||||
doc := search.PageDocument{
|
||||
ID: page.PageID,
|
||||
SiteID: page.SiteID.String(),
|
||||
TenantID: page.TenantID.String(),
|
||||
Title: page.Title,
|
||||
Content: page.Content,
|
||||
Excerpt: page.Excerpt,
|
||||
URL: page.URL,
|
||||
Status: page.Status,
|
||||
PostType: page.PostType,
|
||||
Author: page.Author,
|
||||
PublishedAt: page.PublishedAt.Unix(),
|
||||
ModifiedAt: page.ModifiedAt.Unix(),
|
||||
}
|
||||
|
||||
documentsToIndex = append(documentsToIndex, doc)
|
||||
}
|
||||
}
|
||||
|
||||
// Index documents in Meilisearch if any
|
||||
if len(documentsToIndex) > 0 {
|
||||
_, err := uc.searchClient.AddDocuments(siteID.String(), documentsToIndex)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to index documents", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to index documents: %w", err)
|
||||
}
|
||||
indexedCount = len(documentsToIndex)
|
||||
// Note: Usage tracking is handled by the service layer via UpdateSiteUsageUseCase
|
||||
}
|
||||
|
||||
uc.logger.Info("pages synced successfully",
|
||||
zap.String("site_id", siteID.String()),
|
||||
zap.Int("synced", syncedCount),
|
||||
zap.Int("indexed", indexedCount),
|
||||
zap.Int("failed", len(failedPages)))
|
||||
|
||||
message := fmt.Sprintf("Successfully synced %d pages, indexed %d pages", syncedCount, indexedCount)
|
||||
if len(failedPages) > 0 {
|
||||
message += fmt.Sprintf(", failed %d pages", len(failedPages))
|
||||
}
|
||||
|
||||
return &SyncPagesOutput{
|
||||
SyncedCount: syncedCount,
|
||||
IndexedCount: indexedCount,
|
||||
FailedPages: failedPages,
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
)
|
||||
|
||||
// UpdateSiteUsageUseCase updates site usage counters after indexing
|
||||
type UpdateSiteUsageUseCase struct {
|
||||
siteRepo domainsite.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideUpdateSiteUsageUseCase creates a new UpdateSiteUsageUseCase
|
||||
func ProvideUpdateSiteUsageUseCase(
|
||||
siteRepo domainsite.Repository,
|
||||
logger *zap.Logger,
|
||||
) *UpdateSiteUsageUseCase {
|
||||
return &UpdateSiteUsageUseCase{
|
||||
siteRepo: siteRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute updates the site's monthly page indexed count (for billing tracking)
|
||||
func (uc *UpdateSiteUsageUseCase) Execute(
|
||||
ctx context.Context,
|
||||
site *domainsite.Site,
|
||||
indexedCount int,
|
||||
) error {
|
||||
if indexedCount <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
site.IncrementMonthlyPageCount(int64(indexedCount))
|
||||
|
||||
if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil {
|
||||
uc.logger.Error("failed to update usage", zap.Error(err))
|
||||
// Don't fail the whole operation, just log the error
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
|
||||
)
|
||||
|
||||
// UpsertPageUseCase saves or updates a page in the repository
|
||||
type UpsertPageUseCase struct {
|
||||
pageRepo domainpage.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideUpsertPageUseCase creates a new UpsertPageUseCase
|
||||
func ProvideUpsertPageUseCase(
|
||||
pageRepo domainpage.Repository,
|
||||
logger *zap.Logger,
|
||||
) *UpsertPageUseCase {
|
||||
return &UpsertPageUseCase{
|
||||
pageRepo: pageRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute saves or updates a page in the database
|
||||
func (uc *UpsertPageUseCase) Execute(ctx context.Context, page *domainpage.Page) error {
|
||||
if err := uc.pageRepo.Upsert(ctx, page); err != nil {
|
||||
uc.logger.Error("failed to upsert page",
|
||||
zap.String("page_id", page.PageID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
)
|
||||
|
||||
// ValidateSiteUseCase validates site status and verification
|
||||
type ValidateSiteUseCase struct {
|
||||
siteRepo domainsite.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideValidateSiteUseCase creates a new ValidateSiteUseCase
|
||||
func ProvideValidateSiteUseCase(
|
||||
siteRepo domainsite.Repository,
|
||||
logger *zap.Logger,
|
||||
) *ValidateSiteUseCase {
|
||||
return &ValidateSiteUseCase{
|
||||
siteRepo: siteRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute validates the site and returns it if valid
|
||||
func (uc *ValidateSiteUseCase) Execute(
|
||||
ctx context.Context,
|
||||
tenantID, siteID gocql.UUID,
|
||||
) (*domainsite.Site, error) {
|
||||
// Get site from repository
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
)
|
||||
|
||||
// ValidateSiteForDeletionUseCase validates that a site exists and is authorized for deletion
|
||||
type ValidateSiteForDeletionUseCase struct {
|
||||
siteRepo domainsite.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideValidateSiteForDeletionUseCase creates a new ValidateSiteForDeletionUseCase
|
||||
func ProvideValidateSiteForDeletionUseCase(
|
||||
siteRepo domainsite.Repository,
|
||||
logger *zap.Logger,
|
||||
) *ValidateSiteForDeletionUseCase {
|
||||
return &ValidateSiteForDeletionUseCase{
|
||||
siteRepo: siteRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute validates the site for deletion operations
|
||||
func (uc *ValidateSiteForDeletionUseCase) Execute(
|
||||
ctx context.Context,
|
||||
tenantID, siteID gocql.UUID,
|
||||
) (*domainsite.Site, error) {
|
||||
// Get site from repository
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
)
|
||||
|
||||
// ValidateSiteForSearchUseCase validates that a site exists and is authorized for search
|
||||
type ValidateSiteForSearchUseCase struct {
|
||||
siteRepo domainsite.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideValidateSiteForSearchUseCase creates a new ValidateSiteForSearchUseCase
|
||||
func ProvideValidateSiteForSearchUseCase(
|
||||
siteRepo domainsite.Repository,
|
||||
logger *zap.Logger,
|
||||
) *ValidateSiteForSearchUseCase {
|
||||
return &ValidateSiteForSearchUseCase{
|
||||
siteRepo: siteRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute validates the site for search operations
|
||||
func (uc *ValidateSiteForSearchUseCase) Execute(
|
||||
ctx context.Context,
|
||||
tenantID, siteID gocql.UUID,
|
||||
) (*domainsite.Site, error) {
|
||||
// Get site from repository
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
)
|
||||
|
||||
// ValidateSiteForStatusUseCase validates that a site exists and is authorized for status queries
|
||||
type ValidateSiteForStatusUseCase struct {
|
||||
siteRepo domainsite.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideValidateSiteForStatusUseCase creates a new ValidateSiteForStatusUseCase
|
||||
func ProvideValidateSiteForStatusUseCase(
|
||||
siteRepo domainsite.Repository,
|
||||
logger *zap.Logger,
|
||||
) *ValidateSiteForStatusUseCase {
|
||||
return &ValidateSiteForStatusUseCase{
|
||||
siteRepo: siteRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute validates the site for status operations
|
||||
func (uc *ValidateSiteForStatusUseCase) Execute(
|
||||
ctx context.Context,
|
||||
tenantID, siteID gocql.UUID,
|
||||
) (*domainsite.Site, error) {
|
||||
// Get site from repository
|
||||
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
|
||||
if err != nil {
|
||||
uc.logger.Error("failed to get site", zap.Error(err))
|
||||
return nil, domainsite.ErrSiteNotFound
|
||||
}
|
||||
|
||||
// Verify site is verified (skip for test mode)
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
|
||||
return nil, domainsite.ErrSiteNotVerified
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue