feat: implement relation management system with controllers, services, repositories, and corresponding API documentation.
Build and Release / release (push) Successful in 1m47s
Build and Release / release (push) Successful in 1m47s
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user