diff --git a/cmd/api/server.go b/cmd/api/server.go index 3c78e56..ce6089c 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -159,6 +159,7 @@ 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.ProjectRoutes(s.App, projectController, commitController, userRepo) routes.SubmissionRoutes(s.App, submissionController, userRepo) routes.ChatbotRoutes(s.App, chatbotController, userRepo) diff --git a/db/query/entities.sql b/db/query/entities.sql index 82a38d4..1ba932d 100644 --- a/db/query/entities.sql +++ b/db/query/entities.sql @@ -67,4 +67,9 @@ FROM entities WHERE slug = $1 AND is_deleted = false; -- name: GetEntitiesBySlugs :many -SELECT * FROM entities WHERE slug = ANY($1::text[]) AND is_deleted = false; \ No newline at end of file +SELECT * FROM entities WHERE slug = ANY($1::text[]) AND is_deleted = false; + +-- name: GetEntityIDsByGeometryIDs :many +SELECT geometry_id, entity_id +FROM entity_geometries +WHERE geometry_id = ANY($1::uuid[]); \ No newline at end of file diff --git a/db/query/wiki.sql b/db/query/wiki.sql index dd56f6d..dd82b9f 100644 --- a/db/query/wiki.sql +++ b/db/query/wiki.sql @@ -127,3 +127,8 @@ SELECT id, wiki_id, title, created_at FROM wiki_content WHERE wiki_id = ANY($1::uuid[]) AND is_deleted = false ORDER BY created_at DESC; + +-- name: GetWikiIDsByEntityIDs :many +SELECT entity_id, wiki_id +FROM entity_wikis +WHERE entity_id = ANY($1::uuid[]); diff --git a/internal/controllers/entityController.go b/internal/controllers/entityController.go index a662b0a..b51f45e 100644 --- a/internal/controllers/entityController.go +++ b/internal/controllers/entityController.go @@ -136,3 +136,40 @@ func (h *EntityController) SearchEntities(c fiber.Ctx) error { }) } +// GetEntitiesByGeometryIDs handles fetching entities by a list of geometry IDs. +// @Summary Get entities by geometry IDs +// @Description Get entities grouped by geometry IDs +// @Tags Relations +// @Accept json +// @Produce json +// @Param query query request.GetEntitiesByGeometryIDsDto true "Query Parameters" +// @Success 200 {object} response.CommonResponse +// @Failure 400 {object} response.CommonResponse +// @Failure 500 {object} response.CommonResponse +// @Router /relations/entities-by-geometries [get] +func (h *EntityController) GetEntitiesByGeometryIDs(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + dto := &request.GetEntitiesByGeometryIDsDto{} + if err := validator.ValidateQueryDto(c, dto); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{ + Status: false, + Errors: err, + }) + } + + res, err := h.service.GetEntitiesByGeometryIDs(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, + }) +} + diff --git a/internal/controllers/wikiController.go b/internal/controllers/wikiController.go index 65dde24..e53e1bc 100644 --- a/internal/controllers/wikiController.go +++ b/internal/controllers/wikiController.go @@ -163,3 +163,76 @@ func (h *WikiController) GetWikiContentById(c fiber.Ctx) error { }) } +// GetWikisByEntityIDs handles fetching wikis by a list of entity IDs. +// @Summary Get wikis by entity IDs +// @Description Get wikis grouped by entity IDs +// @Tags Relations +// @Accept json +// @Produce json +// @Param query query request.GetWikisByEntityIDsDto true "Query Parameters" +// @Success 200 {object} response.CommonResponse +// @Failure 400 {object} response.CommonResponse +// @Failure 500 {object} response.CommonResponse +// @Router /relations/wikis-by-entities [get] +func (h *WikiController) GetWikisByEntityIDs(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + dto := &request.GetWikisByEntityIDsDto{} + if err := validator.ValidateQueryDto(c, dto); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{ + Status: false, + Errors: err, + }) + } + + res, err := h.service.GetWikisByEntityIDs(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, + }) +} + +// GetWikiContentsPreviewByIDs handles fetching wiki content previews by a list of IDs. +// @Summary Get wiki content previews by IDs +// @Description Get previews of specific wiki contents by a list of their IDs +// @Tags Relations +// @Accept json +// @Produce json +// @Param query query request.GetWikiContentsPreviewDto true "Query Parameters" +// @Success 200 {object} response.CommonResponse +// @Failure 400 {object} response.CommonResponse +// @Failure 500 {object} response.CommonResponse +// @Router /relations/wiki-contents/preview [get] +func (h *WikiController) GetWikiContentsPreviewByIDs(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + dto := &request.GetWikiContentsPreviewDto{} + if err := validator.ValidateQueryDto(c, dto); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{ + Status: false, + Errors: err, + }) + } + + res, err := h.service.GetWikiContentsPreviewByIDs(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, + }) +} diff --git a/internal/dtos/request/relation.go b/internal/dtos/request/relation.go new file mode 100644 index 0000000..6e57160 --- /dev/null +++ b/internal/dtos/request/relation.go @@ -0,0 +1,13 @@ +package request + +type GetWikisByEntityIDsDto struct { + EntityIDs []string `json:"entity_ids" query:"entity_ids" validate:"required,min=1,dive,uuid"` +} + +type GetEntitiesByGeometryIDsDto struct { + GeometryIDs []string `json:"geometry_ids" query:"geometry_ids" validate:"required,min=1,dive,uuid"` +} + +type GetWikiContentsPreviewDto struct { + IDs []string `json:"ids" query:"ids" validate:"required,min=1,dive,uuid"` +} diff --git a/internal/dtos/response/wiki.go b/internal/dtos/response/wiki.go index 000f946..2e2bbc5 100644 --- a/internal/dtos/response/wiki.go +++ b/internal/dtos/response/wiki.go @@ -29,3 +29,9 @@ type WikiContentResponse struct { Preview string `json:"preview"` CreatedAt *time.Time `json:"created_at"` } + +type WikiContentPreviewResponse struct { + ID string `json:"id"` + Preview string `json:"preview"` + CreatedAt *time.Time `json:"created_at"` +} diff --git a/internal/gen/sqlc/entities.sql.go b/internal/gen/sqlc/entities.sql.go index 2328096..6e1ac3e 100644 --- a/internal/gen/sqlc/entities.sql.go +++ b/internal/gen/sqlc/entities.sql.go @@ -242,6 +242,37 @@ func (q *Queries) GetEntityBySlug(ctx context.Context, slug pgtype.Text) (Entity return i, err } +const getEntityIDsByGeometryIDs = `-- name: GetEntityIDsByGeometryIDs :many +SELECT geometry_id, entity_id +FROM entity_geometries +WHERE geometry_id = ANY($1::uuid[]) +` + +type GetEntityIDsByGeometryIDsRow struct { + GeometryID pgtype.UUID `json:"geometry_id"` + EntityID pgtype.UUID `json:"entity_id"` +} + +func (q *Queries) GetEntityIDsByGeometryIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetEntityIDsByGeometryIDsRow, error) { + rows, err := q.db.Query(ctx, getEntityIDsByGeometryIDs, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetEntityIDsByGeometryIDsRow{} + for rows.Next() { + var i GetEntityIDsByGeometryIDsRow + if err := rows.Scan(&i.GeometryID, &i.EntityID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const searchEntities = `-- name: SearchEntities :many SELECT id, project_id, name, slug, description, status, time_start, time_end, is_deleted, created_at, updated_at FROM entities diff --git a/internal/gen/sqlc/wiki.sql.go b/internal/gen/sqlc/wiki.sql.go index f9e82ef..478176b 100644 --- a/internal/gen/sqlc/wiki.sql.go +++ b/internal/gen/sqlc/wiki.sql.go @@ -370,6 +370,37 @@ func (q *Queries) GetWikiContentCount(ctx context.Context, wikiID pgtype.UUID) ( return count, err } +const getWikiIDsByEntityIDs = `-- name: GetWikiIDsByEntityIDs :many +SELECT entity_id, wiki_id +FROM entity_wikis +WHERE entity_id = ANY($1::uuid[]) +` + +type GetWikiIDsByEntityIDsRow struct { + EntityID pgtype.UUID `json:"entity_id"` + WikiID pgtype.UUID `json:"wiki_id"` +} + +func (q *Queries) GetWikiIDsByEntityIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetWikiIDsByEntityIDsRow, error) { + rows, err := q.db.Query(ctx, getWikiIDsByEntityIDs, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetWikiIDsByEntityIDsRow{} + for rows.Next() { + var i GetWikiIDsByEntityIDsRow + if err := rows.Scan(&i.EntityID, &i.WikiID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWikisByIDs = `-- name: GetWikisByIDs :many SELECT id, project_id, title, slug, is_deleted, created_at, updated_at FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false ` diff --git a/internal/repositories/entityRepository.go b/internal/repositories/entityRepository.go index 5b8a39f..3f13ca9 100644 --- a/internal/repositories/entityRepository.go +++ b/internal/repositories/entityRepository.go @@ -27,6 +27,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) + GetEntityIDsByGeometryIDs(ctx context.Context, geometryIDs []string) (map[string][]string, error) WithTx(tx pgx.Tx) EntityRepository } @@ -412,3 +413,62 @@ func (r *entityRepository) GetBySlugs(ctx context.Context, slugs []string) ([]*m return entities, nil } + +func (r *entityRepository) GetEntityIDsByGeometryIDs(ctx context.Context, geometryIDs []string) (map[string][]string, error) { + if len(geometryIDs) == 0 { + return make(map[string][]string), nil + } + + keys := make([]string, len(geometryIDs)) + for i, id := range geometryIDs { + keys[i] = fmt.Sprintf("entity_geometries:geometry:%s", id) + } + + raws := r.c.MGet(ctx, keys...) + result := make(map[string][]string) + var missingGeometryIDs []string + var missingPgIDs []pgtype.UUID + + for i, b := range raws { + if len(b) > 0 { + var entityIDs []string + if err := json.Unmarshal(b, &entityIDs); err == nil { + result[geometryIDs[i]] = entityIDs + continue + } + } + missingGeometryIDs = append(missingGeometryIDs, geometryIDs[i]) + pgID, err := convert.StringToUUID(geometryIDs[i]) + if err == nil { + missingPgIDs = append(missingPgIDs, pgID) + } + } + + if len(missingPgIDs) > 0 { + rows, err := r.q.GetEntityIDsByGeometryIDs(ctx, missingPgIDs) + if err != nil { + return nil, err + } + + dbMap := make(map[string][]string) + for _, id := range missingGeometryIDs { + dbMap[id] = []string{} + } + for _, row := range rows { + gID := convert.UUIDToString(row.GeometryID) + eID := convert.UUIDToString(row.EntityID) + dbMap[gID] = append(dbMap[gID], eID) + } + + missingToCache := make(map[string]any) + for gID, eIDs := range dbMap { + result[gID] = eIDs + missingToCache[fmt.Sprintf("entity_geometries:geometry:%s", gID)] = eIDs + } + if len(missingToCache) > 0 { + _ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration) + } + } + + return result, nil +} diff --git a/internal/repositories/wikiRepository.go b/internal/repositories/wikiRepository.go index 7c4282f..71c48de 100644 --- a/internal/repositories/wikiRepository.go +++ b/internal/repositories/wikiRepository.go @@ -36,6 +36,7 @@ type WikiRepository interface { GetContentCountByWikiID(ctx context.Context, wikiID pgtype.UUID) (int64, error) GetContentByID(ctx context.Context, id pgtype.UUID) (*models.WikiContentEntity, error) GetContentByIDs(ctx context.Context, ids []string) ([]*models.WikiContentEntity, error) + GetWikiIDsByEntityIDs(ctx context.Context, entityIDs []string) (map[string][]string, error) WithTx(tx pgx.Tx) WikiRepository } @@ -638,3 +639,62 @@ func (r *wikiRepository) GetContentByID(ctx context.Context, id pgtype.UUID) (*m func (r *wikiRepository) GetContentByIDs(ctx context.Context, ids []string) ([]*models.WikiContentEntity, error) { return r.getContentByIDsWithFallback(ctx, ids) } + +func (r *wikiRepository) GetWikiIDsByEntityIDs(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] = fmt.Sprintf("entity_wikis:entity:%s", id) + } + + raws := r.c.MGet(ctx, keys...) + result := make(map[string][]string) + var missingEntityIDs []string + var missingPgIDs []pgtype.UUID + + for i, b := range raws { + if len(b) > 0 { + var wikiIDs []string + if err := json.Unmarshal(b, &wikiIDs); err == nil { + result[entityIDs[i]] = wikiIDs + 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.GetWikiIDsByEntityIDs(ctx, missingPgIDs) + if err != nil { + return nil, err + } + + dbMap := make(map[string][]string) + for _, id := range missingEntityIDs { + dbMap[id] = []string{} + } + for _, row := range rows { + eID := convert.UUIDToString(row.EntityID) + wID := convert.UUIDToString(row.WikiID) + dbMap[eID] = append(dbMap[eID], wID) + } + + missingToCache := make(map[string]any) + for eID, wIDs := range dbMap { + result[eID] = wIDs + missingToCache[fmt.Sprintf("entity_wikis:entity:%s", eID)] = wIDs + } + if len(missingToCache) > 0 { + _ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration) + } + } + + return result, nil +} diff --git a/internal/routes/relationRoute.go b/internal/routes/relationRoute.go new file mode 100644 index 0000000..8c25638 --- /dev/null +++ b/internal/routes/relationRoute.go @@ -0,0 +1,14 @@ +package routes + +import ( + "history-api/internal/controllers" + + "github.com/gofiber/fiber/v3" +) + +func RelationRoutes(router fiber.Router, wikiController *controllers.WikiController, entityController *controllers.EntityController) { + relation := router.Group("/relations") + relation.Get("/wikis-by-entities", wikiController.GetWikisByEntityIDs) + relation.Get("/entities-by-geometries", entityController.GetEntitiesByGeometryIDs) + relation.Get("/wiki-contents/preview", wikiController.GetWikiContentsPreviewByIDs) +} diff --git a/internal/routes/wikiRoute.go b/internal/routes/wikiRoute.go index cf03a11..007fd99 100644 --- a/internal/routes/wikiRoute.go +++ b/internal/routes/wikiRoute.go @@ -14,4 +14,3 @@ func WikiRoutes(router fiber.Router, wikiController *controllers.WikiController) wiki.Get("/content/:id", wikiController.GetWikiContentById) wiki.Get("/:id", wikiController.GetWikiById) } - diff --git a/internal/services/entityService.go b/internal/services/entityService.go index ac3d03d..f9e45ce 100644 --- a/internal/services/entityService.go +++ b/internal/services/entityService.go @@ -19,6 +19,7 @@ type EntityService interface { GetEntityBySlug(ctx context.Context, slug string) (*response.EntityResponse, *fiber.Error) IsExistEntitySlug(ctx context.Context, slug string) (bool, *fiber.Error) SearchEntities(ctx context.Context, req *request.SearchEntityDto) ([]*response.EntityResponse, *fiber.Error) + GetEntitiesByGeometryIDs(ctx context.Context, req *request.GetEntitiesByGeometryIDsDto) (map[string][]*response.EntityResponse, *fiber.Error) } type entityService struct { @@ -101,3 +102,45 @@ func (s *entityService) SearchEntities(ctx context.Context, req *request.SearchE return models.EntitiesEntityToResponse(entities), nil } + +func (s *entityService) GetEntitiesByGeometryIDs(ctx context.Context, req *request.GetEntitiesByGeometryIDsDto) (map[string][]*response.EntityResponse, *fiber.Error) { + mapping, err := s.entityRepo.GetEntityIDsByGeometryIDs(ctx, req.GeometryIDs) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch entity IDs by geometry IDs") + } + + entityIDMap := make(map[string]struct{}) + var allEntityIDs []string + 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) + for _, e := range entities { + entitiesByID[e.ID] = e + } + + result := make(map[string][]*response.EntityResponse) + for _, idStr := range req.GeometryIDs { + result[idStr] = make([]*response.EntityResponse, 0) + if eIDs, exists := mapping[idStr]; exists { + for _, eID := range eIDs { + if e, found := entitiesByID[eID]; found { + result[idStr] = append(result[idStr], e.ToResponse()) + } + } + } + } + + return result, nil +} diff --git a/internal/services/wikiService.go b/internal/services/wikiService.go index 4e859e6..d64feb8 100644 --- a/internal/services/wikiService.go +++ b/internal/services/wikiService.go @@ -20,6 +20,8 @@ type WikiService interface { IsExistWikiSlug(ctx context.Context, slug string) (bool, *fiber.Error) SearchWikis(ctx context.Context, req *request.SearchWikiDto) ([]*response.WikiResponse, *fiber.Error) GetWikiContentByID(ctx context.Context, id string) (*response.WikiContentResponse, *fiber.Error) + GetWikisByEntityIDs(ctx context.Context, req *request.GetWikisByEntityIDsDto) (map[string][]*response.WikiResponse, *fiber.Error) + GetWikiContentsPreviewByIDs(ctx context.Context, req *request.GetWikiContentsPreviewDto) ([]*response.WikiContentPreviewResponse, *fiber.Error) } type wikiService struct { @@ -128,3 +130,63 @@ func (s *wikiService) GetWikiContentByID(ctx context.Context, id string) (*respo CreatedAt: content.CreatedAt, }, nil } + +func (s *wikiService) GetWikisByEntityIDs(ctx context.Context, req *request.GetWikisByEntityIDsDto) (map[string][]*response.WikiResponse, *fiber.Error) { + mapping, err := s.wikiRepo.GetWikiIDsByEntityIDs(ctx, req.EntityIDs) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch wiki IDs by entity IDs") + } + + wikiIDMap := make(map[string]struct{}) + var allWikiIDs []string + 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) + for _, w := range wikis { + wikisByID[w.ID] = w + } + + result := make(map[string][]*response.WikiResponse) + for _, idStr := range req.EntityIDs { + result[idStr] = make([]*response.WikiResponse, 0) + if wIDs, exists := mapping[idStr]; exists { + for _, wID := range wIDs { + if w, found := wikisByID[wID]; found { + result[idStr] = append(result[idStr], w.ToResponse()) + } + } + } + } + + return result, nil +} + +func (s *wikiService) GetWikiContentsPreviewByIDs(ctx context.Context, req *request.GetWikiContentsPreviewDto) ([]*response.WikiContentPreviewResponse, *fiber.Error) { + contents, err := s.wikiRepo.GetContentByIDs(ctx, req.IDs) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch wiki contents") + } + + var results []*response.WikiContentPreviewResponse + for _, c := range contents { + results = append(results, &response.WikiContentPreviewResponse{ + ID: c.ID, + Preview: c.Preview, + CreatedAt: c.CreatedAt, + }) + } + + return results, nil +}