feat: implement relation management system with controllers, services, repositories, and corresponding API documentation.
Build and Release / release (push) Successful in 1m47s

This commit is contained in:
2026-06-06 17:22:03 +07:00
parent 7ff15ae662
commit fd8d413760
15 changed files with 706 additions and 192 deletions
+4 -1
View File
@@ -162,6 +162,7 @@ func (s *FiberServer) SetupServer(
statisticService := services.NewStatisticService(statisticRepo)
battleReplayService := services.NewBattleReplayService(battleReplayRepo)
goongService := services.NewGoongService(redis)
relationService := services.NewRelationService(wikiRepo, entityRepo, geometryRepo)
// controller setup
authController := controllers.NewAuthController(authService, oauth)
@@ -181,6 +182,7 @@ func (s *FiberServer) SetupServer(
statisticController := controllers.NewStatisticController(statisticService)
battleReplayController := controllers.NewBattleReplayController(battleReplayService)
goongController := controllers.NewGoongController(goongService)
relationController := controllers.NewRelationController(relationService)
// route setup
routes.AuthRoutes(s.App, authController, userRepo)
@@ -193,11 +195,12 @@ func (s *FiberServer) SetupServer(
routes.EntityRoutes(s.App, entityController)
routes.GeometryRoutes(s.App, geometryController)
routes.WikiRoutes(s.App, wikiController)
routes.RelationRoutes(s.App, wikiController, entityController)
routes.RelationRoutes(s.App, wikiController, entityController, relationController)
routes.ProjectRoutes(s.App, projectController, commitController, userRepo)
routes.SubmissionRoutes(s.App, submissionController, userRepo)
routes.ChatbotRoutes(s.App, chatbotController, userRepo)
routes.StatisticRoutes(s.App, statisticController, userRepo)
routes.BattleReplayRoutes(s.App, battleReplayController)
routes.GoongRoutes(s.App, goongController)
routes.NotFoundRoute(s.App)
+6
View File
@@ -248,3 +248,9 @@ SELECT
)::uuid[] AS replay_ids
FROM geometries
WHERE geometries.bound_with = $1 AND geometries.is_deleted = false;
-- name: GetGeometryIDsByEntityIDs :many
SELECT entity_id, geometry_id
FROM entity_geometries
WHERE entity_id = ANY($1::uuid[]);
+6
View File
@@ -132,3 +132,9 @@ ORDER BY created_at DESC;
SELECT entity_id, wiki_id
FROM entity_wikis
WHERE entity_id = ANY($1::uuid[]);
-- name: GetEntityIDsByWikiIDs :many
SELECT wiki_id, entity_id
FROM entity_wikis
WHERE wiki_id = ANY($1::uuid[]);
+69 -1
View File
@@ -895,6 +895,13 @@ const docTemplate = `{
"name": "has_bound",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"name": "limit",
"in": "query"
},
{
"maximum": 90,
"minimum": -90,
@@ -2933,6 +2940,66 @@ const docTemplate = `{
}
}
},
"/relations": {
"get": {
"description": "Get relations by type (wiki-entity, entity-wiki, geometry-entity, entity-geometry) and list of IDs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Relations"
],
"summary": "Get generalized batch relations",
"parameters": [
{
"minItems": 1,
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "ids",
"in": "query",
"required": true
},
{
"enum": [
"wiki-entity",
"entity-wiki",
"geometry-entity",
"entity-geometry"
],
"type": "string",
"name": "type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/relations/entities-by-geometries": {
"get": {
"description": "Get entities grouped by geometry IDs",
@@ -4900,7 +4967,8 @@ const docTemplate = `{
"type": "string"
},
"question": {
"type": "string"
"type": "string",
"maxLength": 500
}
}
},
+69 -1
View File
@@ -888,6 +888,13 @@
"name": "has_bound",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"name": "limit",
"in": "query"
},
{
"maximum": 90,
"minimum": -90,
@@ -2926,6 +2933,66 @@
}
}
},
"/relations": {
"get": {
"description": "Get relations by type (wiki-entity, entity-wiki, geometry-entity, entity-geometry) and list of IDs",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Relations"
],
"summary": "Get generalized batch relations",
"parameters": [
{
"minItems": 1,
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "csv",
"name": "ids",
"in": "query",
"required": true
},
{
"enum": [
"wiki-entity",
"entity-wiki",
"geometry-entity",
"entity-geometry"
],
"type": "string",
"name": "type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
}
}
}
}
},
"/relations/entities-by-geometries": {
"get": {
"description": "Get entities grouped by geometry IDs",
@@ -4893,7 +4960,8 @@
"type": "string"
},
"question": {
"type": "string"
"type": "string",
"maxLength": 500
}
}
},
+48
View File
@@ -82,6 +82,7 @@ definitions:
project_id:
type: string
question:
maxLength: 500
type: string
required:
- question
@@ -1327,6 +1328,11 @@ paths:
- in: query
name: has_bound
type: boolean
- in: query
maximum: 100
minimum: 1
name: limit
type: integer
- in: query
maximum: 90
minimum: -90
@@ -2655,6 +2661,48 @@ paths:
summary: Get raster tile metadata
tags:
- Tile
/relations:
get:
consumes:
- application/json
description: Get relations by type (wiki-entity, entity-wiki, geometry-entity,
entity-geometry) and list of IDs
parameters:
- collectionFormat: csv
in: query
items:
type: string
minItems: 1
name: ids
required: true
type: array
- enum:
- wiki-entity
- entity-wiki
- geometry-entity
- entity-geometry
in: query
name: type
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
summary: Get generalized batch relations
tags:
- Relations
/relations/entities-by-geometries:
get:
consumes:
@@ -0,0 +1,57 @@
package controllers
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/services"
"history-api/pkg/validator"
"time"
"github.com/gofiber/fiber/v3"
)
type RelationController struct {
service services.RelationService
}
func NewRelationController(svc services.RelationService) *RelationController {
return &RelationController{service: svc}
}
// GetRelations handles fetching relationships dynamically.
// @Summary Get generalized batch relations
// @Description Get relations by type (wiki-entity, entity-wiki, geometry-entity, entity-geometry) and list of IDs
// @Tags Relations
// @Accept json
// @Produce json
// @Param query query request.GetRelationsDto true "Query Parameters"
// @Success 200 {object} response.CommonResponse
// @Failure 400 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /relations [get]
func (h *RelationController) GetRelations(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.GetRelationsDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.service.GetRelations(ctx, dto)
if err != nil {
return c.Status(err.Code).JSON(response.CommonResponse{
Status: false,
Message: err.Message,
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
+15
View File
@@ -11,3 +11,18 @@ type GetEntitiesByGeometryIDsDto struct {
type GetWikiContentsPreviewDto struct {
IDs []string `json:"ids" query:"ids" validate:"required,min=1,dive,uuid"`
}
type GetEntitiesByWikiIDsDto struct {
WikiIDs []string `json:"wiki_ids" query:"wiki_ids" validate:"required,min=1,dive,uuid"`
}
type GetGeometriesByEntityIDsDto struct {
EntityIDs []string `json:"entity_ids" query:"entity_ids" validate:"required,min=1,dive,uuid"`
}
type GetRelationsDto struct {
Type string `json:"type" query:"type" validate:"required,oneof=wiki-entity entity-wiki geometry-entity entity-geometry"`
IDs []string `json:"ids" query:"ids" validate:"required,min=1,dive,uuid"`
}
+31
View File
@@ -538,6 +538,37 @@ func (q *Queries) GetGeometryById(ctx context.Context, id pgtype.UUID) (GetGeome
return i, err
}
const getGeometryIDsByEntityIDs = `-- name: GetGeometryIDsByEntityIDs :many
SELECT entity_id, geometry_id
FROM entity_geometries
WHERE entity_id = ANY($1::uuid[])
`
type GetGeometryIDsByEntityIDsRow struct {
EntityID pgtype.UUID `json:"entity_id"`
GeometryID pgtype.UUID `json:"geometry_id"`
}
func (q *Queries) GetGeometryIDsByEntityIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetGeometryIDsByEntityIDsRow, error) {
rows, err := q.db.Query(ctx, getGeometryIDsByEntityIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetGeometryIDsByEntityIDsRow{}
for rows.Next() {
var i GetGeometryIDsByEntityIDsRow
if err := rows.Scan(&i.EntityID, &i.GeometryID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchGeometries = `-- name: SearchGeometries :many
SELECT
g.id, g.geo_type, g.draw_geometry, g.bound_with, g.time_start, g.time_end, g.project_id,
+31
View File
@@ -188,6 +188,37 @@ func (q *Queries) DeleteWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
return err
}
const getEntityIDsByWikiIDs = `-- name: GetEntityIDsByWikiIDs :many
SELECT wiki_id, entity_id
FROM entity_wikis
WHERE wiki_id = ANY($1::uuid[])
`
type GetEntityIDsByWikiIDsRow struct {
WikiID pgtype.UUID `json:"wiki_id"`
EntityID pgtype.UUID `json:"entity_id"`
}
func (q *Queries) GetEntityIDsByWikiIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetEntityIDsByWikiIDsRow, error) {
rows, err := q.db.Query(ctx, getEntityIDsByWikiIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetEntityIDsByWikiIDsRow{}
for rows.Next() {
var i GetEntityIDsByWikiIDsRow
if err := rows.Scan(&i.WikiID, &i.EntityID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWikiById = `-- name: GetWikiById :one
SELECT id, project_id, title, slug, is_deleted, created_at, updated_at
FROM wikis
+61
View File
@@ -25,6 +25,7 @@ type EntityRepository interface {
Delete(ctx context.Context, id pgtype.UUID) error
DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.EntityEntity, error)
GetEntityIDsByWikiIDs(ctx context.Context, wikiIDs []string) (map[string][]string, error)
GetEntityIDsByGeometryIDs(ctx context.Context, geometryIDs []string) (map[string][]string, error)
WithTx(tx pgx.Tx) EntityRepository
}
@@ -473,3 +474,63 @@ func (r *entityRepository) GetEntityIDsByGeometryIDs(ctx context.Context, geomet
return result, nil
}
func (r *entityRepository) GetEntityIDsByWikiIDs(ctx context.Context, wikiIDs []string) (map[string][]string, error) {
if len(wikiIDs) == 0 {
return make(map[string][]string), nil
}
keys := make([]string, len(wikiIDs))
for i, id := range wikiIDs {
keys[i] = cache.Key("entity_wikis:wiki", id)
}
raws := r.c.MGet(ctx, keys...)
result := make(map[string][]string, len(wikiIDs))
missingWikiIDs := make([]string, 0, len(wikiIDs))
missingPgIDs := make([]pgtype.UUID, 0, len(wikiIDs))
for i, b := range raws {
if len(b) > 0 {
var entityIDs []string
if err := json.Unmarshal(b, &entityIDs); err == nil {
result[wikiIDs[i]] = entityIDs
continue
}
}
missingWikiIDs = append(missingWikiIDs, wikiIDs[i])
pgID, err := convert.StringToUUID(wikiIDs[i])
if err == nil {
missingPgIDs = append(missingPgIDs, pgID)
}
}
if len(missingPgIDs) > 0 {
rows, err := r.q.GetEntityIDsByWikiIDs(ctx, missingPgIDs)
if err != nil {
return nil, err
}
dbMap := make(map[string][]string, len(missingWikiIDs))
for _, id := range missingWikiIDs {
dbMap[id] = []string{}
}
for _, row := range rows {
wID := convert.UUIDToString(row.WikiID)
eID := convert.UUIDToString(row.EntityID)
dbMap[wID] = append(dbMap[wID], eID)
}
missingToCache := make(map[string]any, len(dbMap))
for wID, eIDs := range dbMap {
result[wID] = eIDs
missingToCache[cache.Key("entity_wikis:wiki", wID)] = eIDs
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
}
return result, nil
}
@@ -32,6 +32,7 @@ type GeometryRepository interface {
BulkDeleteEntityGeometriesByGeometryID(ctx context.Context, geometryID pgtype.UUID) error
DeleteEntityGeometry(ctx context.Context, entityID pgtype.UUID, geometryID pgtype.UUID) error
DeleteEntityGeometriesByProjectID(ctx context.Context, projectID pgtype.UUID) error
GetGeometryIDsByEntityIDs(ctx context.Context, entityIDs []string) (map[string][]string, error)
WithTx(tx pgx.Tx) GeometryRepository
}
@@ -565,3 +566,63 @@ func (r *geometryRepository) SearchByEntityName(ctx context.Context, params sqlc
return geometries, nil
}
func (r *geometryRepository) GetGeometryIDsByEntityIDs(ctx context.Context, entityIDs []string) (map[string][]string, error) {
if len(entityIDs) == 0 {
return make(map[string][]string), nil
}
keys := make([]string, len(entityIDs))
for i, id := range entityIDs {
keys[i] = cache.Key("entity_geometries:entity", id)
}
raws := r.c.MGet(ctx, keys...)
result := make(map[string][]string, len(entityIDs))
missingEntityIDs := make([]string, 0, len(entityIDs))
missingPgIDs := make([]pgtype.UUID, 0, len(entityIDs))
for i, b := range raws {
if len(b) > 0 {
var geometryIDs []string
if err := json.Unmarshal(b, &geometryIDs); err == nil {
result[entityIDs[i]] = geometryIDs
continue
}
}
missingEntityIDs = append(missingEntityIDs, entityIDs[i])
pgID, err := convert.StringToUUID(entityIDs[i])
if err == nil {
missingPgIDs = append(missingPgIDs, pgID)
}
}
if len(missingPgIDs) > 0 {
rows, err := r.q.GetGeometryIDsByEntityIDs(ctx, missingPgIDs)
if err != nil {
return nil, err
}
dbMap := make(map[string][]string, len(missingEntityIDs))
for _, id := range missingEntityIDs {
dbMap[id] = []string{}
}
for _, row := range rows {
eID := convert.UUIDToString(row.EntityID)
gID := convert.UUIDToString(row.GeometryID)
dbMap[eID] = append(dbMap[eID], gID)
}
missingToCache := make(map[string]any, len(dbMap))
for eID, gIDs := range dbMap {
result[eID] = gIDs
missingToCache[cache.Key("entity_geometries:entity", eID)] = gIDs
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
}
return result, nil
}
+8 -1
View File
@@ -6,9 +6,16 @@ import (
"github.com/gofiber/fiber/v3"
)
func RelationRoutes(router fiber.Router, wikiController *controllers.WikiController, entityController *controllers.EntityController) {
func RelationRoutes(
router fiber.Router,
wikiController *controllers.WikiController,
entityController *controllers.EntityController,
relationController *controllers.RelationController,
) {
relation := router.Group("/relations")
relation.Get("", relationController.GetRelations)
relation.Get("/wikis-by-entities", wikiController.GetWikisByEntityIDs)
relation.Get("/entities-by-geometries", entityController.GetEntitiesByGeometryIDs)
relation.Get("/wiki-contents/preview", wikiController.GetWikiContentsPreviewByIDs)
}
-188
View File
@@ -1,188 +0,0 @@
package services
import (
"context"
"errors"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/internal/repositories"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
// mockGeometryRepository implements repositories.GeometryRepository for unit testing
type mockGeometryRepository struct {
mockGeometries []*models.GeometryEntity
mockErr error
}
func (m *mockGeometryRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.GeometryEntity, error) {
if m.mockErr != nil {
return nil, m.mockErr
}
if len(m.mockGeometries) > 0 {
return m.mockGeometries[0], nil
}
return nil, errors.New("not found")
}
func (m *mockGeometryRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.GeometryEntity, error) {
return m.mockGeometries, m.mockErr
}
func (m *mockGeometryRepository) Search(ctx context.Context, params sqlc.SearchGeometriesParams) ([]*models.GeometryEntity, error) {
return m.mockGeometries, m.mockErr
}
func (m *mockGeometryRepository) SearchByEntityName(ctx context.Context, params sqlc.SearchGeometriesByEntityNameParams) ([]*models.EntityGeometriesSearchEntity, error) {
return nil, m.mockErr
}
func (m *mockGeometryRepository) Create(ctx context.Context, params sqlc.CreateGeometryParams) (*models.GeometryEntity, error) {
return nil, nil
}
func (m *mockGeometryRepository) Update(ctx context.Context, params sqlc.UpdateGeometryParams) (*models.GeometryEntity, error) {
return nil, nil
}
func (m *mockGeometryRepository) Delete(ctx context.Context, id pgtype.UUID) error {
return nil
}
func (m *mockGeometryRepository) CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error {
return nil
}
func (m *mockGeometryRepository) BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error {
return nil
}
func (m *mockGeometryRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.GeometryEntity, error) {
return m.mockGeometries, m.mockErr
}
func (m *mockGeometryRepository) GetGeometriesByBoundWith(ctx context.Context, boundWith pgtype.UUID) ([]*models.GeometryEntity, error) {
return m.mockGeometries, m.mockErr
}
func (m *mockGeometryRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error {
return nil
}
func (m *mockGeometryRepository) BulkDeleteEntityGeometriesByGeometryID(ctx context.Context, geometryID pgtype.UUID) error {
return nil
}
func (m *mockGeometryRepository) DeleteEntityGeometry(ctx context.Context, entityID pgtype.UUID, geometryID pgtype.UUID) error {
return nil
}
func (m *mockGeometryRepository) DeleteEntityGeometriesByProjectID(ctx context.Context, projectID pgtype.UUID) error {
return nil
}
func (m *mockGeometryRepository) WithTx(tx pgx.Tx) repositories.GeometryRepository {
return m
}
// Helper to float pointer
func floatPtr(f float64) *float64 {
return &f
}
// Helper to int pointer
func intPtr(i int32) *int32 {
return &i
}
func TestGetGeometryByID_InvalidID(t *testing.T) {
repo := &mockGeometryRepository{}
svc := NewGeometryService(repo)
_, fErr := svc.GetGeometryByID(context.Background(), "invalid-uuid-format")
if fErr == nil {
t.Fatal("Expected error for invalid UUID format, got nil")
}
if fErr.Code != 400 {
t.Errorf("Expected status 400, got %d", fErr.Code)
}
}
func TestSearchGeometries_NoBoundingBox(t *testing.T) {
repo := &mockGeometryRepository{}
svc := NewGeometryService(repo)
req := &request.SearchGeometryDto{
MinLng: nil, // Missing bounding box parameters
}
_, fErr := svc.SearchGeometries(context.Background(), req)
if fErr == nil {
t.Fatal("Expected error for missing bounding box, got nil")
}
if fErr.Code != 400 {
t.Errorf("Expected status 400, got %d", fErr.Code)
}
}
func TestSearchGeometries_InvalidBoundingBox(t *testing.T) {
repo := &mockGeometryRepository{}
svc := NewGeometryService(repo)
req := &request.SearchGeometryDto{
MinLng: floatPtr(105.0),
MinLat: floatPtr(21.0),
MaxLng: floatPtr(100.0), // MaxLng < MinLng (Invalid)
MaxLat: floatPtr(22.0),
}
_, fErr := svc.SearchGeometries(context.Background(), req)
if fErr == nil {
t.Fatal("Expected error for invalid bounding box coordinates, got nil")
}
if fErr.Code != 400 {
t.Errorf("Expected status 400, got %d", fErr.Code)
}
if fErr.Message != "Invalid bounding box coordinates" {
t.Errorf("Expected error message 'Invalid bounding box coordinates', got '%s'", fErr.Message)
}
}
func TestSearchGeometries_Valid(t *testing.T) {
mockGeometries := []*models.GeometryEntity{
{
ID: "87570494-0cfb-4e14-8789-7cfc0245037d",
GeoType: 3, // Polygon
Bbox: &response.Bbox{
MinLng: 102.0, MinLat: 8.0, MaxLng: 109.0, MaxLat: 23.0,
},
},
}
repo := &mockGeometryRepository{
mockGeometries: mockGeometries,
}
svc := NewGeometryService(repo)
req := &request.SearchGeometryDto{
MinLng: floatPtr(100.0),
MinLat: floatPtr(5.0),
MaxLng: floatPtr(110.0),
MaxLat: floatPtr(25.0),
}
res, fErr := svc.SearchGeometries(context.Background(), req)
if fErr != nil {
t.Fatalf("Expected no error, got %v", fErr)
}
if len(res) != 1 {
t.Fatalf("Expected 1 geometry response, got %d", len(res))
}
if res[0].ID != mockGeometries[0].ID {
t.Errorf("Expected ID %s, got %s", mockGeometries[0].ID, res[0].ID)
}
}
+240
View File
@@ -0,0 +1,240 @@
package services
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/models"
"history-api/internal/repositories"
"github.com/gofiber/fiber/v3"
)
type RelationService interface {
GetRelations(ctx context.Context, req *request.GetRelationsDto) (interface{}, *fiber.Error)
}
type relationService struct {
wikiRepo repositories.WikiRepository
entityRepo repositories.EntityRepository
geometryRepo repositories.GeometryRepository
}
func NewRelationService(
wikiRepo repositories.WikiRepository,
entityRepo repositories.EntityRepository,
geometryRepo repositories.GeometryRepository,
) RelationService {
return &relationService{
wikiRepo: wikiRepo,
entityRepo: entityRepo,
geometryRepo: geometryRepo,
}
}
func (s *relationService) GetRelations(ctx context.Context, req *request.GetRelationsDto) (interface{}, *fiber.Error) {
switch req.Type {
case "wiki-entity":
return s.getEntitiesByWikiIDs(ctx, req.IDs)
case "entity-wiki":
return s.getWikisByEntityIDs(ctx, req.IDs)
case "geometry-entity":
return s.getEntitiesByGeometryIDs(ctx, req.IDs)
case "entity-geometry":
return s.getGeometriesByEntityIDs(ctx, req.IDs)
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "Unsupported relation type")
}
}
func (s *relationService) getEntitiesByWikiIDs(ctx context.Context, wikiIDs []string) (map[string][]*response.EntityResponse, *fiber.Error) {
mapping, err := s.entityRepo.GetEntityIDsByWikiIDs(ctx, wikiIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch entity IDs by wiki IDs")
}
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 {
entityIDMap[eID] = struct{}{}
allEntityIDs = append(allEntityIDs, eID)
}
}
}
entities, err := s.entityRepo.GetByIDs(ctx, allEntityIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch entities")
}
entitiesByID := make(map[string]*models.EntityEntity, len(entities))
for _, e := range entities {
entitiesByID[e.ID] = e
}
result := make(map[string][]*response.EntityResponse, len(wikiIDs))
for _, idStr := range wikiIDs {
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())
}
}
}
}
return result, nil
}
func (s *relationService) getWikisByEntityIDs(ctx context.Context, entityIDs []string) (map[string][]*response.WikiResponse, *fiber.Error) {
mapping, err := s.wikiRepo.GetWikiIDsByEntityIDs(ctx, entityIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch wiki IDs by entity IDs")
}
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 {
wikiIDMap[wID] = struct{}{}
allWikiIDs = append(allWikiIDs, wID)
}
}
}
wikis, err := s.wikiRepo.GetByIDs(ctx, allWikiIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch wikis")
}
wikisByID := make(map[string]*models.WikiEntity, len(wikis))
for _, w := range wikis {
wikisByID[w.ID] = w
}
result := make(map[string][]*response.WikiResponse, len(entityIDs))
for _, idStr := range entityIDs {
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())
}
}
}
}
return result, nil
}
func (s *relationService) getEntitiesByGeometryIDs(ctx context.Context, geometryIDs []string) (map[string][]*response.EntityResponse, *fiber.Error) {
mapping, err := s.entityRepo.GetEntityIDsByGeometryIDs(ctx, geometryIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch entity IDs by geometry IDs")
}
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 {
entityIDMap[eID] = struct{}{}
allEntityIDs = append(allEntityIDs, eID)
}
}
}
entities, err := s.entityRepo.GetByIDs(ctx, allEntityIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch entities")
}
entitiesByID := make(map[string]*models.EntityEntity, len(entities))
for _, e := range entities {
entitiesByID[e.ID] = e
}
result := make(map[string][]*response.EntityResponse, len(geometryIDs))
for _, idStr := range geometryIDs {
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())
}
}
}
}
return result, nil
}
func (s *relationService) getGeometriesByEntityIDs(ctx context.Context, entityIDs []string) (map[string][]*response.GeometryResponse, *fiber.Error) {
mapping, err := s.geometryRepo.GetGeometryIDsByEntityIDs(ctx, entityIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch geometry IDs by entity IDs")
}
totalGeometryIDs := 0
for _, gIDs := range mapping {
totalGeometryIDs += len(gIDs)
}
geometryIDMap := make(map[string]struct{}, totalGeometryIDs)
allGeometryIDs := make([]string, 0, totalGeometryIDs)
for _, gIDs := range mapping {
for _, gID := range gIDs {
if _, ok := geometryIDMap[gID]; !ok {
geometryIDMap[gID] = struct{}{}
allGeometryIDs = append(allGeometryIDs, gID)
}
}
}
geometries, err := s.geometryRepo.GetByIDs(ctx, allGeometryIDs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch geometries")
}
geometriesByID := make(map[string]*models.GeometryEntity, len(geometries))
for _, g := range geometries {
geometriesByID[g.ID] = g
}
result := make(map[string][]*response.GeometryResponse, len(entityIDs))
for _, idStr := range entityIDs {
gIDs, exists := mapping[idStr]
result[idStr] = make([]*response.GeometryResponse, 0, len(gIDs))
if exists {
for _, gID := range gIDs {
if g, found := geometriesByID[gID]; found {
result[idStr] = append(result[idStr], g.ToResponse())
}
}
}
}
return result, nil
}