UPDATE: Entity, Geo, Wiki
Some checks failed
Build and Release / release (push) Failing after 1m7s

This commit is contained in:
2026-04-22 17:45:09 +07:00
parent f127e2f029
commit adb65d8292
50 changed files with 3354 additions and 119 deletions

View File

@@ -0,0 +1,83 @@
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 EntityController struct {
service services.EntityService
}
func NewEntityController(svc services.EntityService) *EntityController {
return &EntityController{service: svc}
}
// GetEntityById handles fetching a single entity by ID.
// @Summary Get entity by ID
// @Description Get detailed information about a specific entity
// @Tags Entities
// @Accept json
// @Produce json
// @Param id path string true "Entity ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /entities/{id} [get]
func (h *EntityController) GetEntityById(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
res, err := h.service.GetEntityByID(ctx, id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// SearchEntities handles searching for entities.
// @Summary Search entities
// @Description Search entities with cursor pagination
// @Tags Entities
// @Accept json
// @Produce json
// @Param query query request.SearchEntityDto false "Search Query"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /entities [get]
func (h *EntityController) SearchEntities(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.SearchEntityDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.service.SearchEntities(ctx, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}

View File

@@ -0,0 +1,83 @@
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 GeometryController struct {
service services.GeometryService
}
func NewGeometryController(svc services.GeometryService) *GeometryController {
return &GeometryController{service: svc}
}
// GetGeometryById handles fetching a single geometry by ID.
// @Summary Get geometry by ID
// @Description Get detailed information about a specific geometry
// @Tags Geometries
// @Accept json
// @Produce json
// @Param id path string true "Geometry ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /geometries/{id} [get]
func (h *GeometryController) GetGeometryById(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
res, err := h.service.GetGeometryByID(ctx, id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// SearchGeometries handles searching for geometries.
// @Summary Search geometries
// @Description Search geometries with cursor pagination and spatial filtering
// @Tags Geometries
// @Accept json
// @Produce json
// @Param query query request.SearchGeometryDto false "Search Query"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /geometries [get]
func (h *GeometryController) SearchGeometries(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.SearchGeometryDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.service.SearchGeometries(ctx, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}

View File

@@ -0,0 +1,83 @@
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 WikiController struct {
service services.WikiService
}
func NewWikiController(svc services.WikiService) *WikiController {
return &WikiController{service: svc}
}
// GetWikiById handles fetching a single wiki by ID.
// @Summary Get wiki by ID
// @Description Get detailed information about a specific wiki
// @Tags Wikis
// @Accept json
// @Produce json
// @Param id path string true "Wiki ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /wikis/{id} [get]
func (h *WikiController) GetWikiById(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
res, err := h.service.GetWikiByID(ctx, id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}
// SearchWikis handles searching for wikis.
// @Summary Search wikis
// @Description Search wikis with cursor pagination
// @Tags Wikis
// @Accept json
// @Produce json
// @Param query query request.SearchWikiDto false "Search Query"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /wikis [get]
func (h *WikiController) SearchWikis(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dto := &request.SearchWikiDto{}
if err := validator.ValidateQueryDto(c, dto); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
Status: false,
Errors: err,
})
}
res, err := h.service.SearchWikis(ctx, dto)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
Status: false,
Message: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
Status: true,
Data: res,
})
}

View File

@@ -0,0 +1,7 @@
package request
type SearchEntityDto struct {
Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
Name string `json:"name" query:"name" validate:"omitempty,max=255"`
}

View File

@@ -0,0 +1,10 @@
package request
type SearchGeometryDto struct {
MinLng *float64 `query:"min_lng" validate:"required,gte=-180,lte=180"`
MinLat *float64 `query:"min_lat" validate:"required,gte=-90,lte=90"`
MaxLng *float64 `query:"max_lng" validate:"required,gte=-180,lte=180"`
MaxLat *float64 `query:"max_lat" validate:"required,gte=-90,lte=90"`
TimePoint *int32 `json:"time" query:"time" validate:"omitempty,number"`
EntityID *string `json:"entity_id" query:"entity_id" validate:"omitempty,uuid"`
}

View File

@@ -5,13 +5,14 @@ import "time"
type UpdateProfileDto struct {
DisplayName *string `json:"display_name" validate:"omitempty,min=2,max=50"`
FullName *string `json:"full_name" validate:"omitempty,min=2,max=100"`
AvatarUrl *string `json:"avatar_url" validate:"omitempty,url,image_url"`
AvatarUrl *string `json:"avatar_url" validate:"omitempty,image_url"`
Bio *string `json:"bio" validate:"omitempty,max=255"`
Location *string `json:"location" validate:"omitempty,max=100"`
Website *string `json:"website" validate:"omitempty,url"`
Website *string `json:"website" validate:"omitempty,optional_url"`
CountryCode *string `json:"country_code" validate:"omitempty,len=2"`
Phone *string `json:"phone" validate:"omitempty,min=8,max=20"`
}
type ChangePasswordDto struct {
OldPassword string `json:"old_password" validate:"required,min=8,max=64"`
NewPassword string `json:"new_password" validate:"required,min=8,max=64,nefield=OldPassword"`
@@ -26,6 +27,7 @@ type PaginationDto struct {
Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
Order string `json:"order" query:"order" validate:"omitempty,oneof=asc desc"`
}
type SearchUserDto struct {
PaginationDto
Sort string `json:"sort" query:"sort" validate:"omitempty,oneof=id created_at updated_at email is_deleted auth_provider"`

View File

@@ -0,0 +1,8 @@
package request
type SearchWikiDto struct {
Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
Title string `json:"title" query:"title" validate:"omitempty,max=1000"`
EntityID string `json:"entity_id" query:"entity_id" validate:"omitempty,uuid"`
}

View File

@@ -1,8 +1,8 @@
package response
type AuthResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
type VerifyTokenResponse struct {

View File

@@ -8,9 +8,9 @@ import (
type CommonResponse struct {
Status bool `json:"status"`
Data any `json:"data"`
Errors any `json:"errors"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
Errors any `json:"errors,omitempty"`
Message string `json:"message,omitempty"`
}
type JWTClaims struct {
@@ -29,10 +29,10 @@ type PaginationMeta struct {
type PaginatedResponse struct {
Status bool `json:"status"`
Message string `json:"message"`
Data any `json:"data"`
Errors any `json:"errors"`
Pagination *PaginationMeta `json:"pagination"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
Errors any `json:"errors,omitempty"`
Pagination *PaginationMeta `json:"pagination,omitempty"`
}
func BuildPaginatedResponse(data any, totalRecords int64, page int, limit int) *PaginatedResponse {

View File

@@ -0,0 +1,13 @@
package response
import "time"
type EntityResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
ThumbnailUrl string `json:"thumbnail_url,omitempty"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}

View File

@@ -0,0 +1,26 @@
package response
import (
"encoding/json"
"time"
)
type Bbox struct {
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
}
type GeometryResponse struct {
ID string `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding json.RawMessage `json:"binding,omitempty"`
TimeStart int32 `json:"time_start,omitempty"`
TimeEnd int32 `json:"time_end,omitempty"`
Bbox *Bbox `json:"bbox,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

View File

@@ -20,8 +20,8 @@ type MediaResponse struct {
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
FileMetadata json.RawMessage `json:"file_metadata"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
type MediaSimpleResponse struct {
@@ -31,5 +31,5 @@ type MediaSimpleResponse struct {
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
FileMetadata json.RawMessage `json:"file_metadata"`
CreatedAt *time.Time `json:"created_at"`
CreatedAt *time.Time `json:"created_at,omitempty"`
}

View File

@@ -10,7 +10,7 @@ type RoleSimpleResponse struct {
type RoleResponse struct {
ID string `json:"id"`
Name string `json:"name"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

View File

@@ -5,29 +5,29 @@ import "time"
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Profile *UserProfileSimpleResponse `json:"profile"`
TokenVersion int32 `json:"token_version"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
Roles []*RoleSimpleResponse `json:"roles"`
Profile *UserProfileSimpleResponse `json:"profile,omitempty"`
TokenVersion int32 `json:"token_version,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Roles []*RoleSimpleResponse `json:"roles,omitempty"`
}
type UserSimpleResponse struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name"`
FullName string `json:"full_name"`
AvatarUrl string `json:"avatar_url"`
DisplayName string `json:"display_name,omitempty"`
FullName string `json:"full_name,omitempty"`
AvatarUrl string `json:"avatar_url,omitempty"`
}
type UserProfileSimpleResponse struct {
DisplayName string `json:"display_name"`
FullName string `json:"full_name"`
AvatarUrl string `json:"avatar_url"`
Bio string `json:"bio"`
Location string `json:"location"`
Website string `json:"website"`
CountryCode string `json:"country_code"`
Phone string `json:"phone"`
FullName string `json:"full_name,omitempty"`
AvatarUrl string `json:"avatar_url,omitempty"`
Bio string `json:"bio,omitempty"`
Location string `json:"location,omitempty"`
Website string `json:"website,omitempty"`
CountryCode string `json:"country_code,omitempty"`
Phone string `json:"phone,omitempty"`
}

View File

@@ -4,13 +4,13 @@ import "time"
type UserVerificationResponse struct {
ID string `json:"id"`
User *UserSimpleResponse `json:"user"`
User *UserSimpleResponse `json:"user,omitempty"`
VerifyType string `json:"verify_type"`
Content string `json:"content"`
Status string `json:"status"`
Reviewer *UserSimpleResponse `json:"reviewer"`
ReviewNote string `json:"review_note"`
ReviewedAt *time.Time `json:"reviewed_at"`
CreatedAt *time.Time `json:"created_at"`
Medias []*MediaSimpleResponse `json:"media"`
Reviewer *UserSimpleResponse `json:"reviewer,omitempty"`
ReviewNote string `json:"review_note,omitempty"`
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
Medias []*MediaSimpleResponse `json:"media,omitempty"`
}

View File

@@ -0,0 +1,12 @@
package response
import "time"
type WikiResponse struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

View File

@@ -0,0 +1,156 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: entities.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createEntity = `-- name: CreateEntity :one
INSERT INTO entities (
name, description, thumbnail_url
) VALUES (
$1, $2, $3
)
RETURNING id, name, description, thumbnail_url, is_deleted, created_at, updated_at
`
type CreateEntityParams struct {
Name string `json:"name"`
Description pgtype.Text `json:"description"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"`
}
func (q *Queries) CreateEntity(ctx context.Context, arg CreateEntityParams) (Entity, error) {
row := q.db.QueryRow(ctx, createEntity, arg.Name, arg.Description, arg.ThumbnailUrl)
var i Entity
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.ThumbnailUrl,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteEntity = `-- name: DeleteEntity :exec
UPDATE entities
SET
is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteEntity(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteEntity, id)
return err
}
const getEntityById = `-- name: GetEntityById :one
SELECT id, name, description, thumbnail_url, is_deleted, created_at, updated_at
FROM entities
WHERE id = $1 AND is_deleted = false
`
func (q *Queries) GetEntityById(ctx context.Context, id pgtype.UUID) (Entity, error) {
row := q.db.QueryRow(ctx, getEntityById, id)
var i Entity
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.ThumbnailUrl,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const searchEntities = `-- name: SearchEntities :many
SELECT id, name, description, thumbnail_url, is_deleted, created_at, updated_at
FROM entities
WHERE is_deleted = false
AND name ILIKE '%' || $1::text || '%'
AND ($2::uuid IS NULL OR id < $2::uuid)
ORDER BY id DESC
LIMIT $3
`
type SearchEntitiesParams struct {
Name string `json:"name"`
CursorID pgtype.UUID `json:"cursor_id"`
LimitCount int32 `json:"limit_count"`
}
func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams) ([]Entity, error) {
rows, err := q.db.Query(ctx, searchEntities, arg.Name, arg.CursorID, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Entity{}
for rows.Next() {
var i Entity
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.ThumbnailUrl,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateEntity = `-- name: UpdateEntity :one
UPDATE entities
SET
name = COALESCE($1, name),
description = COALESCE($2, description),
thumbnail_url = COALESCE($3, thumbnail_url)
WHERE id = $4 AND is_deleted = false
RETURNING id, name, description, thumbnail_url, is_deleted, created_at, updated_at
`
type UpdateEntityParams struct {
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) UpdateEntity(ctx context.Context, arg UpdateEntityParams) (Entity, error) {
row := q.db.QueryRow(ctx, updateEntity,
arg.Name,
arg.Description,
arg.ThumbnailUrl,
arg.ID,
)
var i Entity
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.ThumbnailUrl,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@@ -0,0 +1,371 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: geometries.sql
package sqlc
import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
const bulkDeleteEntityGeometriesByEntityId = `-- name: BulkDeleteEntityGeometriesByEntityId :many
DELETE FROM entity_geometries
WHERE entity_id = $1
RETURNING geometry_id
`
func (q *Queries) BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityID pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, bulkDeleteEntityGeometriesByEntityId, entityID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []pgtype.UUID{}
for rows.Next() {
var geometry_id pgtype.UUID
if err := rows.Scan(&geometry_id); err != nil {
return nil, err
}
items = append(items, geometry_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const createEntityGeometries = `-- name: CreateEntityGeometries :exec
INSERT INTO entity_geometries (
entity_id, geometry_id
)
SELECT $1, unnest($2::uuid[])
`
type CreateEntityGeometriesParams struct {
EntityID pgtype.UUID `json:"entity_id"`
GeometryIds []pgtype.UUID `json:"geometry_ids"`
}
func (q *Queries) CreateEntityGeometries(ctx context.Context, arg CreateEntityGeometriesParams) error {
_, err := q.db.Exec(ctx, createEntityGeometries, arg.EntityID, arg.GeometryIds)
return err
}
const createGeometry = `-- name: CreateGeometry :one
INSERT INTO geometries (
geo_type, draw_geometry, binding, time_start, time_end, bbox
) VALUES (
$1, $2, $3, $4, $5, ST_MakeEnvelope($6::float8, $7::float8, $8::float8, $9::float8, 4326)
)
RETURNING id, geo_type, draw_geometry, binding, time_start, time_end,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at
`
type CreateGeometryParams struct {
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
}
type CreateGeometryRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) CreateGeometry(ctx context.Context, arg CreateGeometryParams) (CreateGeometryRow, error) {
row := q.db.QueryRow(ctx, createGeometry,
arg.GeoType,
arg.DrawGeometry,
arg.Binding,
arg.TimeStart,
arg.TimeEnd,
arg.MinLng,
arg.MinLat,
arg.MaxLng,
arg.MaxLat,
)
var i CreateGeometryRow
err := row.Scan(
&i.ID,
&i.GeoType,
&i.DrawGeometry,
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
&i.MaxLat,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteGeometry = `-- name: DeleteGeometry :exec
UPDATE geometries
SET
is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteGeometry(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteGeometry, id)
return err
}
const getGeometryById = `-- name: GetGeometryById :one
SELECT id, geo_type, draw_geometry, binding, time_start, time_end,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at
FROM geometries
WHERE id = $1 AND is_deleted = false
`
type GetGeometryByIdRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetGeometryById(ctx context.Context, id pgtype.UUID) (GetGeometryByIdRow, error) {
row := q.db.QueryRow(ctx, getGeometryById, id)
var i GetGeometryByIdRow
err := row.Scan(
&i.ID,
&i.GeoType,
&i.DrawGeometry,
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
&i.MaxLat,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const searchGeometries = `-- name: SearchGeometries :many
SELECT
g.id, g.geo_type, g.draw_geometry, g.binding, g.time_start, g.time_end,
ST_XMin(g.bbox)::float8 as min_lng,
ST_YMin(g.bbox)::float8 as min_lat,
ST_XMax(g.bbox)::float8 as max_lng,
ST_YMax(g.bbox)::float8 as max_lat,
g.is_deleted, g.created_at, g.updated_at
FROM geometries g
WHERE g.is_deleted = false
AND (
$1::float8 IS NULL OR
$2::float8 IS NULL OR
$3::float8 IS NULL OR
$4::float8 IS NULL OR
g.bbox && ST_MakeEnvelope(
$1::float8,
$2::float8,
$3::float8,
$4::float8,
4326
)
)
AND (
$5::int IS NULL OR
(g.time_start <= $5::int AND g.time_end >= $5::int)
)
AND (
$6::uuid IS NULL OR
EXISTS (
SELECT 1
FROM entity_geometries eg
WHERE eg.geometry_id = g.id
AND eg.entity_id = $6::uuid
)
)
ORDER BY g.id DESC
`
type SearchGeometriesParams struct {
SearchMinLng pgtype.Float8 `json:"search_min_lng"`
SearchMinLat pgtype.Float8 `json:"search_min_lat"`
SearchMaxLng pgtype.Float8 `json:"search_max_lng"`
SearchMaxLat pgtype.Float8 `json:"search_max_lat"`
TimePoint pgtype.Int4 `json:"time_point"`
EntityID pgtype.UUID `json:"entity_id"`
}
type SearchGeometriesRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) SearchGeometries(ctx context.Context, arg SearchGeometriesParams) ([]SearchGeometriesRow, error) {
rows, err := q.db.Query(ctx, searchGeometries,
arg.SearchMinLng,
arg.SearchMinLat,
arg.SearchMaxLng,
arg.SearchMaxLat,
arg.TimePoint,
arg.EntityID,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchGeometriesRow{}
for rows.Next() {
var i SearchGeometriesRow
if err := rows.Scan(
&i.ID,
&i.GeoType,
&i.DrawGeometry,
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
&i.MaxLat,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateGeometry = `-- name: UpdateGeometry :one
UPDATE geometries
SET
geo_type = COALESCE($1, geo_type),
draw_geometry = COALESCE($2, draw_geometry),
binding = COALESCE($3, binding),
time_start = COALESCE($4, time_start),
time_end = COALESCE($5, time_end),
bbox = CASE
WHEN $6::boolean = true THEN
ST_MakeEnvelope($7::float8, $8::float8, $9::float8, $10::float8, 4326)
ELSE bbox
END,
updated_at = now()
WHERE id = $11 AND is_deleted = false
RETURNING id, geo_type, draw_geometry, binding, time_start, time_end,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at
`
type UpdateGeometryParams struct {
GeoType pgtype.Text `json:"geo_type"`
DrawGeometry []byte `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
UpdateBbox pgtype.Bool `json:"update_bbox"`
MinLng pgtype.Float8 `json:"min_lng"`
MinLat pgtype.Float8 `json:"min_lat"`
MaxLng pgtype.Float8 `json:"max_lng"`
MaxLat pgtype.Float8 `json:"max_lat"`
ID pgtype.UUID `json:"id"`
}
type UpdateGeometryRow struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) UpdateGeometry(ctx context.Context, arg UpdateGeometryParams) (UpdateGeometryRow, error) {
row := q.db.QueryRow(ctx, updateGeometry,
arg.GeoType,
arg.DrawGeometry,
arg.Binding,
arg.TimeStart,
arg.TimeEnd,
arg.UpdateBbox,
arg.MinLng,
arg.MinLat,
arg.MaxLng,
arg.MaxLat,
arg.ID,
)
var i UpdateGeometryRow
err := row.Scan(
&i.ID,
&i.GeoType,
&i.DrawGeometry,
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
&i.MaxLat,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@@ -5,9 +5,44 @@
package sqlc
import (
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
type Entity struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type EntityGeometry struct {
EntityID pgtype.UUID `json:"entity_id"`
GeometryID pgtype.UUID `json:"geometry_id"`
}
type EntityWiki struct {
EntityID pgtype.UUID `json:"entity_id"`
WikiID pgtype.UUID `json:"wiki_id"`
}
type Geometry struct {
ID pgtype.UUID `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
Bbox interface{} `json:"bbox"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Media struct {
ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"`
@@ -77,3 +112,12 @@ type VerificationMedia struct {
VerificationID pgtype.UUID `json:"verification_id"`
MediaID pgtype.UUID `json:"media_id"`
}
type Wiki struct {
ID pgtype.UUID `json:"id"`
Title pgtype.Text `json:"title"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}

View File

@@ -0,0 +1,203 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: wiki.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const bulkDeleteEntityWikisByEntityId = `-- name: BulkDeleteEntityWikisByEntityId :many
DELETE FROM entity_wikis
WHERE entity_id = $1
RETURNING wiki_id
`
func (q *Queries) BulkDeleteEntityWikisByEntityId(ctx context.Context, entityID pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, bulkDeleteEntityWikisByEntityId, entityID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []pgtype.UUID{}
for rows.Next() {
var wiki_id pgtype.UUID
if err := rows.Scan(&wiki_id); err != nil {
return nil, err
}
items = append(items, wiki_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const createEntityWikis = `-- name: CreateEntityWikis :exec
INSERT INTO entity_wikis (
entity_id, wiki_id
)
SELECT $1, unnest($2::uuid[])
`
type CreateEntityWikisParams struct {
EntityID pgtype.UUID `json:"entity_id"`
WikiIds []pgtype.UUID `json:"wiki_ids"`
}
func (q *Queries) CreateEntityWikis(ctx context.Context, arg CreateEntityWikisParams) error {
_, err := q.db.Exec(ctx, createEntityWikis, arg.EntityID, arg.WikiIds)
return err
}
const createWiki = `-- name: CreateWiki :one
INSERT INTO wikis (
title, content
) VALUES (
$1, $2
)
RETURNING id, title, content, is_deleted, created_at, updated_at
`
type CreateWikiParams struct {
Title pgtype.Text `json:"title"`
Content pgtype.Text `json:"content"`
}
func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, error) {
row := q.db.QueryRow(ctx, createWiki, arg.Title, arg.Content)
var i Wiki
err := row.Scan(
&i.ID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteWiki = `-- name: DeleteWiki :exec
UPDATE wikis
SET
is_deleted = true
WHERE id = $1
`
func (q *Queries) DeleteWiki(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteWiki, id)
return err
}
const getWikiById = `-- name: GetWikiById :one
SELECT id, title, content, is_deleted, created_at, updated_at
FROM wikis
WHERE id = $1 AND is_deleted = false
`
func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error) {
row := q.db.QueryRow(ctx, getWikiById, id)
var i Wiki
err := row.Scan(
&i.ID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const searchWikis = `-- name: SearchWikis :many
SELECT w.id, w.title, w.content, w.is_deleted, w.created_at, w.updated_at
FROM wikis w
WHERE w.is_deleted = false
AND w.title ILIKE '%' || $1::text || '%'
AND (
$2::uuid IS NULL OR
EXISTS (
SELECT 1
FROM entity_wikis ew
WHERE ew.wiki_id = w.id
AND ew.entity_id = $2::uuid
)
)
AND ($3::uuid IS NULL OR w.id < $3::uuid)
ORDER BY w.id DESC
LIMIT $4
`
type SearchWikisParams struct {
Title string `json:"title"`
EntityID pgtype.UUID `json:"entity_id"`
CursorID pgtype.UUID `json:"cursor_id"`
LimitCount int32 `json:"limit_count"`
}
func (q *Queries) SearchWikis(ctx context.Context, arg SearchWikisParams) ([]Wiki, error) {
rows, err := q.db.Query(ctx, searchWikis,
arg.Title,
arg.EntityID,
arg.CursorID,
arg.LimitCount,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Wiki{}
for rows.Next() {
var i Wiki
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateWiki = `-- name: UpdateWiki :one
UPDATE wikis
SET
title = COALESCE($1, title),
content = COALESCE($2, content)
WHERE id = $3 AND is_deleted = false
RETURNING id, title, content, is_deleted, created_at, updated_at
`
type UpdateWikiParams struct {
Title pgtype.Text `json:"title"`
Content pgtype.Text `json:"content"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, error) {
row := q.db.QueryRow(ctx, updateWiki, arg.Title, arg.Content, arg.ID)
var i Wiki
err := row.Scan(
&i.ID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

45
internal/models/entity.go Normal file
View File

@@ -0,0 +1,45 @@
package models
import (
"history-api/internal/dtos/response"
"time"
)
type EntityEntity struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ThumbnailUrl string `json:"thumbnail_url"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (e *EntityEntity) ToResponse() *response.EntityResponse {
if e == nil {
return nil
}
return &response.EntityResponse{
ID: e.ID,
Name: e.Name,
Description: e.Description,
ThumbnailUrl: e.ThumbnailUrl,
IsDeleted: e.IsDeleted,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func EntitiesEntityToResponse(es []*EntityEntity) []*response.EntityResponse {
out := make([]*response.EntityResponse, 0)
if es == nil {
return out
}
for _, e := range es {
if e == nil {
continue
}
out = append(out, e.ToResponse())
}
return out
}

View File

@@ -0,0 +1,52 @@
package models
import (
"encoding/json"
"history-api/internal/dtos/response"
"time"
)
type GeometryEntity struct {
ID string `json:"id"`
GeoType string `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding json.RawMessage `json:"binding"`
TimeStart int32 `json:"time_start"`
TimeEnd int32 `json:"time_end"`
Bbox *response.Bbox `json:"bbox"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (g *GeometryEntity) ToResponse() *response.GeometryResponse {
if g == nil {
return nil
}
return &response.GeometryResponse{
ID: g.ID,
GeoType: g.GeoType,
DrawGeometry: g.DrawGeometry,
Binding: g.Binding,
TimeStart: g.TimeStart,
TimeEnd: g.TimeEnd,
Bbox: g.Bbox,
IsDeleted: g.IsDeleted,
CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt,
}
}
func GeometriesEntityToResponse(gs []*GeometryEntity) []*response.GeometryResponse {
out := make([]*response.GeometryResponse, 0)
if gs == nil {
return out
}
for _, g := range gs {
if g == nil {
continue
}
out = append(out, g.ToResponse())
}
return out
}

43
internal/models/wiki.go Normal file
View File

@@ -0,0 +1,43 @@
package models
import (
"history-api/internal/dtos/response"
"time"
)
type WikiEntity struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
func (w *WikiEntity) ToResponse() *response.WikiResponse {
if w == nil {
return nil
}
return &response.WikiResponse{
ID: w.ID,
Title: w.Title,
Content: w.Content,
IsDeleted: w.IsDeleted,
CreatedAt: w.CreatedAt,
UpdatedAt: w.UpdatedAt,
}
}
func WikisEntityToResponse(ws []*WikiEntity) []*response.WikiResponse {
out := make([]*response.WikiResponse, 0)
if ws == nil {
return out
}
for _, w := range ws {
if w == nil {
continue
}
out = append(out, w.ToResponse())
}
return out
}

View File

@@ -0,0 +1,205 @@
package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
)
type EntityRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.EntityEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.EntityEntity, error)
Search(ctx context.Context, params sqlc.SearchEntitiesParams) ([]*models.EntityEntity, error)
Create(ctx context.Context, params sqlc.CreateEntityParams) (*models.EntityEntity, error)
Update(ctx context.Context, params sqlc.UpdateEntityParams) (*models.EntityEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
}
type entityRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewEntityRepository(db sqlc.DBTX, c cache.Cache) EntityRepository {
return &entityRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *entityRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (r *entityRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.EntityEntity, error) {
if len(ids) == 0 {
return []*models.EntityEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("entity:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var entities []*models.EntityEntity
missingToCache := make(map[string]any)
for i, b := range raws {
if len(b) > 0 {
var e models.EntityEntity
if err := json.Unmarshal(b, &e); err == nil {
entities = append(entities, &e)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbEntity, err := r.GetByID(ctx, pgId)
if err == nil && dbEntity != nil {
entities = append(entities, dbEntity)
missingToCache[keys[i]] = dbEntity
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return entities, nil
}
func (r *entityRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.EntityEntity, error) {
return r.getByIDsWithFallback(ctx, ids)
}
func (r *entityRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.EntityEntity, error) {
cacheId := fmt.Sprintf("entity:id:%s", convert.UUIDToString(id))
var entity models.EntityEntity
err := r.c.Get(ctx, cacheId, &entity)
if err == nil {
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration)
return &entity, nil
}
row, err := r.q.GetEntityById(ctx, id)
if err != nil {
return nil, err
}
entity = models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration)
return &entity, nil
}
func (r *entityRepository) Search(ctx context.Context, params sqlc.SearchEntitiesParams) ([]*models.EntityEntity, error) {
queryKey := r.generateQueryKey("entity:search", params)
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.SearchEntities(ctx, params)
if err != nil {
return nil, err
}
var entities []*models.EntityEntity
var ids []string
entityToCache := make(map[string]any)
for _, row := range rows {
entity := &models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
ids = append(ids, entity.ID)
entities = append(entities, entity)
entityToCache[fmt.Sprintf("entity:id:%s", entity.ID)] = entity
}
if len(entityToCache) > 0 {
_ = r.c.MSet(ctx, entityToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return entities, nil
}
func (r *entityRepository) Create(ctx context.Context, params sqlc.CreateEntityParams) (*models.EntityEntity, error) {
row, err := r.q.CreateEntity(ctx, params)
if err != nil {
return nil, err
}
entity := models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("entity:id:%s", entity.ID), entity, constants.NormalCacheDuration)
go func() {
_ = r.c.DelByPattern(context.Background(), "entity:search*")
}()
return &entity, nil
}
func (r *entityRepository) Update(ctx context.Context, params sqlc.UpdateEntityParams) (*models.EntityEntity, error) {
row, err := r.q.UpdateEntity(ctx, params)
if err != nil {
return nil, err
}
entity := models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("entity:id:%s", entity.ID), entity, constants.NormalCacheDuration)
return &entity, nil
}
func (r *entityRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteEntity(ctx, id)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("entity:id:%s", convert.UUIDToString(id)))
return nil
}

View File

@@ -0,0 +1,266 @@
package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
)
type GeometryRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.GeometryEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.GeometryEntity, error)
Search(ctx context.Context, params sqlc.SearchGeometriesParams) ([]*models.GeometryEntity, error)
Create(ctx context.Context, params sqlc.CreateGeometryParams) (*models.GeometryEntity, error)
Update(ctx context.Context, params sqlc.UpdateGeometryParams) (*models.GeometryEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error
BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error
}
type geometryRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewGeometryRepository(db sqlc.DBTX, c cache.Cache) GeometryRepository {
return &geometryRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *geometryRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (r *geometryRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.GeometryEntity, error) {
if len(ids) == 0 {
return []*models.GeometryEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("geometry:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var geometries []*models.GeometryEntity
missingToCache := make(map[string]any)
for i, b := range raws {
if len(b) > 0 {
var g models.GeometryEntity
if err := json.Unmarshal(b, &g); err == nil {
geometries = append(geometries, &g)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbGeometry, err := r.GetByID(ctx, pgId)
if err == nil && dbGeometry != nil {
geometries = append(geometries, dbGeometry)
missingToCache[keys[i]] = dbGeometry
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return geometries, nil
}
func (r *geometryRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.GeometryEntity, error) {
return r.getByIDsWithFallback(ctx, ids)
}
func (r *geometryRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.GeometryEntity, error) {
cacheId := fmt.Sprintf("geometry:id:%s", convert.UUIDToString(id))
var geometry models.GeometryEntity
err := r.c.Get(ctx, cacheId, &geometry)
if err == nil {
_ = r.c.Set(ctx, cacheId, geometry, constants.NormalCacheDuration)
return &geometry, nil
}
row, err := r.q.GetGeometryById(ctx, id)
if err != nil {
return nil, err
}
geometry = models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
TimeEnd: convert.Int4ToInt32(row.TimeEnd),
Bbox: &response.Bbox{
MinLng: row.MinLng,
MinLat: row.MinLat,
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, cacheId, geometry, constants.NormalCacheDuration)
return &geometry, nil
}
func (r *geometryRepository) Search(ctx context.Context, params sqlc.SearchGeometriesParams) ([]*models.GeometryEntity, error) {
queryKey := r.generateQueryKey("geometry:search", params)
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.SearchGeometries(ctx, params)
if err != nil {
return nil, err
}
var geometries []*models.GeometryEntity
var ids []string
geometryToCache := make(map[string]any)
for _, row := range rows {
geometry := &models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
TimeEnd: convert.Int4ToInt32(row.TimeEnd),
Bbox: &response.Bbox{
MinLng: row.MinLng,
MinLat: row.MinLat,
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
ids = append(ids, geometry.ID)
geometries = append(geometries, geometry)
geometryToCache[fmt.Sprintf("geometry:id:%s", geometry.ID)] = geometry
}
if len(geometryToCache) > 0 {
_ = r.c.MSet(ctx, geometryToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return geometries, nil
}
func (r *geometryRepository) Create(ctx context.Context, params sqlc.CreateGeometryParams) (*models.GeometryEntity, error) {
row, err := r.q.CreateGeometry(ctx, params)
if err != nil {
return nil, err
}
geometry := models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
TimeEnd: convert.Int4ToInt32(row.TimeEnd),
Bbox: &response.Bbox{
MinLng: row.MinLng,
MinLat: row.MinLat,
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("geometry:id:%s", geometry.ID), geometry, constants.NormalCacheDuration)
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "geometry:search*")
}()
return &geometry, nil
}
func (r *geometryRepository) Update(ctx context.Context, params sqlc.UpdateGeometryParams) (*models.GeometryEntity, error) {
row, err := r.q.UpdateGeometry(ctx, params)
if err != nil {
return nil, err
}
geometry := models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: row.GeoType,
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
TimeEnd: convert.Int4ToInt32(row.TimeEnd),
Bbox: &response.Bbox{
MinLng: row.MinLng,
MinLat: row.MinLat,
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("geometry:id:%s", geometry.ID), geometry, constants.NormalCacheDuration)
return &geometry, nil
}
func (r *geometryRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteGeometry(ctx, id)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("geometry:id:%s", convert.UUIDToString(id)))
return nil
}
func (r *geometryRepository) CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error {
err := r.q.CreateEntityGeometries(ctx, params)
if err != nil {
return err
}
return err
}
func (r *geometryRepository) BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error {
geometryIDs, err := r.q.BulkDeleteEntityGeometriesByEntityId(ctx, entityId)
if err != nil {
return err
}
if len(geometryIDs) > 0 {
keys := make([]string, len(geometryIDs))
for i, id := range geometryIDs {
keys[i] = fmt.Sprintf("geometry:id:%s", convert.UUIDToString(id))
}
go func() {
_ = r.c.Del(context.Background(), keys...)
}()
}
return nil
}

View File

@@ -148,8 +148,9 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
if err != nil {
return nil, err
}
go func() {
bgCtx := context.Background()
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "role:all*")
}()

View File

@@ -225,8 +225,7 @@ func (v *verificationRepository) BulkVerificationMediaByMediaId(ctx context.Cont
}
go func() {
bgCtx := context.Background()
_ = v.c.Del(bgCtx, listCacheId...)
_ = v.c.Del(context.Background(), listCacheId...)
}()
return nil

View File

@@ -0,0 +1,233 @@
package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
)
type WikiRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.WikiEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.WikiEntity, error)
Search(ctx context.Context, params sqlc.SearchWikisParams) ([]*models.WikiEntity, error)
Create(ctx context.Context, params sqlc.CreateWikiParams) (*models.WikiEntity, error)
Update(ctx context.Context, params sqlc.UpdateWikiParams) (*models.WikiEntity, error)
Delete(ctx context.Context, id pgtype.UUID) error
CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) error
BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error
}
type wikiRepository struct {
q *sqlc.Queries
c cache.Cache
}
func NewWikiRepository(db sqlc.DBTX, c cache.Cache) WikiRepository {
return &wikiRepository{
q: sqlc.New(db),
c: c,
}
}
func (r *wikiRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.WikiEntity, error) {
if len(ids) == 0 {
return []*models.WikiEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("wiki:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var wikis []*models.WikiEntity
missingToCache := make(map[string]any)
for i, b := range raws {
if len(b) > 0 {
var w models.WikiEntity
if err := json.Unmarshal(b, &w); err == nil {
wikis = append(wikis, &w)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbWiki, err := r.GetByID(ctx, pgId)
if err == nil && dbWiki != nil {
wikis = append(wikis, dbWiki)
missingToCache[keys[i]] = dbWiki
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return wikis, nil
}
func (r *wikiRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.WikiEntity, error) {
return r.getByIDsWithFallback(ctx, ids)
}
func (r *wikiRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.WikiEntity, error) {
cacheId := fmt.Sprintf("wiki:id:%s", convert.UUIDToString(id))
var wiki models.WikiEntity
err := r.c.Get(ctx, cacheId, &wiki)
if err == nil {
_ = r.c.Set(ctx, cacheId, wiki, constants.NormalCacheDuration)
return &wiki, nil
}
row, err := r.q.GetWikiById(ctx, id)
if err != nil {
return nil, err
}
wiki = models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, cacheId, wiki, constants.NormalCacheDuration)
return &wiki, nil
}
func (r *wikiRepository) Search(ctx context.Context, params sqlc.SearchWikisParams) ([]*models.WikiEntity, error) {
queryKey := r.generateQueryKey("wiki:search", params)
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.SearchWikis(ctx, params)
if err != nil {
return nil, err
}
var wikis []*models.WikiEntity
var ids []string
wikiToCache := make(map[string]any)
for _, row := range rows {
wiki := &models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
ids = append(ids, wiki.ID)
wikis = append(wikis, wiki)
wikiToCache[fmt.Sprintf("wiki:id:%s", wiki.ID)] = wiki
}
if len(wikiToCache) > 0 {
_ = r.c.MSet(ctx, wikiToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return wikis, nil
}
func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParams) (*models.WikiEntity, error) {
row, err := r.q.CreateWiki(ctx, params)
if err != nil {
return nil, err
}
wiki := models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("wiki:id:%s", wiki.ID), wiki, constants.NormalCacheDuration)
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "wiki:search*")
}()
return &wiki, nil
}
func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParams) (*models.WikiEntity, error) {
row, err := r.q.UpdateWiki(ctx, params)
if err != nil {
return nil, err
}
wiki := models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, fmt.Sprintf("wiki:id:%s", wiki.ID), wiki, constants.NormalCacheDuration)
return &wiki, nil
}
func (r *wikiRepository) Delete(ctx context.Context, id pgtype.UUID) error {
err := r.q.DeleteWiki(ctx, id)
if err != nil {
return err
}
_ = r.c.Del(ctx, fmt.Sprintf("wiki:id:%s", convert.UUIDToString(id)))
go func() {
_ = r.c.DelByPattern(context.Background(), "wiki:search*")
}()
return nil
}
func (r *wikiRepository) CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) error {
err := r.q.CreateEntityWikis(ctx, params)
if err != nil {
return err
}
return nil
}
func (r *wikiRepository) BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error {
wikiIDs, err := r.q.BulkDeleteEntityWikisByEntityId(ctx, entityId)
if err != nil {
return err
}
if len(wikiIDs) > 0 {
keys := make([]string, len(wikiIDs))
for i, id := range wikiIDs {
keys[i] = fmt.Sprintf("wiki:id:%s", convert.UUIDToString(id))
}
go func() {
_ = r.c.Del(context.Background(), keys...)
}()
}
return nil
}

View File

@@ -0,0 +1,13 @@
package routes
import (
"history-api/internal/controllers"
"github.com/gofiber/fiber/v3"
)
func SetupEntityRoutes(router fiber.Router, entityController *controllers.EntityController) {
entity := router.Group("/entities")
entity.Get("/", entityController.SearchEntities)
entity.Get("/:id", entityController.GetEntityById)
}

View File

@@ -0,0 +1,13 @@
package routes
import (
"history-api/internal/controllers"
"github.com/gofiber/fiber/v3"
)
func SetupGeometryRoutes(router fiber.Router, geometryController *controllers.GeometryController) {
geometry := router.Group("/geometries")
geometry.Get("/", geometryController.SearchGeometries)
geometry.Get("/:id", geometryController.GetGeometryById)
}

View File

@@ -0,0 +1,13 @@
package routes
import (
"history-api/internal/controllers"
"github.com/gofiber/fiber/v3"
)
func SetupWikiRoutes(router fiber.Router, wikiController *controllers.WikiController) {
wiki := router.Group("/wikis")
wiki.Get("/", wikiController.SearchWikis)
wiki.Get("/:id", wikiController.GetWikiById)
}

View File

@@ -0,0 +1,68 @@
package services
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/internal/repositories"
"history-api/pkg/convert"
"github.com/gofiber/fiber/v3"
)
type EntityService interface {
GetEntityByID(ctx context.Context, id string) (*response.EntityResponse, error)
SearchEntities(ctx context.Context, req *request.SearchEntityDto) ([]*response.EntityResponse, error)
}
type entityService struct {
entityRepo repositories.EntityRepository
}
func NewEntityService(entityRepo repositories.EntityRepository) EntityService {
return &entityService{
entityRepo: entityRepo,
}
}
func (s *entityService) GetEntityByID(ctx context.Context, id string) (*response.EntityResponse, error) {
entityId, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
entity, err := s.entityRepo.GetByID(ctx, entityId)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found")
}
return entity.ToResponse(), nil
}
func (s *entityService) SearchEntities(ctx context.Context, req *request.SearchEntityDto) ([]*response.EntityResponse, error) {
limit := int32(25)
if req.Limit > 0 {
limit = int32(req.Limit)
}
params := sqlc.SearchEntitiesParams{
LimitCount: limit,
}
if req.Cursor != "" {
cursor, err := convert.StringToUUID(req.Cursor)
if err == nil {
params.CursorID = cursor
}
}
if req.Name != "" {
params.Name = req.Name
}
entities, err := s.entityRepo.Search(ctx, params)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return models.EntitiesEntityToResponse(entities), nil
}

View File

@@ -0,0 +1,79 @@
package services
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/internal/repositories"
"history-api/pkg/convert"
"github.com/gofiber/fiber/v3"
"github.com/jackc/pgx/v5/pgtype"
)
type GeometryService interface {
GetGeometryByID(ctx context.Context, id string) (*response.GeometryResponse, error)
SearchGeometries(ctx context.Context, req *request.SearchGeometryDto) ([]*response.GeometryResponse, error)
}
type geometryService struct {
geometryRepo repositories.GeometryRepository
}
func NewGeometryService(geometryRepo repositories.GeometryRepository) GeometryService {
return &geometryService{
geometryRepo: geometryRepo,
}
}
func (s *geometryService) GetGeometryByID(ctx context.Context, id string) (*response.GeometryResponse, error) {
geometryId, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
geometry, err := s.geometryRepo.GetByID(ctx, geometryId)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found")
}
return geometry.ToResponse(), nil
}
func (s *geometryService) SearchGeometries(ctx context.Context, req *request.SearchGeometryDto) ([]*response.GeometryResponse, error) {
params := sqlc.SearchGeometriesParams{}
if req.MinLng != nil && req.MinLat != nil && req.MaxLng != nil && req.MaxLat != nil {
if *req.MaxLng < *req.MinLng || *req.MaxLat < *req.MinLat {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid bounding box")
}
params.SearchMinLng = pgtype.Float8{Float64: *req.MinLng, Valid: true}
params.SearchMinLat = pgtype.Float8{Float64: *req.MinLat, Valid: true}
params.SearchMaxLng = pgtype.Float8{Float64: *req.MaxLng, Valid: true}
params.SearchMaxLat = pgtype.Float8{Float64: *req.MaxLat, Valid: true}
} else {
return nil, fiber.NewError(fiber.StatusBadRequest, "Must provid Bounding box!")
}
if req.TimePoint != nil {
if *req.TimePoint < 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Time point must be non-negative!")
}
params.TimePoint = pgtype.Int4{Int32: *req.TimePoint, Valid: true}
}
if req.EntityID != nil {
entityId, err := convert.StringToUUID(*req.EntityID)
if err == nil {
params.EntityID = entityId
}
}
geometries, err := s.geometryRepo.Search(ctx, params)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return models.GeometriesEntityToResponse(geometries), nil
}

View File

@@ -64,8 +64,16 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
return fiber.NewError(fiber.StatusNotFound, "User not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
if user.PasswordHash != "" {
if dto.OldPassword == "" {
return fiber.NewError(fiber.StatusBadRequest, "Old password required")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid password!")
}
} else if user.PasswordHash == "" && dto.OldPassword != "" {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request")
}
hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)

View File

@@ -0,0 +1,74 @@
package services
import (
"context"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/internal/repositories"
"history-api/pkg/convert"
"github.com/gofiber/fiber/v3"
)
type WikiService interface {
GetWikiByID(ctx context.Context, id string) (*response.WikiResponse, error)
SearchWikis(ctx context.Context, req *request.SearchWikiDto) ([]*response.WikiResponse, error)
}
type wikiService struct {
wikiRepo repositories.WikiRepository
}
func NewWikiService(wikiRepo repositories.WikiRepository) WikiService {
return &wikiService{
wikiRepo: wikiRepo,
}
}
func (s *wikiService) GetWikiByID(ctx context.Context, id string) (*response.WikiResponse, error) {
wikiId, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
wiki, err := s.wikiRepo.GetByID(ctx, wikiId)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found")
}
return wiki.ToResponse(), nil
}
func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDto) ([]*response.WikiResponse, error) {
limit := int32(25)
if req.Limit > 0 {
limit = int32(req.Limit)
}
params := sqlc.SearchWikisParams{
LimitCount: limit,
}
if req.Cursor != "" {
cursor, err := convert.StringToUUID(req.Cursor)
if err == nil {
params.CursorID = cursor
}
}
if req.Title != "" {
params.Title = req.Title
}
if req.EntityID != "" {
entityId, err := convert.StringToUUID(req.EntityID)
if err == nil {
params.EntityID = entityId
}
}
wikis, err := s.wikiRepo.Search(ctx, params)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return models.WikisEntityToResponse(wikis), nil
}