feat: implement wiki and wiki content management system including database schemas, DTOs, and API endpoints
All checks were successful
Build and Release / release (push) Successful in 1m33s

This commit is contained in:
2026-05-11 11:02:57 +07:00
parent 8cee6b6622
commit 2873e42eab
14 changed files with 536 additions and 63 deletions

View File

@@ -5,7 +5,6 @@ CREATE TABLE IF NOT EXISTS wikis (
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT,
slug TEXT,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS wiki_content;

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS wiki_content (
id UUID PRIMARY KEY DEFAULT uuidv7(),
wiki_id UUID NOT NULL REFERENCES wikis(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_wiki_content_wiki_id ON wiki_content(wiki_id);
CREATE INDEX idx_wiki_content_created_at ON wiki_content(created_at DESC);

View File

@@ -1,8 +1,8 @@
-- name: CreateWiki :one
INSERT INTO wikis (
id, title, slug, content, project_id
id, title, slug, project_id
) VALUES (
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3
)
RETURNING *;
@@ -16,7 +16,6 @@ UPDATE wikis
SET
title = COALESCE(sqlc.narg('title'), title),
slug = COALESCE(sqlc.narg('slug'), slug),
content = COALESCE(sqlc.narg('content'), content),
project_id = COALESCE(sqlc.narg('project_id'), project_id)
WHERE id = sqlc.arg('id') AND is_deleted = false
RETURNING *;
@@ -93,3 +92,38 @@ WHERE slug = $1 AND is_deleted = false;
-- name: GetWikisBySlugs :many
SELECT * FROM wikis WHERE slug = ANY($1::text[]) AND is_deleted = false;
-- name: CreateWikiContent :one
INSERT INTO wiki_content (
id, wiki_id, title, content
) VALUES (
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3
)
RETURNING *;
-- name: GetWikiContentCount :one
SELECT COUNT(*)
FROM wiki_content
WHERE wiki_id = $1;
-- name: GetWikiContentById :one
SELECT *
FROM wiki_content
WHERE id = $1 AND is_deleted = false;
-- name: GetWikiContentByIDs :many
SELECT *
FROM wiki_content
WHERE id = ANY($1::uuid[]) AND is_deleted = false;
-- name: GetWikiContentByWikiID :many
SELECT id, title, created_at
FROM wiki_content
WHERE wiki_id = $1 AND is_deleted = false
ORDER BY created_at DESC;
-- name: GetWikiContentByWikiIDs :many
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;

View File

@@ -102,12 +102,20 @@ CREATE TABLE IF NOT EXISTS wikis (
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT,
slug TEXT UNIQUE,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS wiki_content (
id UUID PRIMARY KEY DEFAULT uuidv7(),
wiki_id UUID NOT NULL REFERENCES wikis(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS entity_wikis (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
wiki_id UUID REFERENCES wikis(id) ON DELETE CASCADE,

View File

@@ -136,3 +136,30 @@ func (h *WikiController) SearchWikis(c fiber.Ctx) error {
})
}
// GetWikiContentById handles fetching a single wiki content by ID.
// @Summary Get wiki content by ID
// @Description Get detailed information about a specific wiki content version
// @Tags Wikis
// @Accept json
// @Produce json
// @Param id path string true "Wiki Content ID"
// @Success 200 {object} response.CommonResponse
// @Failure 500 {object} response.CommonResponse
// @Router /wikis/content/{id} [get]
func (h *WikiController) GetWikiContentById(c fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
id := c.Params("id")
res, err := h.service.GetWikiContentByID(ctx, id)
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,
})
}

View File

@@ -4,13 +4,27 @@ import (
"time"
)
type WikiResponse struct {
type WikiContentSample struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Slug string `json:"slug,omitempty"`
Content string `json:"content,omitempty"`
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Title string `json:"title"`
CreatedAt *time.Time `json:"created_at"`
}
type WikiResponse struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Slug string `json:"slug,omitempty"`
ContentSample []WikiContentSample `json:"content_sample,omitempty"`
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
type WikiContentResponse struct {
ID string `json:"id"`
WikiID string `json:"wiki_id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt *time.Time `json:"created_at"`
}

View File

@@ -234,8 +234,16 @@ type Wiki struct {
ProjectID pgtype.UUID `json:"project_id"`
Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type WikiContent struct {
ID pgtype.UUID `json:"id"`
WikiID pgtype.UUID `json:"wiki_id"`
Title string `json:"title"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}

View File

@@ -68,17 +68,16 @@ func (q *Queries) CreateEntityWikis(ctx context.Context, arg CreateEntityWikisPa
const createWiki = `-- name: CreateWiki :one
INSERT INTO wikis (
id, title, slug, content, project_id
id, title, slug, project_id
) VALUES (
COALESCE($5::uuid, uuidv7()), $1, $2, $3, $4
COALESCE($4::uuid, uuidv7()), $1, $2, $3
)
RETURNING id, project_id, title, slug, content, is_deleted, created_at, updated_at
RETURNING id, project_id, title, slug, is_deleted, created_at, updated_at
`
type CreateWikiParams struct {
Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"`
ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"`
}
@@ -87,7 +86,6 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
row := q.db.QueryRow(ctx, createWiki,
arg.Title,
arg.Slug,
arg.Content,
arg.ProjectID,
arg.ID,
)
@@ -97,7 +95,6 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -105,6 +102,41 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
return i, err
}
const createWikiContent = `-- name: CreateWikiContent :one
INSERT INTO wiki_content (
id, wiki_id, title, content
) VALUES (
COALESCE($4::uuid, uuidv7()), $1, $2, $3
)
RETURNING id, wiki_id, title, content, is_deleted, created_at
`
type CreateWikiContentParams struct {
WikiID pgtype.UUID `json:"wiki_id"`
Title string `json:"title"`
Content pgtype.Text `json:"content"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) CreateWikiContent(ctx context.Context, arg CreateWikiContentParams) (WikiContent, error) {
row := q.db.QueryRow(ctx, createWikiContent,
arg.WikiID,
arg.Title,
arg.Content,
arg.ID,
)
var i WikiContent
err := row.Scan(
&i.ID,
&i.WikiID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
)
return i, err
}
const deleteEntityWiki = `-- name: DeleteEntityWiki :exec
DELETE FROM entity_wikis
WHERE entity_id = $1 AND wiki_id = $2
@@ -154,7 +186,7 @@ func (q *Queries) DeleteWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
}
const getWikiById = `-- name: GetWikiById :one
SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at
SELECT id, project_id, title, slug, is_deleted, created_at, updated_at
FROM wikis
WHERE id = $1 AND is_deleted = false
`
@@ -167,7 +199,6 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error)
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -176,7 +207,7 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error)
}
const getWikiBySlug = `-- name: GetWikiBySlug :one
SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at
SELECT id, project_id, title, slug, is_deleted, created_at, updated_at
FROM wikis
WHERE slug = $1 AND is_deleted = false
`
@@ -189,7 +220,6 @@ func (q *Queries) GetWikiBySlug(ctx context.Context, slug pgtype.Text) (Wiki, er
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -197,8 +227,146 @@ func (q *Queries) GetWikiBySlug(ctx context.Context, slug pgtype.Text) (Wiki, er
return i, err
}
const getWikiContentByIDs = `-- name: GetWikiContentByIDs :many
SELECT id, wiki_id, title, content, is_deleted, created_at
FROM wiki_content
WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetWikiContentByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]WikiContent, error) {
rows, err := q.db.Query(ctx, getWikiContentByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []WikiContent{}
for rows.Next() {
var i WikiContent
if err := rows.Scan(
&i.ID,
&i.WikiID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWikiContentById = `-- name: GetWikiContentById :one
SELECT id, wiki_id, title, content, is_deleted, created_at
FROM wiki_content
WHERE id = $1 AND is_deleted = false
`
func (q *Queries) GetWikiContentById(ctx context.Context, id pgtype.UUID) (WikiContent, error) {
row := q.db.QueryRow(ctx, getWikiContentById, id)
var i WikiContent
err := row.Scan(
&i.ID,
&i.WikiID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
)
return i, err
}
const getWikiContentByWikiID = `-- name: GetWikiContentByWikiID :many
SELECT id, title, created_at
FROM wiki_content
WHERE wiki_id = $1 AND is_deleted = false
ORDER BY created_at DESC
`
type GetWikiContentByWikiIDRow struct {
ID pgtype.UUID `json:"id"`
Title string `json:"title"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) GetWikiContentByWikiID(ctx context.Context, wikiID pgtype.UUID) ([]GetWikiContentByWikiIDRow, error) {
rows, err := q.db.Query(ctx, getWikiContentByWikiID, wikiID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetWikiContentByWikiIDRow{}
for rows.Next() {
var i GetWikiContentByWikiIDRow
if err := rows.Scan(&i.ID, &i.Title, &i.CreatedAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWikiContentByWikiIDs = `-- name: GetWikiContentByWikiIDs :many
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
`
type GetWikiContentByWikiIDsRow struct {
ID pgtype.UUID `json:"id"`
WikiID pgtype.UUID `json:"wiki_id"`
Title string `json:"title"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) GetWikiContentByWikiIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]GetWikiContentByWikiIDsRow, error) {
rows, err := q.db.Query(ctx, getWikiContentByWikiIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetWikiContentByWikiIDsRow{}
for rows.Next() {
var i GetWikiContentByWikiIDsRow
if err := rows.Scan(
&i.ID,
&i.WikiID,
&i.Title,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWikiContentCount = `-- name: GetWikiContentCount :one
SELECT COUNT(*)
FROM wiki_content
WHERE wiki_id = $1
`
func (q *Queries) GetWikiContentCount(ctx context.Context, wikiID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, getWikiContentCount, wikiID)
var count int64
err := row.Scan(&count)
return count, err
}
const getWikisByIDs = `-- name: GetWikisByIDs :many
SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false
SELECT id, project_id, title, slug, is_deleted, created_at, updated_at FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false
`
func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Wiki, error) {
@@ -215,7 +383,6 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -231,7 +398,7 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
}
const getWikisByProjectId = `-- name: GetWikisByProjectId :many
SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at
SELECT id, project_id, title, slug, is_deleted, created_at, updated_at
FROM wikis
WHERE project_id = $1 AND is_deleted = false
`
@@ -250,7 +417,6 @@ func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -266,7 +432,7 @@ func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID
}
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
SELECT id, project_id, title, slug, 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) {
@@ -283,7 +449,6 @@ func (q *Queries) GetWikisBySlugs(ctx context.Context, dollar_1 []string) ([]Wik
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -299,7 +464,7 @@ func (q *Queries) GetWikisBySlugs(ctx context.Context, dollar_1 []string) ([]Wik
}
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
SELECT w.id, w.project_id, w.title, w.slug, w.is_deleted, w.created_at, w.updated_at
FROM wikis w
WHERE w.is_deleted = false
AND ($1::uuid IS NULL OR w.project_id = $1::uuid)
@@ -347,7 +512,6 @@ func (q *Queries) SearchWikis(ctx context.Context, arg SearchWikisParams) ([]Wik
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -367,16 +531,14 @@ UPDATE wikis
SET
title = COALESCE($1, title),
slug = COALESCE($2, slug),
content = COALESCE($3, content),
project_id = COALESCE($4, project_id)
WHERE id = $5 AND is_deleted = false
RETURNING id, project_id, title, slug, content, is_deleted, created_at, updated_at
project_id = COALESCE($3, project_id)
WHERE id = $4 AND is_deleted = false
RETURNING id, project_id, title, slug, is_deleted, created_at, updated_at
`
type UpdateWikiParams struct {
Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"`
ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"`
}
@@ -385,7 +547,6 @@ func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, e
row := q.db.QueryRow(ctx, updateWiki,
arg.Title,
arg.Slug,
arg.Content,
arg.ProjectID,
arg.ID,
)
@@ -395,7 +556,6 @@ func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, e
&i.ProjectID,
&i.Title,
&i.Slug,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,

View File

@@ -5,30 +5,46 @@ import (
"time"
)
type WikiEntity struct {
type WikiContentSample struct {
ID string `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"`
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
type WikiEntity struct {
ID string `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
ContentSample []WikiContentSample `json:"content_sample"`
ProjectID string `json:"project_id"`
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
}
var contentSample []response.WikiContentSample
for _, c := range w.ContentSample {
contentSample = append(contentSample, response.WikiContentSample{
ID: c.ID,
Title: c.Title,
CreatedAt: c.CreatedAt,
})
}
return &response.WikiResponse{
ID: w.ID,
Title: w.Title,
Slug: w.Slug,
Content: w.Content,
ProjectID: w.ProjectID,
IsDeleted: w.IsDeleted,
CreatedAt: w.CreatedAt,
UpdatedAt: w.UpdatedAt,
ID: w.ID,
Title: w.Title,
Slug: w.Slug,
ContentSample: contentSample,
ProjectID: w.ProjectID,
IsDeleted: w.IsDeleted,
CreatedAt: w.CreatedAt,
UpdatedAt: w.UpdatedAt,
}
}
@@ -45,3 +61,12 @@ func WikisEntityToResponse(ws []*WikiEntity) []*response.WikiResponse {
}
return out
}
type WikiContentEntity struct {
ID string `json:"id"`
WikiID string `json:"wiki_id"`
Title string `json:"title"`
Content string `json:"content"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
}

View File

@@ -32,6 +32,10 @@ type WikiRepository interface {
BulkDeleteEntityWikisByWikiID(ctx context.Context, wikiID pgtype.UUID) error
DeleteEntityWiki(ctx context.Context, entityID pgtype.UUID, wikiID pgtype.UUID) error
DeleteEntityWikisByProjectID(ctx context.Context, projectID pgtype.UUID) error
CreateContent(ctx context.Context, params sqlc.CreateWikiContentParams) (*models.WikiContentEntity, error)
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)
WithTx(tx pgx.Tx) WikiRepository
}
@@ -93,7 +97,6 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string)
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),
@@ -101,6 +104,20 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string)
}
dbMap[item.ID] = &item
}
samples, err := r.q.GetWikiContentByWikiIDs(ctx, missingPgIds)
if err == nil {
for _, sample := range samples {
wikiID := convert.UUIDToString(sample.WikiID)
if item, ok := dbMap[wikiID]; ok {
item.ContentSample = append(item.ContentSample, models.WikiContentSample{
ID: convert.UUIDToString(sample.ID),
Title: sample.Title,
CreatedAt: convert.TimeToPtr(sample.CreatedAt),
})
}
}
}
}
}
@@ -147,12 +164,24 @@ func (r *wikiRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.W
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),
}
// Fetch content samples
samples, err := r.q.GetWikiContentByWikiID(ctx, row.ID)
if err == nil {
for _, sample := range samples {
wiki.ContentSample = append(wiki.ContentSample, models.WikiContentSample{
ID: convert.UUIDToString(sample.ID),
Title: sample.Title,
CreatedAt: convert.TimeToPtr(sample.CreatedAt),
})
}
}
_ = r.c.Set(ctx, cacheId, wiki, constants.NormalCacheDuration)
return &wiki, nil
@@ -178,7 +207,6 @@ func (r *wikiRepository) Search(ctx context.Context, params sqlc.SearchWikisPara
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),
@@ -209,7 +237,6 @@ func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParam
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),
@@ -228,7 +255,6 @@ func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParam
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),
@@ -285,7 +311,6 @@ func (r *wikiRepository) GetByProjectID(ctx context.Context, projectID pgtype.UU
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),
@@ -350,7 +375,6 @@ func (r *wikiRepository) GetBySlug(ctx context.Context, slug string) (*models.Wi
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),
@@ -390,7 +414,6 @@ func (r *wikiRepository) GetBySlugs(ctx context.Context, slugs []string) ([]*mod
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),
@@ -421,3 +444,117 @@ func (r *wikiRepository) GetBySlugs(ctx context.Context, slugs []string) ([]*mod
return wikis, nil
}
func (r *wikiRepository) CreateContent(ctx context.Context, params sqlc.CreateWikiContentParams) (*models.WikiContentEntity, error) {
row, err := r.q.CreateWikiContent(ctx, params)
if err != nil {
return nil, err
}
return &models.WikiContentEntity{
ID: convert.UUIDToString(row.ID),
WikiID: convert.UUIDToString(row.WikiID),
Title: row.Title,
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}, nil
}
func (r *wikiRepository) GetContentCountByWikiID(ctx context.Context, wikiID pgtype.UUID) (int64, error) {
return r.q.GetWikiContentCount(ctx, wikiID)
}
func (r *wikiRepository) getContentByIDsWithFallback(ctx context.Context, ids []string) ([]*models.WikiContentEntity, error) {
if len(ids) == 0 {
return []*models.WikiContentEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("wiki_content:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var contents []*models.WikiContentEntity
missingToCache := make(map[string]any)
var missingPgIds []pgtype.UUID
for i, b := range raws {
if len(b) == 0 {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err == nil {
missingPgIds = append(missingPgIds, pgId)
}
}
}
dbMap := make(map[string]*models.WikiContentEntity)
if len(missingPgIds) > 0 {
dbRows, err := r.q.GetWikiContentByIDs(ctx, missingPgIds)
if err == nil {
for _, row := range dbRows {
item := models.WikiContentEntity{
ID: convert.UUIDToString(row.ID),
WikiID: convert.UUIDToString(row.WikiID),
Title: row.Title,
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
dbMap[item.ID] = &item
}
}
}
for i, b := range raws {
if len(b) > 0 {
var u models.WikiContentEntity
if err := json.Unmarshal(b, &u); err == nil {
contents = append(contents, &u)
}
} else {
if item, ok := dbMap[ids[i]]; ok {
contents = append(contents, item)
missingToCache[keys[i]] = item
}
}
}
if len(missingToCache) > 0 {
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
}
return contents, nil
}
func (r *wikiRepository) GetContentByID(ctx context.Context, id pgtype.UUID) (*models.WikiContentEntity, error) {
cacheId := fmt.Sprintf("wiki_content:id:%s", convert.UUIDToString(id))
var content models.WikiContentEntity
err := r.c.Get(ctx, cacheId, &content)
if err == nil {
_ = r.c.Set(ctx, cacheId, content, constants.NormalCacheDuration)
return &content, nil
}
row, err := r.q.GetWikiContentById(ctx, id)
if err != nil {
return nil, err
}
content = models.WikiContentEntity{
ID: convert.UUIDToString(row.ID),
WikiID: convert.UUIDToString(row.WikiID),
Title: row.Title,
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}
_ = r.c.Set(ctx, cacheId, content, constants.NormalCacheDuration)
return &content, nil
}
func (r *wikiRepository) GetContentByIDs(ctx context.Context, ids []string) ([]*models.WikiContentEntity, error) {
return r.getContentByIDsWithFallback(ctx, ids)
}

View File

@@ -11,6 +11,7 @@ func WikiRoutes(router fiber.Router, wikiController *controllers.WikiController)
wiki.Get("/", wikiController.SearchWikis)
wiki.Get("/slug/exists", wikiController.IsExistWikiSlug)
wiki.Get("/slug/:slug", wikiController.GetWikiBySlug)
wiki.Get("/content/:id", wikiController.GetWikiContentById)
wiki.Get("/:id", wikiController.GetWikiById)
}

View File

@@ -512,12 +512,29 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
ID: wikiUUID,
Title: convert.StringToText(wiki.Title),
Slug: convert.PtrToText(wiki.Slug),
Content: convert.StringToText(wiki.Doc),
ProjectID: projectUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+err.Error())
}
count, err := s.wikiRepo.GetContentCountByWikiID(ctx, wikiUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get wiki content count: "+err.Error())
}
versionTitle := fmt.Sprintf("Version %d", count+1)
_, err = wikiRepo.CreateContent(ctx, sqlc.CreateWikiContentParams{
WikiID: wikiUUID,
Title: versionTitle,
Content: convert.StringToText(wiki.Doc),
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki content: "+err.Error())
}
_ = s.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wikiUUID.String()), fmt.Sprintf("wiki:slug:%s", *wiki.Slug))
newWikis = append(newWikis, snapshotData.Wikis[i])
} else if wiki.Source == "inline" {
@@ -525,12 +542,23 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
ID: wikiUUID,
Title: convert.StringToText(wiki.Title),
Slug: convert.PtrToText(wiki.Slug),
Content: convert.StringToText(wiki.Doc),
ProjectID: projectUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+err.Error())
}
_, err = wikiRepo.CreateContent(ctx, sqlc.CreateWikiContentParams{
WikiID: wikiUUID,
Title: "Version 1",
Content: convert.StringToText(wiki.Doc),
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki content: "+err.Error())
}
_ = s.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wikiUUID.String()), fmt.Sprintf("wiki:slug:%s", *wiki.Slug))
newWikis = append(newWikis, snapshotData.Wikis[i])
} else if wiki.Source == "ref" {

View File

@@ -19,6 +19,7 @@ type WikiService interface {
GetWikiBySlug(ctx context.Context, slug string) (*response.WikiResponse, *fiber.Error)
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)
}
type wikiService struct {
@@ -107,3 +108,22 @@ func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDt
return models.WikisEntityToResponse(wikis), nil
}
func (s *wikiService) GetWikiContentByID(ctx context.Context, id string) (*response.WikiContentResponse, *fiber.Error) {
contentId, err := convert.StringToUUID(id)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid content ID format")
}
content, err := s.wikiRepo.GetContentByID(ctx, contentId)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Wiki content not found")
}
return &response.WikiContentResponse{
ID: content.ID,
WikiID: content.WikiID,
Title: content.Title,
Content: content.Content,
CreatedAt: content.CreatedAt,
}, nil
}