From 9dacfd036d2a7095d9f77b9fb6f8c32067ffa6c2 Mon Sep 17 00:00:00 2001 From: AzenKain Date: Sun, 24 May 2026 12:58:30 +0700 Subject: [PATCH] feat: implement submission lifecycle management with creation, review, and status tracking services --- db/query/submission.sql | 11 + internal/gen/sqlc/submission.sql.go | 50 + internal/repositories/submissionRepository.go | 26 + internal/services/submissionService.go | 1228 ++++++++++------- 4 files changed, 780 insertions(+), 535 deletions(-) diff --git a/db/query/submission.sql b/db/query/submission.sql index fc40d7c..0b8cc64 100644 --- a/db/query/submission.sql +++ b/db/query/submission.sql @@ -205,3 +205,14 @@ LEFT JOIN users ru ON s.reviewed_by = ru.id LEFT JOIN user_profiles rup ON ru.id = rup.user_id WHERE s.id = ANY($1::uuid[]) AND s.is_deleted = false; +-- name: GetLatestApprovedSubmissionExcluding :one +SELECT + id, project_id, commit_id, user_id, created_at, status, reviewed_by, reviewed_at, review_note, content, is_deleted +FROM submissions +WHERE project_id = $1 + AND status = 2 + AND id != $2 + AND is_deleted = false +ORDER BY reviewed_at DESC, created_at DESC +LIMIT 1; + diff --git a/internal/gen/sqlc/submission.sql.go b/internal/gen/sqlc/submission.sql.go index 2307b82..f84aa6b 100644 --- a/internal/gen/sqlc/submission.sql.go +++ b/internal/gen/sqlc/submission.sql.go @@ -159,6 +159,56 @@ func (q *Queries) DeleteSubmission(ctx context.Context, id pgtype.UUID) error { return err } +const getLatestApprovedSubmissionExcluding = `-- name: GetLatestApprovedSubmissionExcluding :one +SELECT + id, project_id, commit_id, user_id, created_at, status, reviewed_by, reviewed_at, review_note, content, is_deleted +FROM submissions +WHERE project_id = $1 + AND status = 2 + AND id != $2 + AND is_deleted = false +ORDER BY reviewed_at DESC, created_at DESC +LIMIT 1 +` + +type GetLatestApprovedSubmissionExcludingParams struct { + ProjectID pgtype.UUID `json:"project_id"` + ID pgtype.UUID `json:"id"` +} + +type GetLatestApprovedSubmissionExcludingRow struct { + ID pgtype.UUID `json:"id"` + ProjectID pgtype.UUID `json:"project_id"` + CommitID pgtype.UUID `json:"commit_id"` + UserID pgtype.UUID `json:"user_id"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + Status int16 `json:"status"` + ReviewedBy pgtype.UUID `json:"reviewed_by"` + ReviewedAt pgtype.Timestamptz `json:"reviewed_at"` + ReviewNote pgtype.Text `json:"review_note"` + Content pgtype.Text `json:"content"` + IsDeleted bool `json:"is_deleted"` +} + +func (q *Queries) GetLatestApprovedSubmissionExcluding(ctx context.Context, arg GetLatestApprovedSubmissionExcludingParams) (GetLatestApprovedSubmissionExcludingRow, error) { + row := q.db.QueryRow(ctx, getLatestApprovedSubmissionExcluding, arg.ProjectID, arg.ID) + var i GetLatestApprovedSubmissionExcludingRow + err := row.Scan( + &i.ID, + &i.ProjectID, + &i.CommitID, + &i.UserID, + &i.CreatedAt, + &i.Status, + &i.ReviewedBy, + &i.ReviewedAt, + &i.ReviewNote, + &i.Content, + &i.IsDeleted, + ) + return i, err +} + const getSubmissionById = `-- name: GetSubmissionById :one SELECT s.id, s.project_id, s.commit_id, s.user_id, s.created_at, s.status, s.reviewed_by, s.reviewed_at, s.review_note, s.content, s.is_deleted, diff --git a/internal/repositories/submissionRepository.go b/internal/repositories/submissionRepository.go index e7e2236..66f8060 100644 --- a/internal/repositories/submissionRepository.go +++ b/internal/repositories/submissionRepository.go @@ -23,6 +23,7 @@ type SubmissionRepository interface { Create(ctx context.Context, params sqlc.CreateSubmissionParams) (*models.SubmissionEntity, error) Update(ctx context.Context, params sqlc.UpdateSubmissionParams) (*models.SubmissionEntity, error) Delete(ctx context.Context, id pgtype.UUID) error + GetLatestApprovedSubmissionExcluding(ctx context.Context, projectID pgtype.UUID, id pgtype.UUID) (*models.SubmissionEntity, error) WithTx(tx pgx.Tx) SubmissionRepository } @@ -323,3 +324,28 @@ func (r *submissionRepository) Delete(ctx context.Context, id pgtype.UUID) error }() return nil } + +func (r *submissionRepository) GetLatestApprovedSubmissionExcluding(ctx context.Context, projectID pgtype.UUID, id pgtype.UUID) (*models.SubmissionEntity, error) { + row, err := r.q.GetLatestApprovedSubmissionExcluding(ctx, sqlc.GetLatestApprovedSubmissionExcludingParams{ + ProjectID: projectID, + ID: id, + }) + if err != nil { + return nil, err + } + + entity := &models.SubmissionEntity{ + ID: convert.UUIDToString(row.ID), + ProjectID: convert.UUIDToString(row.ProjectID), + CommitID: convert.UUIDToString(row.CommitID), + UserID: convert.UUIDToString(row.UserID), + CreatedAt: convert.TimeToPtr(row.CreatedAt), + Status: constants.ParseStatusType(row.Status), + ReviewedBy: convert.UUIDToStringPtr(row.ReviewedBy), + ReviewedAt: convert.TimeToPtr(row.ReviewedAt), + ReviewNote: convert.TextToPtr(row.ReviewNote), + Content: convert.TextToPtr(row.Content), + IsDeleted: row.IsDeleted, + } + return entity, nil +} diff --git a/internal/services/submissionService.go b/internal/services/submissionService.go index a3ab209..9e2a3eb 100644 --- a/internal/services/submissionService.go +++ b/internal/services/submissionService.go @@ -2,6 +2,7 @@ package services import ( "context" + "errors" "encoding/json" "fmt" "history-api/internal/dtos/request" @@ -17,6 +18,7 @@ import ( "strconv" "github.com/gofiber/fiber/v3" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "github.com/rs/zerolog/log" @@ -188,11 +190,6 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer defer tx.Rollback(ctx) submissionRepo := s.submissionRepo.WithTx(tx) - commitRepo := s.commitRepo.WithTx(tx) - entityRepo := s.entityRepo.WithTx(tx) - geometryRepo := s.geometryRepo.WithTx(tx) - wikiRepo := s.wikiRepo.WithTx(tx) - battleReplayRepo := s.battleReplayRepo.WithTx(tx) submissionUUID, err := convert.StringToUUID(submissionID) if err != nil { @@ -232,10 +229,6 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer return nil, fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to project") } - listDeleteEntities := make([]pgtype.UUID, 0) - listDeleteWikis := make([]pgtype.UUID, 0) - listDeleteGeometries := make([]pgtype.UUID, 0) - listDeleteBattleReplays := make([]pgtype.UUID, 0) var snapshotData request.CommitSnapshot err = json.Unmarshal(commit.SnapshotJson, &snapshotData) if err != nil { @@ -247,532 +240,9 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID") } - currentEntity, err := s.entityRepo.GetByProjectID(ctx, projectUUID) - if err != nil { - return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found: "+err.Error()) - } - currentGeometry, err := s.geometryRepo.GetByProjectID(ctx, projectUUID) - if err != nil { - return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found: "+err.Error()) - } - - currentWiki, err := s.wikiRepo.GetByProjectID(ctx, projectUUID) - if err != nil { - return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found: "+err.Error()) - } - - currentBattleReplay, err := s.battleReplayRepo.GetByProjectID(ctx, projectUUID) - if err != nil { - return nil, fiber.NewError(fiber.StatusNotFound, "Battle replay not found: "+err.Error()) - } - - persistItemIDs := make(map[string]struct{}) - for _, item := range snapshotData.Entities { - persistItemIDs[item.ID] = struct{}{} - } - for _, item := range snapshotData.Geometries { - persistItemIDs[item.ID] = struct{}{} - } - for _, item := range snapshotData.Wikis { - persistItemIDs[item.ID] = struct{}{} - } - for _, item := range snapshotData.Replays { - persistItemIDs[item.ID] = struct{}{} - } - - persistCurrentItemIDs := make(map[string]struct{}) - for _, item := range currentEntity { - persistCurrentItemIDs[item.ID] = struct{}{} - } - for _, item := range currentGeometry { - persistCurrentItemIDs[item.ID] = struct{}{} - } - for _, item := range currentWiki { - persistCurrentItemIDs[item.ID] = struct{}{} - } - for _, item := range currentBattleReplay { - persistCurrentItemIDs[item.ID] = struct{}{} - } - - for _, e := range currentEntity { - if _, ok := persistItemIDs[e.ID]; !ok { - itemUUID, err := convert.StringToUUID(e.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID") - } - listDeleteEntities = append(listDeleteEntities, itemUUID) - delete(persistCurrentItemIDs, e.ID) - } - } - - for _, g := range currentGeometry { - if _, ok := persistItemIDs[g.ID]; !ok { - itemUUID, err := convert.StringToUUID(g.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID") - } - listDeleteGeometries = append(listDeleteGeometries, itemUUID) - delete(persistCurrentItemIDs, g.ID) - } - } - - for _, w := range currentWiki { - if _, ok := persistItemIDs[w.ID]; !ok { - itemUUID, err := convert.StringToUUID(w.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID") - } - listDeleteWikis = append(listDeleteWikis, itemUUID) - delete(persistCurrentItemIDs, w.ID) - } - } - - for _, br := range currentBattleReplay { - if _, ok := persistItemIDs[br.ID]; !ok { - itemUUID, err := convert.StringToUUID(br.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID") - } - listDeleteBattleReplays = append(listDeleteBattleReplays, itemUUID) - delete(persistCurrentItemIDs, br.ID) - } - } - - if len(listDeleteEntities) > 0 { - if err = entityRepo.DeleteByIDs(ctx, listDeleteEntities); err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete entities") - } - } - - if len(listDeleteGeometries) > 0 { - if err = geometryRepo.DeleteByIDs(ctx, listDeleteGeometries); err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometries") - } - } - - if len(listDeleteWikis) > 0 { - if err = wikiRepo.DeleteByIDs(ctx, listDeleteWikis); err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wikis") - } - } - - if len(listDeleteBattleReplays) > 0 { - if err = battleReplayRepo.DeleteByIDs(ctx, listDeleteBattleReplays); err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete battle replays") - } - } - - refEntityIDs := []string{} - for _, e := range snapshotData.Entities { - if e.Source == "ref" { - refEntityIDs = append(refEntityIDs, e.ID) - } - } - - refEntities, _ := s.entityRepo.GetByIDs(ctx, refEntityIDs) - refEntityMap := make(map[string]bool) - for _, e := range refEntities { - refEntityMap[e.ID] = true - } - - newEntities := make([]*request.EntitySnapshot, 0, len(snapshotData.Entities)) - for i, entity := range snapshotData.Entities { - if entity.Operation == "delete" { - continue - } - - entityUUID, err := convert.StringToUUID(entity.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID") - } - - if _, ok := persistCurrentItemIDs[entity.ID]; ok { - _, err := entityRepo.Update(ctx, sqlc.UpdateEntityParams{ - Name: convert.StringToText(entity.Name), - Description: convert.StringToText(entity.Description), - Slug: convert.PtrToText(entity.Slug), - Status: convert.PtrToInt2(entity.Status), - TimeStart: convert.PtrFloat64ToInt4(entity.TimeStart), - TimeEnd: convert.PtrFloat64ToInt4(entity.TimeEnd), - ID: entityUUID, - }) - - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update entity: "+err.Error()) - } - - newEntities = append(newEntities, snapshotData.Entities[i]) - - } else if entity.Source == "inline" { - _, err := entityRepo.Create(ctx, sqlc.CreateEntityParams{ - ID: entityUUID, - Name: entity.Name, - Description: convert.StringToText(entity.Description), - ProjectID: projectUUID, - Slug: convert.PtrToText(entity.Slug), - Status: convert.PtrToInt2(entity.Status), - TimeStart: convert.PtrFloat64ToInt4(entity.TimeStart), - TimeEnd: convert.PtrFloat64ToInt4(entity.TimeEnd), - }) - - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create entity: "+err.Error()) - } - - newEntities = append(newEntities, snapshotData.Entities[i]) - - } else if entity.Source == "ref" { - if !refEntityMap[entity.ID] { - continue - } - newEntities = append(newEntities, snapshotData.Entities[i]) - } - } - snapshotData.Entities = newEntities - - refGeometryIDs := []string{} - for _, g := range snapshotData.Geometries { - if g.Source == "ref" { - refGeometryIDs = append(refGeometryIDs, g.ID) - } - } - refGeometries, _ := s.geometryRepo.GetByIDs(ctx, refGeometryIDs) - refGeometryMap := make(map[string]bool) - for _, g := range refGeometries { - refGeometryMap[g.ID] = true - } - - newGeometries := make([]*request.GeometrySnapshot, 0, len(snapshotData.Geometries)) - for i, geo := range snapshotData.Geometries { - if geo.Operation == "delete" { - continue - } - - geometryUUID, err := convert.StringToUUID(geo.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID") - } - - binding, _ := json.Marshal(geo.Binding) - geoTypeCode := int16(0) - if geo.Type != "" { - if n, err := strconv.ParseInt(geo.Type, 10, 16); err == nil { - geoTypeCode = int16(n) - } - } - - if _, ok := persistCurrentItemIDs[geo.ID]; ok { - params := sqlc.UpdateGeometryParams{ - ID: geometryUUID, - GeoType: pgtype.Int2{Int16: geoTypeCode, Valid: true}, - DrawGeometry: geo.DrawGeometry, - Binding: binding, - TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart), - TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd), - ProjectID: projectUUID, - } - - if geo.BBox != nil { - params.UpdateBbox = pgtype.Bool{Bool: true, Valid: true} - params.MinLng = pgtype.Float8{Float64: geo.BBox.MinLng, Valid: true} - params.MinLat = pgtype.Float8{Float64: geo.BBox.MinLat, Valid: true} - params.MaxLng = pgtype.Float8{Float64: geo.BBox.MaxLng, Valid: true} - params.MaxLat = pgtype.Float8{Float64: geo.BBox.MaxLat, Valid: true} - } - - _, err := geometryRepo.Update(ctx, params) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update geometry: "+err.Error()) - } - newGeometries = append(newGeometries, snapshotData.Geometries[i]) - - } else if geo.Source == "inline" { - params := sqlc.CreateGeometryParams{ - ID: geometryUUID, - GeoType: geoTypeCode, - DrawGeometry: geo.DrawGeometry, - Binding: binding, - TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart), - TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd), - ProjectID: projectUUID, - } - if geo.BBox != nil { - params.MinLng = geo.BBox.MinLng - params.MinLat = geo.BBox.MinLat - params.MaxLng = geo.BBox.MaxLng - params.MaxLat = geo.BBox.MaxLat - } - - _, err := geometryRepo.Create(ctx, params) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry: "+err.Error()) - } - newGeometries = append(newGeometries, snapshotData.Geometries[i]) - - } else if geo.Source == "ref" { - if !refGeometryMap[geo.ID] { - continue - } - newGeometries = append(newGeometries, snapshotData.Geometries[i]) - } - } - snapshotData.Geometries = newGeometries - - refWikiIDs := []string{} - for _, w := range snapshotData.Wikis { - if w.Source == "ref" { - refWikiIDs = append(refWikiIDs, w.ID) - } - } - refWikis, _ := s.wikiRepo.GetByIDs(ctx, refWikiIDs) - refWikiMap := make(map[string]bool) - for _, w := range refWikis { - refWikiMap[w.ID] = true - } - - newWikis := make([]*request.WikiSnapshot, 0, len(snapshotData.Wikis)) - for i, wiki := range snapshotData.Wikis { - if wiki.Operation == "delete" { - continue - } - - wikiUUID, err := convert.StringToUUID(wiki.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID") - } - - if _, ok := persistCurrentItemIDs[wiki.ID]; ok { - _, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{ - ID: wikiUUID, - Title: convert.StringToText(wiki.Title), - Slug: convert.PtrToText(wiki.Slug), - ProjectID: projectUUID, - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+err.Error()) - } - - count, err := s.wikiRepo.GetContentCountByWikiID(ctx, wikiUUID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get wiki content count: "+err.Error()) - } - versionTitle := fmt.Sprintf("Version %d", count+1) - - _, err = wikiRepo.CreateContent(ctx, sqlc.CreateWikiContentParams{ - WikiID: wikiUUID, - Title: versionTitle, - Content: convert.StringToText(wiki.Doc), - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki content: "+err.Error()) - } - - _ = s.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wikiUUID.String()), fmt.Sprintf("wiki:slug:%s", *wiki.Slug)) - - newWikis = append(newWikis, snapshotData.Wikis[i]) - - } else if wiki.Source == "inline" { - _, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{ - ID: wikiUUID, - Title: convert.StringToText(wiki.Title), - Slug: convert.PtrToText(wiki.Slug), - ProjectID: projectUUID, - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+err.Error()) - } - - _, err = wikiRepo.CreateContent(ctx, sqlc.CreateWikiContentParams{ - WikiID: wikiUUID, - Title: "Version 1", - Content: convert.StringToText(wiki.Doc), - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki content: "+err.Error()) - } - - _ = s.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wikiUUID.String()), fmt.Sprintf("wiki:slug:%s", *wiki.Slug)) - - newWikis = append(newWikis, snapshotData.Wikis[i]) - - } else if wiki.Source == "ref" { - if !refWikiMap[wiki.ID] { - continue - } - newWikis = append(newWikis, snapshotData.Wikis[i]) - } - } - snapshotData.Wikis = newWikis - - for _, replay := range snapshotData.Replays { - replayUUID, err := convert.StringToUUID(replay.ID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID") - } - - geomUUID, err := convert.StringToUUID(replay.GeometryID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID in battle replay") - } - - targetIDs, err := json.Marshal(replay.TargetGeometryIDs) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal target geometry IDs") - } - - if _, ok := persistCurrentItemIDs[replay.ID]; ok { - _, err := battleReplayRepo.Update(ctx, sqlc.UpdateBattleReplayParams{ - ID: replayUUID, - GeometryID: geomUUID, - TargetGeometryIds: targetIDs, - Detail: replay.Detail, - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update battle replay: "+err.Error()) - } - } else { - _, err := battleReplayRepo.Create(ctx, sqlc.CreateBattleReplayParams{ - ID: replayUUID, - GeometryID: geomUUID, - ProjectID: projectUUID, - TargetGeometryIds: targetIDs, - Detail: replay.Detail, - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create battle replay: "+err.Error()) - } - } - } - - err = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometry entity: "+err.Error()) - } - err = wikiRepo.DeleteEntityWikisByProjectID(ctx, projectUUID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wiki entity: "+err.Error()) - } - - validEntities := make(map[string]bool) - for _, e := range snapshotData.Entities { - validEntities[e.ID] = true - } - validGeometries := make(map[string]bool) - for _, g := range snapshotData.Geometries { - validGeometries[g.ID] = true - } - validWikis := make(map[string]bool) - for _, w := range snapshotData.Wikis { - validWikis[w.ID] = true - } - - if len(snapshotData.GeometryEntity) > 0 { - geomLinks := make(map[string][]pgtype.UUID) - for _, link := range snapshotData.GeometryEntity { - if !validEntities[link.EntityID] || !validGeometries[link.GeometryID] { - continue - } - gID, _ := convert.StringToUUID(link.GeometryID) - geomLinks[link.EntityID] = append(geomLinks[link.EntityID], gID) - } - - for eIDStr, gIDs := range geomLinks { - eID, _ := convert.StringToUUID(eIDStr) - err = geometryRepo.CreateEntityGeometries(ctx, sqlc.CreateEntityGeometriesParams{ - EntityID: eID, - GeometryIds: gIDs, - ProjectID: projectUUID, - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry entity: "+err.Error()) - } - } - } - - if len(snapshotData.EntityWiki) > 0 { - wikiLinks := make(map[string][]pgtype.UUID) - for _, link := range snapshotData.EntityWiki { - if link.Operation == "delete" || (link.IsDeleted != nil && *link.IsDeleted == 1) { - continue - } - if !validEntities[link.EntityID] || !validWikis[link.WikiID] { - continue - } - wID, _ := convert.StringToUUID(link.WikiID) - wikiLinks[link.EntityID] = append(wikiLinks[link.EntityID], wID) - } - - for eIDStr, wIDs := range wikiLinks { - eID, _ := convert.StringToUUID(eIDStr) - err = wikiRepo.CreateEntityWikis(ctx, sqlc.CreateEntityWikisParams{ - EntityID: eID, - WikiIds: wIDs, - ProjectID: projectUUID, - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki entity: "+err.Error()) - } - } - } - - newSnapshot, err := json.Marshal(snapshotData) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal snapshot") - } - _, err = commitRepo.UpdateSnapshot(ctx, commitUUID, newSnapshot) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update snapshot: "+err.Error()) - } - } - - if status == constants.StatusTypeApproved { - wikiDeleteIDs := make([]string, 0) - entityDeleteIDs := make([]string, 0) - - for _, id := range listDeleteWikis { - wikiDeleteIDs = append(wikiDeleteIDs, convert.UUIDToString(id)) - } - for _, id := range listDeleteEntities { - entityDeleteIDs = append(entityDeleteIDs, convert.UUIDToString(id)) - } - - for _, wiki := range snapshotData.Wikis { - if wiki.Operation == "delete" { - wikiDeleteIDs = append(wikiDeleteIDs, wiki.ID) - } - } - for _, entity := range snapshotData.Entities { - if entity.Operation == "delete" { - entityDeleteIDs = append(entityDeleteIDs, entity.ID) - } - } - - ragTask := models.RagIndexTask{ - ProjectID: commit.ProjectID, - DeleteWikiIDs: wikiDeleteIDs, - DeleteEntityIDs: entityDeleteIDs, - } - - for _, wiki := range snapshotData.Wikis { - ragTask.Wikis = append(ragTask.Wikis, &models.RagWikiItem{ - ID: wiki.ID, - Title: wiki.Title, - Doc: wiki.Doc, - Source: wiki.Source, - }) - } - for _, entity := range snapshotData.Entities { - ragTask.Entities = append(ragTask.Entities, &models.RagEntityItem{ - ID: entity.ID, - Name: entity.Name, - Description: entity.Description, - Source: entity.Source, - }) - } - - if err := s.c.PublishTask(ctx, constants.StreamRagName, constants.TaskTypeRagIndexSubmission, ragTask); err != nil { - log.Error().Err(err).Str("project_id", commit.ProjectID).Msg("Failed to publish RAG index task") + if err := s.applySnapshot(ctx, tx, projectUUID, commitUUID, &snapshotData); err != nil { + return nil, err } } @@ -943,12 +413,700 @@ func (s *submissionService) DeleteSubmission(ctx context.Context, userID string, return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this submission") } - err = s.submissionRepo.Delete(ctx, submissionUUID) + tx, err := s.db.Begin(ctx) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") + } + defer tx.Rollback(ctx) + + submissionRepo := s.submissionRepo.WithTx(tx) + + if submission.Status == constants.StatusTypeApproved { + projectUUID, err := convert.StringToUUID(submission.ProjectID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project ID") + } + + prevSubmission, err := s.submissionRepo.GetLatestApprovedSubmissionExcluding(ctx, projectUUID, submissionUUID) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + if err := s.clearProjectItems(ctx, tx, projectUUID); err != nil { + return err + } + } else { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get previous approved submission: "+err.Error()) + } + } else if prevSubmission != nil { + prevCommitUUID, err := convert.StringToUUID(prevSubmission.CommitID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid previous commit ID") + } + + prevCommit, err := s.commitRepo.GetByID(ctx, prevCommitUUID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, "Previous commit not found") + } + + var prevSnapshotData request.CommitSnapshot + err = json.Unmarshal(prevCommit.SnapshotJson, &prevSnapshotData) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse previous commit snapshot") + } + + if err := s.applySnapshot(ctx, tx, projectUUID, prevCommitUUID, &prevSnapshotData); err != nil { + return err + } + } else { + if err := s.clearProjectItems(ctx, tx, projectUUID); err != nil { + return err + } + } + } + + err = submissionRepo.Delete(ctx, submissionUUID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete submission") } + err = tx.Commit(ctx) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction: "+err.Error()) + } + + if submission.Status == constants.StatusTypeApproved { + go func() { + bgCtx := context.Background() + _ = s.c.DelByPattern(bgCtx, "entity:search*") + _ = s.c.DelByPattern(bgCtx, "geometry:search*") + _ = s.c.DelByPattern(bgCtx, "geometry:search:entity*") + _ = s.c.DelByPattern(bgCtx, "wiki:search*") + }() + } + _ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", submission.ProjectID)) return nil } + +func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projectUUID pgtype.UUID, commitUUID pgtype.UUID, snapshotData *request.CommitSnapshot) *fiber.Error { + entityRepo := s.entityRepo.WithTx(tx) + geometryRepo := s.geometryRepo.WithTx(tx) + wikiRepo := s.wikiRepo.WithTx(tx) + battleReplayRepo := s.battleReplayRepo.WithTx(tx) + + currentEntity, err := s.entityRepo.GetByProjectID(ctx, projectUUID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, "Entity not found: "+err.Error()) + } + + currentGeometry, err := s.geometryRepo.GetByProjectID(ctx, projectUUID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, "Geometry not found: "+err.Error()) + } + + currentWiki, err := s.wikiRepo.GetByProjectID(ctx, projectUUID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, "Wiki not found: "+err.Error()) + } + + currentBattleReplay, err := s.battleReplayRepo.GetByProjectID(ctx, projectUUID) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, "Battle replay not found: "+err.Error()) + } + + persistItemIDs := make(map[string]struct{}) + for _, item := range snapshotData.Entities { + persistItemIDs[item.ID] = struct{}{} + } + for _, item := range snapshotData.Geometries { + persistItemIDs[item.ID] = struct{}{} + } + for _, item := range snapshotData.Wikis { + persistItemIDs[item.ID] = struct{}{} + } + for _, item := range snapshotData.Replays { + persistItemIDs[item.ID] = struct{}{} + } + + persistCurrentItemIDs := make(map[string]struct{}) + for _, item := range currentEntity { + persistCurrentItemIDs[item.ID] = struct{}{} + } + for _, item := range currentGeometry { + persistCurrentItemIDs[item.ID] = struct{}{} + } + for _, item := range currentWiki { + persistCurrentItemIDs[item.ID] = struct{}{} + } + for _, item := range currentBattleReplay { + persistCurrentItemIDs[item.ID] = struct{}{} + } + + listDeleteEntities := make([]pgtype.UUID, 0) + listDeleteWikis := make([]pgtype.UUID, 0) + listDeleteGeometries := make([]pgtype.UUID, 0) + listDeleteBattleReplays := make([]pgtype.UUID, 0) + + for _, e := range currentEntity { + if _, ok := persistItemIDs[e.ID]; !ok { + itemUUID, err := convert.StringToUUID(e.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID") + } + listDeleteEntities = append(listDeleteEntities, itemUUID) + delete(persistCurrentItemIDs, e.ID) + } + } + + for _, g := range currentGeometry { + if _, ok := persistItemIDs[g.ID]; !ok { + itemUUID, err := convert.StringToUUID(g.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID") + } + listDeleteGeometries = append(listDeleteGeometries, itemUUID) + delete(persistCurrentItemIDs, g.ID) + } + } + + for _, w := range currentWiki { + if _, ok := persistItemIDs[w.ID]; !ok { + itemUUID, err := convert.StringToUUID(w.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID") + } + listDeleteWikis = append(listDeleteWikis, itemUUID) + delete(persistCurrentItemIDs, w.ID) + } + } + + for _, br := range currentBattleReplay { + if _, ok := persistItemIDs[br.ID]; !ok { + itemUUID, err := convert.StringToUUID(br.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID") + } + listDeleteBattleReplays = append(listDeleteBattleReplays, itemUUID) + delete(persistCurrentItemIDs, br.ID) + } + } + + if len(listDeleteEntities) > 0 { + if err = entityRepo.DeleteByIDs(ctx, listDeleteEntities); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete entities") + } + } + + if len(listDeleteGeometries) > 0 { + if err = geometryRepo.DeleteByIDs(ctx, listDeleteGeometries); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometries") + } + } + + if len(listDeleteWikis) > 0 { + if err = wikiRepo.DeleteByIDs(ctx, listDeleteWikis); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wikis") + } + } + + if len(listDeleteBattleReplays) > 0 { + if err = battleReplayRepo.DeleteByIDs(ctx, listDeleteBattleReplays); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete battle replays") + } + } + + refEntityIDs := []string{} + for _, e := range snapshotData.Entities { + if e.Source == "ref" { + refEntityIDs = append(refEntityIDs, e.ID) + } + } + + refEntities, _ := s.entityRepo.GetByIDs(ctx, refEntityIDs) + refEntityMap := make(map[string]bool) + for _, e := range refEntities { + refEntityMap[e.ID] = true + } + + newEntities := make([]*request.EntitySnapshot, 0, len(snapshotData.Entities)) + for i, entity := range snapshotData.Entities { + if entity.Operation == "delete" { + continue + } + + entityUUID, err := convert.StringToUUID(entity.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID") + } + + if _, ok := persistCurrentItemIDs[entity.ID]; ok { + _, err := entityRepo.Update(ctx, sqlc.UpdateEntityParams{ + Name: convert.StringToText(entity.Name), + Description: convert.StringToText(entity.Description), + Slug: convert.PtrToText(entity.Slug), + Status: convert.PtrToInt2(entity.Status), + TimeStart: convert.PtrFloat64ToInt4(entity.TimeStart), + TimeEnd: convert.PtrFloat64ToInt4(entity.TimeEnd), + ID: entityUUID, + }) + + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update entity: "+err.Error()) + } + + newEntities = append(newEntities, snapshotData.Entities[i]) + + } else if entity.Source == "inline" { + _, err := entityRepo.Create(ctx, sqlc.CreateEntityParams{ + ID: entityUUID, + Name: entity.Name, + Description: convert.StringToText(entity.Description), + ProjectID: projectUUID, + Slug: convert.PtrToText(entity.Slug), + Status: convert.PtrToInt2(entity.Status), + TimeStart: convert.PtrFloat64ToInt4(entity.TimeStart), + TimeEnd: convert.PtrFloat64ToInt4(entity.TimeEnd), + }) + + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create entity: "+err.Error()) + } + + newEntities = append(newEntities, snapshotData.Entities[i]) + + } else if entity.Source == "ref" { + if !refEntityMap[entity.ID] { + continue + } + newEntities = append(newEntities, snapshotData.Entities[i]) + } + } + snapshotData.Entities = newEntities + + refGeometryIDs := []string{} + for _, g := range snapshotData.Geometries { + if g.Source == "ref" { + refGeometryIDs = append(refGeometryIDs, g.ID) + } + } + refGeometries, _ := s.geometryRepo.GetByIDs(ctx, refGeometryIDs) + refGeometryMap := make(map[string]bool) + for _, g := range refGeometries { + refGeometryMap[g.ID] = true + } + + newGeometries := make([]*request.GeometrySnapshot, 0, len(snapshotData.Geometries)) + for i, geo := range snapshotData.Geometries { + if geo.Operation == "delete" { + continue + } + + geometryUUID, err := convert.StringToUUID(geo.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID") + } + + binding, _ := json.Marshal(geo.Binding) + geoTypeCode := int16(0) + if geo.Type != "" { + if n, err := strconv.ParseInt(geo.Type, 10, 16); err == nil { + geoTypeCode = int16(n) + } + } + + if _, ok := persistCurrentItemIDs[geo.ID]; ok { + params := sqlc.UpdateGeometryParams{ + ID: geometryUUID, + GeoType: pgtype.Int2{Int16: geoTypeCode, Valid: true}, + DrawGeometry: geo.DrawGeometry, + Binding: binding, + TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart), + TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd), + ProjectID: projectUUID, + } + + if geo.BBox != nil { + params.UpdateBbox = pgtype.Bool{Bool: true, Valid: true} + params.MinLng = pgtype.Float8{Float64: geo.BBox.MinLng, Valid: true} + params.MinLat = pgtype.Float8{Float64: geo.BBox.MinLat, Valid: true} + params.MaxLng = pgtype.Float8{Float64: geo.BBox.MaxLng, Valid: true} + params.MaxLat = pgtype.Float8{Float64: geo.BBox.MaxLat, Valid: true} + } + + _, err := geometryRepo.Update(ctx, params) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update geometry: "+err.Error()) + } + newGeometries = append(newGeometries, snapshotData.Geometries[i]) + + } else if geo.Source == "inline" { + params := sqlc.CreateGeometryParams{ + ID: geometryUUID, + GeoType: geoTypeCode, + DrawGeometry: geo.DrawGeometry, + Binding: binding, + TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart), + TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd), + ProjectID: projectUUID, + } + if geo.BBox != nil { + params.MinLng = geo.BBox.MinLng + params.MinLat = geo.BBox.MinLat + params.MaxLng = geo.BBox.MaxLng + params.MaxLat = geo.BBox.MaxLat + } + + _, err := geometryRepo.Create(ctx, params) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry: "+err.Error()) + } + newGeometries = append(newGeometries, snapshotData.Geometries[i]) + + } else if geo.Source == "ref" { + if !refGeometryMap[geo.ID] { + continue + } + newGeometries = append(newGeometries, snapshotData.Geometries[i]) + } + } + snapshotData.Geometries = newGeometries + + refWikiIDs := []string{} + for _, w := range snapshotData.Wikis { + if w.Source == "ref" { + refWikiIDs = append(refWikiIDs, w.ID) + } + } + refWikis, _ := s.wikiRepo.GetByIDs(ctx, refWikiIDs) + refWikiMap := make(map[string]bool) + for _, w := range refWikis { + refWikiMap[w.ID] = true + } + + newWikis := make([]*request.WikiSnapshot, 0, len(snapshotData.Wikis)) + for i, wiki := range snapshotData.Wikis { + if wiki.Operation == "delete" { + continue + } + + wikiUUID, err := convert.StringToUUID(wiki.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID") + } + + if _, ok := persistCurrentItemIDs[wiki.ID]; ok { + _, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{ + ID: wikiUUID, + Title: convert.StringToText(wiki.Title), + Slug: convert.PtrToText(wiki.Slug), + ProjectID: projectUUID, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+err.Error()) + } + + count, err := s.wikiRepo.GetContentCountByWikiID(ctx, wikiUUID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get wiki content count: "+err.Error()) + } + versionTitle := fmt.Sprintf("Version %d", count+1) + + _, err = wikiRepo.CreateContent(ctx, sqlc.CreateWikiContentParams{ + WikiID: wikiUUID, + Title: versionTitle, + Content: convert.StringToText(wiki.Doc), + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki content: "+err.Error()) + } + + _ = s.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wikiUUID.String()), fmt.Sprintf("wiki:slug:%s", *wiki.Slug)) + + newWikis = append(newWikis, snapshotData.Wikis[i]) + + } else if wiki.Source == "inline" { + _, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{ + ID: wikiUUID, + Title: convert.StringToText(wiki.Title), + Slug: convert.PtrToText(wiki.Slug), + ProjectID: projectUUID, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+err.Error()) + } + + _, err = wikiRepo.CreateContent(ctx, sqlc.CreateWikiContentParams{ + WikiID: wikiUUID, + Title: "Version 1", + Content: convert.StringToText(wiki.Doc), + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki content: "+err.Error()) + } + + _ = s.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wikiUUID.String()), fmt.Sprintf("wiki:slug:%s", *wiki.Slug)) + + newWikis = append(newWikis, snapshotData.Wikis[i]) + + } else if wiki.Source == "ref" { + if !refWikiMap[wiki.ID] { + continue + } + newWikis = append(newWikis, snapshotData.Wikis[i]) + } + } + snapshotData.Wikis = newWikis + + for _, replay := range snapshotData.Replays { + replayUUID, err := convert.StringToUUID(replay.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid battle replay ID") + } + + geomUUID, err := convert.StringToUUID(replay.GeometryID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID in battle replay") + } + + targetIDs, err := json.Marshal(replay.TargetGeometryIDs) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal target geometry IDs") + } + + if _, ok := persistCurrentItemIDs[replay.ID]; ok { + _, err := battleReplayRepo.Update(ctx, sqlc.UpdateBattleReplayParams{ + ID: replayUUID, + GeometryID: geomUUID, + TargetGeometryIds: targetIDs, + Detail: replay.Detail, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update battle replay: "+err.Error()) + } + } else { + _, err := battleReplayRepo.Create(ctx, sqlc.CreateBattleReplayParams{ + ID: replayUUID, + GeometryID: geomUUID, + ProjectID: projectUUID, + TargetGeometryIds: targetIDs, + Detail: replay.Detail, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create battle replay: "+err.Error()) + } + } + } + + err = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometry entity: "+err.Error()) + } + err = wikiRepo.DeleteEntityWikisByProjectID(ctx, projectUUID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wiki entity: "+err.Error()) + } + + validEntities := make(map[string]bool) + for _, e := range snapshotData.Entities { + validEntities[e.ID] = true + } + validGeometries := make(map[string]bool) + for _, g := range snapshotData.Geometries { + validGeometries[g.ID] = true + } + validWikis := make(map[string]bool) + for _, w := range snapshotData.Wikis { + validWikis[w.ID] = true + } + + if len(snapshotData.GeometryEntity) > 0 { + geomLinks := make(map[string][]pgtype.UUID) + for _, link := range snapshotData.GeometryEntity { + if !validEntities[link.EntityID] || !validGeometries[link.GeometryID] { + continue + } + gID, _ := convert.StringToUUID(link.GeometryID) + geomLinks[link.EntityID] = append(geomLinks[link.EntityID], gID) + } + + for eIDStr, gIDs := range geomLinks { + eID, _ := convert.StringToUUID(eIDStr) + err = geometryRepo.CreateEntityGeometries(ctx, sqlc.CreateEntityGeometriesParams{ + EntityID: eID, + GeometryIds: gIDs, + ProjectID: projectUUID, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry entity: "+err.Error()) + } + } + } + + if len(snapshotData.EntityWiki) > 0 { + wikiLinks := make(map[string][]pgtype.UUID) + for _, link := range snapshotData.EntityWiki { + if link.Operation == "delete" || (link.IsDeleted != nil && *link.IsDeleted == 1) { + continue + } + if !validEntities[link.EntityID] || !validWikis[link.WikiID] { + continue + } + wID, _ := convert.StringToUUID(link.WikiID) + wikiLinks[link.EntityID] = append(wikiLinks[link.EntityID], wID) + } + + for eIDStr, wIDs := range wikiLinks { + eID, _ := convert.StringToUUID(eIDStr) + err = wikiRepo.CreateEntityWikis(ctx, sqlc.CreateEntityWikisParams{ + EntityID: eID, + WikiIds: wIDs, + ProjectID: projectUUID, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki entity: "+err.Error()) + } + } + } + + newSnapshot, err := json.Marshal(snapshotData) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal snapshot") + } + commitRepo := s.commitRepo.WithTx(tx) + _, err = commitRepo.UpdateSnapshot(ctx, commitUUID, newSnapshot) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update snapshot: "+err.Error()) + } + + wikiDeleteIDs := make([]string, 0) + entityDeleteIDs := make([]string, 0) + + for _, id := range listDeleteWikis { + wikiDeleteIDs = append(wikiDeleteIDs, convert.UUIDToString(id)) + } + for _, id := range listDeleteEntities { + entityDeleteIDs = append(entityDeleteIDs, convert.UUIDToString(id)) + } + + for _, wiki := range snapshotData.Wikis { + if wiki.Operation == "delete" { + wikiDeleteIDs = append(wikiDeleteIDs, wiki.ID) + } + } + for _, entity := range snapshotData.Entities { + if entity.Operation == "delete" { + entityDeleteIDs = append(entityDeleteIDs, entity.ID) + } + } + + ragTask := models.RagIndexTask{ + ProjectID: convert.UUIDToString(projectUUID), + DeleteWikiIDs: wikiDeleteIDs, + DeleteEntityIDs: entityDeleteIDs, + } + + for _, wiki := range snapshotData.Wikis { + ragTask.Wikis = append(ragTask.Wikis, &models.RagWikiItem{ + ID: wiki.ID, + Title: wiki.Title, + Doc: wiki.Doc, + Source: wiki.Source, + }) + } + for _, entity := range snapshotData.Entities { + ragTask.Entities = append(ragTask.Entities, &models.RagEntityItem{ + ID: entity.ID, + Name: entity.Name, + Description: entity.Description, + Source: entity.Source, + }) + } + + if err := s.c.PublishTask(ctx, constants.StreamRagName, constants.TaskTypeRagIndexSubmission, ragTask); err != nil { + log.Error().Err(err).Str("project_id", convert.UUIDToString(projectUUID)).Msg("Failed to publish RAG index task") + } + + return nil +} + +func (s *submissionService) clearProjectItems(ctx context.Context, tx pgx.Tx, projectUUID pgtype.UUID) *fiber.Error { + entityRepo := s.entityRepo.WithTx(tx) + geometryRepo := s.geometryRepo.WithTx(tx) + wikiRepo := s.wikiRepo.WithTx(tx) + battleReplayRepo := s.battleReplayRepo.WithTx(tx) + + currentEntity, _ := s.entityRepo.GetByProjectID(ctx, projectUUID) + currentGeometry, _ := s.geometryRepo.GetByProjectID(ctx, projectUUID) + currentWiki, _ := s.wikiRepo.GetByProjectID(ctx, projectUUID) + currentBattleReplay, _ := s.battleReplayRepo.GetByProjectID(ctx, projectUUID) + + var entityIDs []pgtype.UUID + for _, e := range currentEntity { + id, err := convert.StringToUUID(e.ID) + if err == nil { + entityIDs = append(entityIDs, id) + } + } + var geometryIDs []pgtype.UUID + for _, g := range currentGeometry { + id, err := convert.StringToUUID(g.ID) + if err == nil { + geometryIDs = append(geometryIDs, id) + } + } + var wikiIDs []pgtype.UUID + for _, w := range currentWiki { + id, err := convert.StringToUUID(w.ID) + if err == nil { + wikiIDs = append(wikiIDs, id) + } + } + var replayIDs []pgtype.UUID + for _, br := range currentBattleReplay { + id, err := convert.StringToUUID(br.ID) + if err == nil { + replayIDs = append(replayIDs, id) + } + } + + if len(entityIDs) > 0 { + _ = entityRepo.DeleteByIDs(ctx, entityIDs) + for _, e := range currentEntity { + _ = s.c.Del(ctx, fmt.Sprintf("entity:slug:%s", e.Slug)) + } + } + if len(geometryIDs) > 0 { + _ = geometryRepo.DeleteByIDs(ctx, geometryIDs) + } + if len(wikiIDs) > 0 { + _ = wikiRepo.DeleteByIDs(ctx, wikiIDs) + for _, w := range currentWiki { + _ = s.c.Del(ctx, fmt.Sprintf("wiki:slug:%s", w.Slug)) + } + } + if len(replayIDs) > 0 { + _ = battleReplayRepo.DeleteByIDs(ctx, replayIDs) + } + + _ = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID) + _ = wikiRepo.DeleteEntityWikisByProjectID(ctx, projectUUID) + + var entityDeleteIDs []string + for _, e := range currentEntity { + entityDeleteIDs = append(entityDeleteIDs, e.ID) + } + var wikiDeleteIDs []string + for _, w := range currentWiki { + wikiDeleteIDs = append(wikiDeleteIDs, w.ID) + } + if len(entityDeleteIDs) > 0 || len(wikiDeleteIDs) > 0 { + ragTask := models.RagIndexTask{ + ProjectID: convert.UUIDToString(projectUUID), + DeleteWikiIDs: wikiDeleteIDs, + DeleteEntityIDs: entityDeleteIDs, + } + _ = s.c.PublishTask(ctx, constants.StreamRagName, constants.TaskTypeRagIndexSubmission, ragTask) + } + + return nil +}