feat: implement core backend architecture and project management services for the History API
Build and Release / release (push) Successful in 1m33s

This commit is contained in:
2026-06-05 14:18:55 +07:00
parent 420a9ad43a
commit fdcd44cc00
70 changed files with 944 additions and 734 deletions
+9 -2
View File
@@ -61,9 +61,16 @@ func (s *battleReplayService) GetByGeometryIDs(ctx context.Context, req *request
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get battle replays")
}
result := make(map[string][]*response.BattleReplayResponse)
counts := make(map[string]int, len(req.GeometryIDs))
for _, replay := range replays {
if replay != nil {
counts[replay.GeometryID]++
}
}
result := make(map[string][]*response.BattleReplayResponse, len(req.GeometryIDs))
for _, idStr := range req.GeometryIDs {
result[idStr] = make([]*response.BattleReplayResponse, 0)
result[idStr] = make([]*response.BattleReplayResponse, 0, counts[idStr])
}
for _, replay := range replays {
+1
View File
@@ -70,6 +70,7 @@ func (s *chatbotService) Chat(ctx context.Context, userID string, projectID *str
}
var contextBuilder strings.Builder
contextBuilder.Grow(len(results) * 96)
for i, res := range results {
contextBuilder.WriteString(fmt.Sprintf("<doc id=\"%d\" score=\"%.2f\">\n%s\n</doc>\n\n", i+1, res.Similarity, res.Content))
}
+4 -5
View File
@@ -2,8 +2,6 @@ package services
import (
"context"
"encoding/json"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
@@ -12,6 +10,7 @@ import (
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
json "history-api/pkg/jsonx"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
@@ -52,7 +51,7 @@ func (s *commitService) checkWritePermission(ctx context.Context, userID string,
return fiber.NewError(fiber.StatusNotFound, "Project not found")
}
lockKey := fmt.Sprintf("project:lock:%s", convert.UUIDToString(projectUUID))
lockKey := cache.Key("project:lock", convert.UUIDToString(projectUUID))
var lockUser string
if err := s.c.Get(ctx, lockKey, &lockUser); err == nil && lockUser != "" && lockUser != userID {
return fiber.NewError(fiber.StatusConflict, "Cannot commit: Project is locked by another user who is editing")
@@ -141,7 +140,7 @@ func (s *commitService) CreateCommit(ctx context.Context, userID string, project
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
_ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", projectID), fmt.Sprintf("commit:project:%s", projectID))
_ = s.c.Del(ctx, cache.Key("project:id", projectID), cache.Key("commit:project", projectID))
return commit.ToResponse(), nil
}
@@ -178,7 +177,7 @@ func (s *commitService) RestoreCommit(ctx context.Context, userID string, projec
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore commit")
}
_ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", projectID), fmt.Sprintf("commit:project:%s", projectID))
_ = s.c.Del(ctx, cache.Key("project:id", projectID), cache.Key("commit:project", projectID))
return nil
}
+12 -6
View File
@@ -109,8 +109,13 @@ func (s *entityService) GetEntitiesByGeometryIDs(ctx context.Context, req *reque
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch entity IDs by geometry IDs")
}
entityIDMap := make(map[string]struct{})
var allEntityIDs []string
totalEntityIDs := 0
for _, eIDs := range mapping {
totalEntityIDs += len(eIDs)
}
entityIDMap := make(map[string]struct{}, totalEntityIDs)
allEntityIDs := make([]string, 0, totalEntityIDs)
for _, eIDs := range mapping {
for _, eID := range eIDs {
if _, ok := entityIDMap[eID]; !ok {
@@ -125,15 +130,16 @@ func (s *entityService) GetEntitiesByGeometryIDs(ctx context.Context, req *reque
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch entities")
}
entitiesByID := make(map[string]*models.EntityEntity)
entitiesByID := make(map[string]*models.EntityEntity, len(entities))
for _, e := range entities {
entitiesByID[e.ID] = e
}
result := make(map[string][]*response.EntityResponse)
result := make(map[string][]*response.EntityResponse, len(req.GeometryIDs))
for _, idStr := range req.GeometryIDs {
result[idStr] = make([]*response.EntityResponse, 0)
if eIDs, exists := mapping[idStr]; exists {
eIDs, exists := mapping[idStr]
result[idStr] = make([]*response.EntityResponse, 0, len(eIDs))
if exists {
for _, eID := range eIDs {
if e, found := entitiesByID[eID]; found {
result[idStr] = append(result[idStr], e.ToResponse())
+6 -3
View File
@@ -57,6 +57,9 @@ func (s *geometryService) GetGeometriesByBoundWith(ctx context.Context, boundWit
func (s *geometryService) SearchGeometries(ctx context.Context, req *request.SearchGeometryDto) ([]*response.GeometryResponse, *fiber.Error) {
params := sqlc.SearchGeometriesParams{}
if req.Limit > 0 {
params.LimitCount = int32(req.Limit)
}
if req.MinLng != nil && req.MinLat != nil && req.MaxLng != nil && req.MaxLat != nil {
if *req.MaxLng < *req.MinLng || *req.MaxLat < *req.MinLat {
@@ -133,8 +136,8 @@ func (s *geometryService) SearchGeometriesByEntityName(
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search geometries by entity name")
}
byEntity := make(map[string]*response.EntityGeometriesSearchItem)
order := make([]string, 0)
byEntity := make(map[string]*response.EntityGeometriesSearchItem, len(rows))
order := make([]string, 0, len(rows))
for _, row := range rows {
item := byEntity[row.EntityID]
@@ -143,7 +146,7 @@ func (s *geometryService) SearchGeometriesByEntityName(
EntityID: row.EntityID,
Name: row.EntityName,
Description: row.EntityDescription,
Geometries: make([]*response.EntityGeometrySearchGeo, 0),
Geometries: make([]*response.EntityGeometrySearchGeo, 0, 1),
}
byEntity[row.EntityID] = item
order = append(order, row.EntityID)
+2 -2
View File
@@ -70,7 +70,7 @@ func (s *goongService) ProxyRequest(ctx context.Context, method string, targetUR
parsedUrl.RawQuery = q.Encode()
finalURL := parsedUrl.String()
cacheKey := fmt.Sprintf("goong_proxy_v2:%s", finalURL)
cacheKey := cache.Key("goong_proxy_v2", finalURL)
if method == http.MethodGet {
var entry CacheEntry
@@ -180,7 +180,7 @@ func (s *goongService) ProxyRequest(ctx context.Context, method string, targetUR
Msg("Goong Map proxy request returned non-200 status code")
}
respHeaders := make(map[string]string)
respHeaders := make(map[string]string, len(resp.Header))
for k, v := range resp.Header {
lowerK := strings.ToLower(k)
// Skip hop-by-hop/transport headers in response
+4 -3
View File
@@ -2,7 +2,6 @@ package services
import (
"context"
"encoding/json"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
@@ -12,6 +11,7 @@ import (
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
json "history-api/pkg/jsonx"
"history-api/pkg/storage"
"io"
"mime/multipart"
@@ -98,8 +98,8 @@ func (m *mediaService) BulkDeleteMedia(ctx context.Context, claims *response.JWT
if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) {
shoudDelete = true
}
listMediaIds := make([]pgtype.UUID, 0)
listMediaStorageEntities := make([]*models.MediaStorageEntity, 0)
listMediaIds := make([]pgtype.UUID, 0, len(listMedia))
listMediaStorageEntities := make([]*models.MediaStorageEntity, 0, len(listMedia))
for _, media := range listMedia {
if media.UserID != claims.UId && !shoudDelete {
return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete media "+media.ID)
@@ -171,6 +171,7 @@ func (m *mediaService) fillSearchArgs(arg *sqlc.SearchMediasParams, dto *request
}
if len(dto.UserIDs) > 0 {
arg.UserIds = make([]pgtype.UUID, 0, len(dto.UserIDs))
for _, id := range dto.UserIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.UserIds = append(arg.UserIds, u)
+6 -5
View File
@@ -2,7 +2,6 @@ package services
import (
"context"
"fmt"
"time"
"github.com/gofiber/fiber/v3"
@@ -87,7 +86,7 @@ func (s *projectService) GetProjectByID(ctx context.Context, id string) (*respon
}
res := project.ToResponse()
lockKey := fmt.Sprintf("project:lock:%s", id)
lockKey := cache.Key("project:lock", id)
var lockUser string
if err := s.c.Get(ctx, lockKey, &lockUser); err == nil && lockUser != "" {
res.LockedBy = &lockUser
@@ -141,6 +140,7 @@ func (s *projectService) fillSearchArgs(arg *sqlc.SearchProjectsParams, dto *req
}
if len(dto.Statuses) > 0 {
arg.Statuses = make([]int16, 0, len(dto.Statuses))
for _, statusStr := range dto.Statuses {
statusType := constants.ParseProjectStatusTypeText(statusStr)
if statusType != constants.ProjectStatusTypeUnknow {
@@ -150,6 +150,7 @@ func (s *projectService) fillSearchArgs(arg *sqlc.SearchProjectsParams, dto *req
}
if len(dto.UserIDs) > 0 {
arg.UserIds = make([]pgtype.UUID, 0, len(dto.UserIDs))
for _, id := range dto.UserIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.UserIds = append(arg.UserIds, u)
@@ -464,7 +465,7 @@ func (s *projectService) LockProject(ctx context.Context, userID string, project
return fiber.NewError(fiber.StatusNotFound, "Project not found")
}
lockKey := fmt.Sprintf("project:lock:%s", projectID)
lockKey := cache.Key("project:lock", projectID)
var lockUser string
err = s.c.Get(ctx, lockKey, &lockUser)
if err == nil && lockUser != "" {
@@ -487,7 +488,7 @@ func (s *projectService) LockProject(ctx context.Context, userID string, project
}
func (s *projectService) UnlockProject(ctx context.Context, userID string, projectID string) *fiber.Error {
lockKey := fmt.Sprintf("project:lock:%s", projectID)
lockKey := cache.Key("project:lock", projectID)
var lockUser string
err := s.c.Get(ctx, lockKey, &lockUser)
if err != nil || lockUser == "" {
@@ -503,7 +504,7 @@ func (s *projectService) UnlockProject(ctx context.Context, userID string, proje
}
func (s *projectService) HeartbeatProject(ctx context.Context, userID string, projectID string) *fiber.Error {
lockKey := fmt.Sprintf("project:lock:%s", projectID)
lockKey := cache.Key("project:lock", projectID)
var lockUser string
err := s.c.Get(ctx, lockKey, &lockUser)
if err != nil || lockUser == "" {
+13 -13
View File
@@ -9,7 +9,7 @@ import (
type RasterTileService interface {
GetMetadata(ctx context.Context) (map[string]string, *fiber.Error)
GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, *fiber.Error)
GetTile(ctx context.Context, z, x, y int) (TileResponse, *fiber.Error)
}
type rasterTileService struct {
@@ -32,26 +32,26 @@ func (t *rasterTileService) GetMetadata(ctx context.Context) (map[string]string,
return metaData, nil
}
func (t *rasterTileService) GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, *fiber.Error) {
contentType := make(map[string]string)
func (t *rasterTileService) GetTile(ctx context.Context, z, x, y int) (TileResponse, *fiber.Error) {
data, format, err := t.tileRepo.GetTile(ctx, z, x, y)
if err != nil {
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch tile data")
return TileResponse{}, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch tile data")
}
res := TileResponse{
Data: data,
CacheControl: tileCacheControl,
}
switch format {
case "png":
contentType["Content-Type"] = "image/png"
res.ContentType = "image/png"
case "jpg", "jpeg":
contentType["Content-Type"] = "image/jpeg"
res.ContentType = "image/jpeg"
case "webp":
contentType["Content-Type"] = "image/webp"
res.ContentType = "image/webp"
default:
contentType["Content-Type"] = "application/octet-stream"
res.ContentType = "application/octet-stream"
}
return data, contentType, nil
return res, nil
}
+63 -59
View File
@@ -2,7 +2,6 @@ package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"history-api/internal/dtos/request"
@@ -14,6 +13,7 @@ import (
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
json "history-api/pkg/jsonx"
"regexp"
"slices"
"strconv"
@@ -110,8 +110,8 @@ func (s *submissionService) CreateSubmission(ctx context.Context, userID string,
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot")
}
var entitySlugs []string
entitySlugToID := make(map[string]string)
entitySlugs := make([]string, 0, len(snapshotData.Entities))
entitySlugToID := make(map[string]string, len(snapshotData.Entities))
for _, entity := range snapshotData.Entities {
if entity.Source == "inline" && entity.Slug != nil {
entitySlugs = append(entitySlugs, *entity.Slug)
@@ -133,8 +133,8 @@ func (s *submissionService) CreateSubmission(ctx context.Context, userID string,
}
}
var wikiSlugs []string
wikiSlugToID := make(map[string]string)
wikiSlugs := make([]string, 0, len(snapshotData.Wikis))
wikiSlugToID := make(map[string]string, len(snapshotData.Wikis))
for _, wiki := range snapshotData.Wikis {
if wiki.Source == "inline" && wiki.Slug != nil {
wikiSlugs = append(wikiSlugs, *wiki.Slug)
@@ -180,7 +180,7 @@ func (s *submissionService) CreateSubmission(ctx context.Context, userID string,
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create submission")
}
_ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", project.ID))
_ = s.c.Del(ctx, cache.Key("project:id", project.ID))
return submission.ToResponse(), nil
}
@@ -277,11 +277,11 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
}
_ = s.c.Del(ctx,
fmt.Sprintf("project:id:%s", submission.ProjectID),
fmt.Sprintf("entity:project:%s", submission.ProjectID),
fmt.Sprintf("geometry:project:%s", submission.ProjectID),
fmt.Sprintf("wiki:project:%s", submission.ProjectID),
fmt.Sprintf("battle_replay:project:%s", submission.ProjectID),
cache.Key("project:id", submission.ProjectID),
cache.Key("entity:project", submission.ProjectID),
cache.Key("geometry:project", submission.ProjectID),
cache.Key("wiki:project", submission.ProjectID),
cache.Key("battle_replay:project", submission.ProjectID),
)
return updatedSubmission.ToResponse(), nil
@@ -300,6 +300,7 @@ func (m *submissionService) fillSearchArgs(arg *sqlc.SearchSubmissionsParams, dt
}
if len(dto.Statuses) > 0 {
arg.Statuses = make([]int16, 0, len(dto.Statuses))
for _, id := range dto.Statuses {
if u := constants.ParseStatusTypeText(id); u != constants.StatusTypeUnknown {
arg.Statuses = append(arg.Statuses, u.Int16())
@@ -308,6 +309,7 @@ func (m *submissionService) fillSearchArgs(arg *sqlc.SearchSubmissionsParams, dt
}
if len(dto.UserIDs) > 0 {
arg.UserIds = make([]pgtype.UUID, 0, len(dto.UserIDs))
for _, id := range dto.UserIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.UserIds = append(arg.UserIds, u)
@@ -503,11 +505,11 @@ func (s *submissionService) DeleteSubmission(ctx context.Context, userID string,
}
_ = s.c.Del(ctx,
fmt.Sprintf("project:id:%s", submission.ProjectID),
fmt.Sprintf("entity:project:%s", submission.ProjectID),
fmt.Sprintf("geometry:project:%s", submission.ProjectID),
fmt.Sprintf("wiki:project:%s", submission.ProjectID),
fmt.Sprintf("battle_replay:project:%s", submission.ProjectID),
cache.Key("project:id", submission.ProjectID),
cache.Key("entity:project", submission.ProjectID),
cache.Key("geometry:project", submission.ProjectID),
cache.Key("wiki:project", submission.ProjectID),
cache.Key("battle_replay:project", submission.ProjectID),
)
return nil
@@ -521,10 +523,10 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
projectIDStr := convert.UUIDToString(projectUUID)
_ = s.c.Del(ctx,
fmt.Sprintf("entity:project:%s", projectIDStr),
fmt.Sprintf("geometry:project:%s", projectIDStr),
fmt.Sprintf("wiki:project:%s", projectIDStr),
fmt.Sprintf("battle_replay:project:%s", projectIDStr),
cache.Key("entity:project", projectIDStr),
cache.Key("geometry:project", projectIDStr),
cache.Key("wiki:project", projectIDStr),
cache.Key("battle_replay:project", projectIDStr),
)
currentEntity, err := entityRepo.GetByProjectID(ctx, projectUUID)
@@ -547,44 +549,44 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
return fiber.NewError(fiber.StatusNotFound, "Battle replay not found: "+err.Error())
}
persistEntityIDs := make(map[string]struct{})
persistEntityIDs := make(map[string]struct{}, len(snapshotData.Entities))
for _, item := range snapshotData.Entities {
persistEntityIDs[item.ID] = struct{}{}
}
persistGeometryIDs := make(map[string]struct{})
persistGeometryIDs := make(map[string]struct{}, len(snapshotData.Geometries))
for _, item := range snapshotData.Geometries {
persistGeometryIDs[item.ID] = struct{}{}
}
persistWikiIDs := make(map[string]struct{})
persistWikiIDs := make(map[string]struct{}, len(snapshotData.Wikis))
for _, item := range snapshotData.Wikis {
persistWikiIDs[item.ID] = struct{}{}
}
persistReplayIDs := make(map[string]struct{})
persistReplayIDs := make(map[string]struct{}, len(snapshotData.Replays))
for _, item := range snapshotData.Replays {
persistReplayIDs[item.ID] = struct{}{}
}
persistCurrentEntityIDs := make(map[string]struct{})
persistCurrentEntityIDs := make(map[string]struct{}, len(currentEntity))
for _, item := range currentEntity {
persistCurrentEntityIDs[item.ID] = struct{}{}
}
persistCurrentGeometryIDs := make(map[string]struct{})
persistCurrentGeometryIDs := make(map[string]struct{}, len(currentGeometry))
for _, item := range currentGeometry {
persistCurrentGeometryIDs[item.ID] = struct{}{}
}
persistCurrentWikiIDs := make(map[string]struct{})
persistCurrentWikiIDs := make(map[string]struct{}, len(currentWiki))
for _, item := range currentWiki {
persistCurrentWikiIDs[item.ID] = struct{}{}
}
persistCurrentReplayIDs := make(map[string]struct{})
persistCurrentReplayIDs := make(map[string]struct{}, len(currentBattleReplay))
for _, item := range currentBattleReplay {
persistCurrentReplayIDs[item.ID] = struct{}{}
}
listDeleteEntities := make([]pgtype.UUID, 0)
listDeleteWikis := make([]pgtype.UUID, 0)
listDeleteGeometries := make([]pgtype.UUID, 0)
listDeleteBattleReplays := make([]pgtype.UUID, 0)
listDeleteEntities := make([]pgtype.UUID, 0, len(currentEntity))
listDeleteWikis := make([]pgtype.UUID, 0, len(currentWiki))
listDeleteGeometries := make([]pgtype.UUID, 0, len(currentGeometry))
listDeleteBattleReplays := make([]pgtype.UUID, 0, len(currentBattleReplay))
for _, e := range currentEntity {
if _, ok := persistEntityIDs[e.ID]; !ok {
@@ -654,7 +656,7 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
}
}
refEntityIDs := []string{}
refEntityIDs := make([]string, 0, len(snapshotData.Entities))
for _, e := range snapshotData.Entities {
if e.Source == "ref" {
refEntityIDs = append(refEntityIDs, e.ID)
@@ -662,7 +664,7 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
}
refEntities, _ := entityRepo.GetByIDs(ctx, refEntityIDs)
refEntityMap := make(map[string]bool)
refEntityMap := make(map[string]bool, len(refEntities))
for _, e := range refEntities {
refEntityMap[e.ID] = true
}
@@ -722,19 +724,19 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
}
snapshotData.Entities = newEntities
refGeometryIDs := []string{}
refGeometryIDs := make([]string, 0, len(snapshotData.Geometries))
for _, g := range snapshotData.Geometries {
if g.Source == "ref" {
refGeometryIDs = append(refGeometryIDs, g.ID)
}
}
refGeometries, _ := geometryRepo.GetByIDs(ctx, refGeometryIDs)
refGeometryMap := make(map[string]bool)
refGeometryMap := make(map[string]bool, len(refGeometries))
for _, g := range refGeometries {
refGeometryMap[g.ID] = true
}
validGeometries := make(map[string]bool)
validGeometries := make(map[string]bool, len(snapshotData.Geometries))
for _, g := range snapshotData.Geometries {
if g.Operation != "delete" {
validGeometries[g.ID] = true
@@ -851,14 +853,14 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
}
}
refWikiIDs := []string{}
refWikiIDs := make([]string, 0, len(snapshotData.Wikis))
for _, w := range snapshotData.Wikis {
if w.Source == "ref" {
refWikiIDs = append(refWikiIDs, w.ID)
}
}
refWikis, _ := wikiRepo.GetByIDs(ctx, refWikiIDs)
refWikiMap := make(map[string]bool)
refWikiMap := make(map[string]bool, len(refWikis))
for _, w := range refWikis {
refWikiMap[w.ID] = true
}
@@ -906,7 +908,7 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
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))
_ = s.c.Del(ctx, cache.Key("wiki:id", wikiUUID.String()), cache.Key("wiki:slug", *wiki.Slug))
newWikis = append(newWikis, snapshotData.Wikis[i])
@@ -936,7 +938,7 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
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))
_ = s.c.Del(ctx, cache.Key("wiki:id", wikiUUID.String()), cache.Key("wiki:slug", *wiki.Slug))
newWikis = append(newWikis, snapshotData.Wikis[i])
@@ -998,17 +1000,17 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wiki entity: "+err.Error())
}
validEntities := make(map[string]bool)
validEntities := make(map[string]bool, len(snapshotData.Entities))
for _, e := range snapshotData.Entities {
validEntities[e.ID] = true
}
validWikis := make(map[string]bool)
validWikis := make(map[string]bool, len(snapshotData.Wikis))
for _, w := range snapshotData.Wikis {
validWikis[w.ID] = true
}
if len(snapshotData.GeometryEntity) > 0 {
geomLinks := make(map[string][]pgtype.UUID)
geomLinks := make(map[string][]pgtype.UUID, len(snapshotData.GeometryEntity))
for _, link := range snapshotData.GeometryEntity {
if link.Operation == "delete" {
continue
@@ -1034,7 +1036,7 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
}
if len(snapshotData.EntityWiki) > 0 {
wikiLinks := make(map[string][]pgtype.UUID)
wikiLinks := make(map[string][]pgtype.UUID, len(snapshotData.EntityWiki))
for _, link := range snapshotData.EntityWiki {
if link.Operation == "delete" || (link.IsDeleted != nil && *link.IsDeleted == 1) {
continue
@@ -1059,8 +1061,8 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
}
}
wikiDeleteIDs := make([]string, 0)
entityDeleteIDs := make([]string, 0)
wikiDeleteIDs := make([]string, 0, len(listDeleteWikis)+len(snapshotData.Wikis))
entityDeleteIDs := make([]string, 0, len(listDeleteEntities)+len(snapshotData.Entities))
for _, id := range listDeleteWikis {
wikiDeleteIDs = append(wikiDeleteIDs, convert.UUIDToString(id))
@@ -1084,6 +1086,8 @@ func (s *submissionService) applySnapshot(ctx context.Context, tx pgx.Tx, projec
ProjectID: convert.UUIDToString(projectUUID),
DeleteWikiIDs: wikiDeleteIDs,
DeleteEntityIDs: entityDeleteIDs,
Wikis: make([]*models.RagWikiItem, 0, len(snapshotData.Wikis)),
Entities: make([]*models.RagEntityItem, 0, len(snapshotData.Entities)),
}
for _, wiki := range snapshotData.Wikis {
@@ -1118,10 +1122,10 @@ func (s *submissionService) clearProjectItems(ctx context.Context, tx pgx.Tx, pr
projectIDStr := convert.UUIDToString(projectUUID)
_ = s.c.Del(ctx,
fmt.Sprintf("entity:project:%s", projectIDStr),
fmt.Sprintf("geometry:project:%s", projectIDStr),
fmt.Sprintf("wiki:project:%s", projectIDStr),
fmt.Sprintf("battle_replay:project:%s", projectIDStr),
cache.Key("entity:project", projectIDStr),
cache.Key("geometry:project", projectIDStr),
cache.Key("wiki:project", projectIDStr),
cache.Key("battle_replay:project", projectIDStr),
)
currentEntity, _ := entityRepo.GetByProjectID(ctx, projectUUID)
@@ -1129,28 +1133,28 @@ func (s *submissionService) clearProjectItems(ctx context.Context, tx pgx.Tx, pr
currentWiki, _ := wikiRepo.GetByProjectID(ctx, projectUUID)
currentBattleReplay, _ := battleReplayRepo.GetByProjectID(ctx, projectUUID)
var entityIDs []pgtype.UUID
entityIDs := make([]pgtype.UUID, 0, len(currentEntity))
for _, e := range currentEntity {
id, err := convert.StringToUUID(e.ID)
if err == nil {
entityIDs = append(entityIDs, id)
}
}
var geometryIDs []pgtype.UUID
geometryIDs := make([]pgtype.UUID, 0, len(currentGeometry))
for _, g := range currentGeometry {
id, err := convert.StringToUUID(g.ID)
if err == nil {
geometryIDs = append(geometryIDs, id)
}
}
var wikiIDs []pgtype.UUID
wikiIDs := make([]pgtype.UUID, 0, len(currentWiki))
for _, w := range currentWiki {
id, err := convert.StringToUUID(w.ID)
if err == nil {
wikiIDs = append(wikiIDs, id)
}
}
var replayIDs []pgtype.UUID
replayIDs := make([]pgtype.UUID, 0, len(currentBattleReplay))
for _, br := range currentBattleReplay {
id, err := convert.StringToUUID(br.ID)
if err == nil {
@@ -1161,7 +1165,7 @@ func (s *submissionService) clearProjectItems(ctx context.Context, tx pgx.Tx, pr
if len(entityIDs) > 0 {
_ = entityRepo.DeleteByIDs(ctx, entityIDs)
for _, e := range currentEntity {
_ = s.c.Del(ctx, fmt.Sprintf("entity:slug:%s", e.Slug))
_ = s.c.Del(ctx, cache.Key("entity:slug", e.Slug))
}
}
if len(geometryIDs) > 0 {
@@ -1170,7 +1174,7 @@ func (s *submissionService) clearProjectItems(ctx context.Context, tx pgx.Tx, pr
if len(wikiIDs) > 0 {
_ = wikiRepo.DeleteByIDs(ctx, wikiIDs)
for _, w := range currentWiki {
_ = s.c.Del(ctx, fmt.Sprintf("wiki:slug:%s", w.Slug))
_ = s.c.Del(ctx, cache.Key("wiki:slug", w.Slug))
}
}
if len(replayIDs) > 0 {
@@ -1180,11 +1184,11 @@ func (s *submissionService) clearProjectItems(ctx context.Context, tx pgx.Tx, pr
_ = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID)
_ = wikiRepo.DeleteEntityWikisByProjectID(ctx, projectUUID)
var entityDeleteIDs []string
entityDeleteIDs := make([]string, 0, len(currentEntity))
for _, e := range currentEntity {
entityDeleteIDs = append(entityDeleteIDs, e.ID)
}
var wikiDeleteIDs []string
wikiDeleteIDs := make([]string, 0, len(currentWiki))
for _, w := range currentWiki {
wikiDeleteIDs = append(wikiDeleteIDs, w.ID)
}
+23 -13
View File
@@ -7,9 +7,18 @@ import (
"github.com/gofiber/fiber/v3"
)
const tileCacheControl = "public, max-age=31536000, immutable"
type TileResponse struct {
Data []byte
ContentType string
ContentEncoding string
CacheControl string
}
type TileService interface {
GetMetadata(ctx context.Context) (map[string]string, *fiber.Error)
GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, *fiber.Error)
GetTile(ctx context.Context, z, x, y int) (TileResponse, *fiber.Error)
}
type tileService struct {
@@ -32,29 +41,30 @@ func (t *tileService) GetMetadata(ctx context.Context) (map[string]string, *fibe
return metaData, nil
}
func (t *tileService) GetTile(ctx context.Context, z, x, y int) ([]byte, map[string]string, *fiber.Error) {
contentType := make(map[string]string)
func (t *tileService) GetTile(ctx context.Context, z, x, y int) (TileResponse, *fiber.Error) {
data, format, isPBF, err := t.tileRepo.GetTile(ctx, z, x, y)
if err != nil {
return nil, contentType, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch tile data")
return TileResponse{}, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch tile data")
}
res := TileResponse{
Data: data,
CacheControl: tileCacheControl,
}
switch format {
case "pbf":
contentType["Content-Type"] = "application/x-protobuf"
res.ContentType = "application/x-protobuf"
case "png":
contentType["Content-Type"] = "image/png"
res.ContentType = "image/png"
case "jpg", "jpeg":
contentType["Content-Type"] = "image/jpeg"
res.ContentType = "image/jpeg"
default:
contentType["Content-Type"] = "application/octet-stream"
res.ContentType = "application/octet-stream"
}
if isPBF {
contentType["Content-Encoding"] = "gzip"
res.ContentEncoding = "gzip"
}
return data, contentType, nil
return res, nil
}
+6 -6
View File
@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
@@ -103,7 +102,7 @@ func (u *userService) CreateUser(ctx context.Context, dto *request.CreateUserDto
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create user profile")
}
var roleIdList []pgtype.UUID
roleIdList := make([]pgtype.UUID, 0, len(dto.Roles))
for _, rId := range dto.Roles {
rid, err := convert.StringToUUID(rId)
if err == nil {
@@ -339,8 +338,8 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
}
}
user.Roles = make([]*models.RoleSimple, 0)
roleIdList := make([]pgtype.UUID, 0)
user.Roles = make([]*models.RoleSimple, 0, len(newListRole))
roleIdList := make([]pgtype.UUID, 0, len(newListRole))
for _, role := range newListRole {
roleID, err := convert.StringToUUID(role.ID)
if err != nil {
@@ -378,8 +377,8 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
}
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
cache.Key("user:email", user.Email): user,
cache.Key("user:id", user.ID): user,
}
_ = u.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
@@ -489,6 +488,7 @@ func (m *userService) fillSearchArgs(arg *sqlc.SearchUsersParams, dto *request.S
}
if len(dto.RoleIDs) > 0 {
arg.RoleIds = make([]pgtype.UUID, 0, len(dto.RoleIDs))
for _, id := range dto.RoleIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.RoleIds = append(arg.RoleIds, u)
+8 -5
View File
@@ -2,7 +2,6 @@ package services
import (
"context"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
@@ -92,7 +91,8 @@ func (v *verificationService) CreateVerification(ctx context.Context, userId str
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid verification ID")
}
mediaIdList := make([]pgtype.UUID, 0)
mediaIdList := make([]pgtype.UUID, 0, len(mediaList))
item.Media = make([]*models.MediaSimpleEntity, 0, len(mediaList))
for _, it := range mediaList {
mediaId, err := convert.StringToUUID(it.ID)
if err != nil {
@@ -185,6 +185,7 @@ func (m *verificationService) fillSearchArgs(arg *sqlc.SearchUserVerificationsPa
}
if len(dto.Statuses) > 0 {
arg.Statuses = make([]int16, 0, len(dto.Statuses))
for _, id := range dto.Statuses {
if u := constants.ParseStatusTypeText(id); u != constants.StatusTypeUnknown {
arg.Statuses = append(arg.Statuses, u.Int16())
@@ -193,6 +194,7 @@ func (m *verificationService) fillSearchArgs(arg *sqlc.SearchUserVerificationsPa
}
if len(dto.VerifyTypes) > 0 {
arg.VerifyTypes = make([]int16, 0, len(dto.VerifyTypes))
for _, id := range dto.VerifyTypes {
if u := constants.ParseVerifyTypeText(id); u != constants.VerifyTypeUnknown {
arg.VerifyTypes = append(arg.VerifyTypes, u.Int16())
@@ -201,6 +203,7 @@ func (m *verificationService) fillSearchArgs(arg *sqlc.SearchUserVerificationsPa
}
if len(dto.UserIDs) > 0 {
arg.UserIds = make([]pgtype.UUID, 0, len(dto.UserIDs))
for _, id := range dto.UserIDs {
if u, err := convert.StringToUUID(id); err == nil {
arg.UserIds = append(arg.UserIds, u)
@@ -355,8 +358,8 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
}
if statusType == constants.StatusTypeApproved {
roleIdList := make([]pgtype.UUID, 0)
userVerification.Roles = append(userVerification.Roles, historianRole.ToRoleSimple())
roleIdList := make([]pgtype.UUID, 0, len(userVerification.Roles)+1)
roleIdList = append(roleIdList, historianRoleID)
@@ -395,8 +398,8 @@ func (v *verificationService) UpdateStatusVerification(ctx context.Context, user
}
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", userVerification.Email): userVerification,
fmt.Sprintf("user:id:%s", userVerification.ID): userVerification,
cache.Key("user:email", userVerification.Email): userVerification,
cache.Key("user:id", userVerification.ID): userVerification,
}
_ = v.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
} else {
+13 -7
View File
@@ -137,8 +137,13 @@ func (s *wikiService) GetWikisByEntityIDs(ctx context.Context, req *request.GetW
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch wiki IDs by entity IDs")
}
wikiIDMap := make(map[string]struct{})
var allWikiIDs []string
totalWikiIDs := 0
for _, wIDs := range mapping {
totalWikiIDs += len(wIDs)
}
wikiIDMap := make(map[string]struct{}, totalWikiIDs)
allWikiIDs := make([]string, 0, totalWikiIDs)
for _, wIDs := range mapping {
for _, wID := range wIDs {
if _, ok := wikiIDMap[wID]; !ok {
@@ -153,15 +158,16 @@ func (s *wikiService) GetWikisByEntityIDs(ctx context.Context, req *request.GetW
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch wikis")
}
wikisByID := make(map[string]*models.WikiEntity)
wikisByID := make(map[string]*models.WikiEntity, len(wikis))
for _, w := range wikis {
wikisByID[w.ID] = w
}
result := make(map[string][]*response.WikiResponse)
result := make(map[string][]*response.WikiResponse, len(req.EntityIDs))
for _, idStr := range req.EntityIDs {
result[idStr] = make([]*response.WikiResponse, 0)
if wIDs, exists := mapping[idStr]; exists {
wIDs, exists := mapping[idStr]
result[idStr] = make([]*response.WikiResponse, 0, len(wIDs))
if exists {
for _, wID := range wIDs {
if w, found := wikisByID[wID]; found {
result[idStr] = append(result[idStr], w.ToResponse())
@@ -179,7 +185,7 @@ func (s *wikiService) GetWikiContentsPreviewByIDs(ctx context.Context, req *requ
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch wiki contents")
}
var results []*response.WikiContentPreviewResponse
results := make([]*response.WikiContentPreviewResponse, 0, len(contents))
for _, c := range contents {
results = append(results, &response.WikiContentPreviewResponse{
ID: c.ID,