diff --git a/db/query/entities.sql b/db/query/entities.sql index 346d4a8..82a38d4 100644 --- a/db/query/entities.sql +++ b/db/query/entities.sql @@ -64,4 +64,7 @@ WHERE id = ANY($1::uuid[]); -- name: GetEntityBySlug :one SELECT * FROM entities -WHERE slug = $1 AND is_deleted = false; \ No newline at end of file +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 diff --git a/db/query/wiki.sql b/db/query/wiki.sql index 0b1e402..0c57f36 100644 --- a/db/query/wiki.sql +++ b/db/query/wiki.sql @@ -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; diff --git a/internal/gen/sqlc/entities.sql.go b/internal/gen/sqlc/entities.sql.go index e32b3ca..2328096 100644 --- a/internal/gen/sqlc/entities.sql.go +++ b/internal/gen/sqlc/entities.sql.go @@ -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 diff --git a/internal/gen/sqlc/wiki.sql.go b/internal/gen/sqlc/wiki.sql.go index 775754e..f263c47 100644 --- a/internal/gen/sqlc/wiki.sql.go +++ b/internal/gen/sqlc/wiki.sql.go @@ -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 diff --git a/internal/repositories/entityRepository.go b/internal/repositories/entityRepository.go index 0c1f554..c58961b 100644 --- a/internal/repositories/entityRepository.go +++ b/internal/repositories/entityRepository.go @@ -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 +} diff --git a/internal/repositories/wikiRepository.go b/internal/repositories/wikiRepository.go index c3e9a32..f6de4e3 100644 --- a/internal/repositories/wikiRepository.go +++ b/internal/repositories/wikiRepository.go @@ -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 +} diff --git a/internal/services/submissionService.go b/internal/services/submissionService.go index c25d5de..c768593 100644 --- a/internal/services/submissionService.go +++ b/internal/services/submissionService.go @@ -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)) + } } } }