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:
+4
-1
@@ -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)
|
||||
|
||||
@@ -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[]);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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