feat: implement wiki and entity repositories and services with corresponding SQL queries and generation
All checks were successful
Build and Release / release (push) Successful in 1m30s

This commit is contained in:
2026-05-10 11:38:33 +07:00
parent 12a04f0670
commit 8cee6b6622
7 changed files with 239 additions and 17 deletions

View File

@@ -64,4 +64,7 @@ WHERE id = ANY($1::uuid[]);
-- name: GetEntityBySlug :one
SELECT *
FROM entities
WHERE slug = $1 AND is_deleted = false;
WHERE slug = $1 AND is_deleted = false;
-- name: GetEntitiesBySlugs :many
SELECT * FROM entities WHERE slug = ANY($1::text[]) AND is_deleted = false;

View File

@@ -90,3 +90,6 @@ WHERE entity_id = $1 AND wiki_id = $2;
SELECT *
FROM wikis
WHERE slug = $1 AND is_deleted = false;
-- name: GetWikisBySlugs :many
SELECT * FROM wikis WHERE slug = ANY($1::text[]) AND is_deleted = false;

View File

@@ -156,6 +156,42 @@ func (q *Queries) GetEntitiesByProjectId(ctx context.Context, projectID pgtype.U
return items, nil
}
const getEntitiesBySlugs = `-- name: GetEntitiesBySlugs :many
SELECT id, project_id, name, slug, description, status, time_start, time_end, is_deleted, created_at, updated_at FROM entities WHERE slug = ANY($1::text[]) AND is_deleted = false
`
func (q *Queries) GetEntitiesBySlugs(ctx context.Context, dollar_1 []string) ([]Entity, error) {
rows, err := q.db.Query(ctx, getEntitiesBySlugs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Entity{}
for rows.Next() {
var i Entity
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Slug,
&i.Description,
&i.Status,
&i.TimeStart,
&i.TimeEnd,
&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 getEntityById = `-- name: GetEntityById :one
SELECT id, project_id, name, slug, description, status, time_start, time_end, is_deleted, created_at, updated_at
FROM entities

View File

@@ -265,6 +265,39 @@ func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID
return items, nil
}
const getWikisBySlugs = `-- name: GetWikisBySlugs :many
SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at FROM wikis WHERE slug = ANY($1::text[]) AND is_deleted = false
`
func (q *Queries) GetWikisBySlugs(ctx context.Context, dollar_1 []string) ([]Wiki, error) {
rows, err := q.db.Query(ctx, getWikisBySlugs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Wiki{}
for rows.Next() {
var i Wiki
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Title,
&i.Slug,
&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 searchWikis = `-- name: SearchWikis :many
SELECT w.id, w.project_id, w.title, w.slug, w.content, w.is_deleted, w.created_at, w.updated_at
FROM wikis w

View File

@@ -20,6 +20,7 @@ type EntityRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.EntityEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.EntityEntity, error)
GetBySlug(ctx context.Context, slug string) (*models.EntityEntity, error)
GetBySlugs(ctx context.Context, slugs []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)
@@ -349,3 +350,67 @@ func (r *entityRepository) GetBySlug(ctx context.Context, slug string) (*models.
return &entity, nil
}
func (r *entityRepository) GetBySlugs(ctx context.Context, slugs []string) ([]*models.EntityEntity, error) {
if len(slugs) == 0 {
return []*models.EntityEntity{}, nil
}
keys := make([]string, len(slugs))
for i, slug := range slugs {
keys[i] = fmt.Sprintf("entity:slug:%s", slug)
}
raws := r.c.MGet(ctx, keys...)
var entities []*models.EntityEntity
missingToCache := make(map[string]any)
var missingSlugs []string
for i, b := range raws {
if len(b) == 0 {
missingSlugs = append(missingSlugs, slugs[i])
}
}
dbMap := make(map[string]*models.EntityEntity)
if len(missingSlugs) > 0 {
dbRows, err := r.q.GetEntitiesBySlugs(ctx, missingSlugs)
if err == nil {
for _, row := range dbRows {
item := models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description),
ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
TimeStart: convert.Int4ToPtr(row.TimeStart),
TimeEnd: convert.Int4ToPtr(row.TimeEnd),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.Slug] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.EntityEntity
if err := json.Unmarshal(b, &u); err == nil {
entities = append(entities, &u)
}
} else {
if item, ok := dbMap[slugs[i]]; ok {
entities = append(entities, item)
missingToCache[keys[i]] = item
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return entities, nil
}

View File

@@ -20,6 +20,7 @@ type WikiRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.WikiEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.WikiEntity, error)
GetBySlug(ctx context.Context, slug string) (*models.WikiEntity, error)
GetBySlugs(ctx context.Context, slugs []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)
@@ -359,3 +360,64 @@ func (r *wikiRepository) GetBySlug(ctx context.Context, slug string) (*models.Wi
return &wiki, nil
}
func (r *wikiRepository) GetBySlugs(ctx context.Context, slugs []string) ([]*models.WikiEntity, error) {
if len(slugs) == 0 {
return []*models.WikiEntity{}, nil
}
keys := make([]string, len(slugs))
for i, slug := range slugs {
keys[i] = fmt.Sprintf("wiki:slug:%s", slug)
}
raws := r.c.MGet(ctx, keys...)
var wikis []*models.WikiEntity
missingToCache := make(map[string]any)
var missingSlugs []string
for i, b := range raws {
if len(b) == 0 {
missingSlugs = append(missingSlugs, slugs[i])
}
}
dbMap := make(map[string]*models.WikiEntity)
if len(missingSlugs) > 0 {
dbRows, err := r.q.GetWikisBySlugs(ctx, missingSlugs)
if err == nil {
for _, row := range dbRows {
item := models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
dbMap[item.Slug] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.WikiEntity
if err := json.Unmarshal(b, &u); err == nil {
wikis = append(wikis, &u)
}
} else {
if item, ok := dbMap[slugs[i]]; ok {
wikis = append(wikis, item)
missingToCache[keys[i]] = item
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return wikis, nil
}

View File

@@ -2,9 +2,7 @@ package services
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
@@ -104,26 +102,48 @@ func (s *submissionService) CreateSubmission(ctx context.Context, userID string,
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot")
}
var entitySlugs []string
entitySlugToID := make(map[string]string)
for _, entity := range snapshotData.Entities {
if entity.Slug != nil {
exist, err := s.entityRepo.GetBySlug(ctx, *entity.Slug)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get entity")
}
if exist != nil {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Entity %s already exists", *entity.Slug))
if entity.Source == "inline" && entity.Slug != nil {
entitySlugs = append(entitySlugs, *entity.Slug)
entitySlugToID[*entity.Slug] = entity.ID
}
}
if len(entitySlugs) > 0 {
existEntities, err := s.entityRepo.GetBySlugs(ctx, entitySlugs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get entities")
}
for _, exist := range existEntities {
if snapID, ok := entitySlugToID[exist.Slug]; ok {
if exist.ID != snapID {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Entity %s already exists", exist.Slug))
}
}
}
}
var wikiSlugs []string
wikiSlugToID := make(map[string]string)
for _, wiki := range snapshotData.Wikis {
if wiki.Slug != nil {
exist, err := s.wikiRepo.GetBySlug(ctx, *wiki.Slug)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get wiki")
}
if exist != nil {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Wiki %s already exists", *wiki.Slug))
if wiki.Source == "inline" && wiki.Slug != nil {
wikiSlugs = append(wikiSlugs, *wiki.Slug)
wikiSlugToID[*wiki.Slug] = wiki.ID
}
}
if len(wikiSlugs) > 0 {
existWikis, err := s.wikiRepo.GetBySlugs(ctx, wikiSlugs)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get wikis")
}
for _, exist := range existWikis {
if snapID, ok := wikiSlugToID[exist.Slug]; ok {
if exist.ID != snapID {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Wiki %s already exists", exist.Slug))
}
}
}
}