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
148
cloud/maplepress-backend/internal/service/page/delete.go
Normal file
148
cloud/maplepress-backend/internal/service/page/delete.go
Normal 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
|
||||
}
|
||||
80
cloud/maplepress-backend/internal/service/page/search.go
Normal file
80
cloud/maplepress-backend/internal/service/page/search.go
Normal 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
|
||||
}
|
||||
133
cloud/maplepress-backend/internal/service/page/status.go
Normal file
133
cloud/maplepress-backend/internal/service/page/status.go
Normal 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
|
||||
}
|
||||
143
cloud/maplepress-backend/internal/service/page/sync.go
Normal file
143
cloud/maplepress-backend/internal/service/page/sync.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue