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, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT, title TEXT,
slug TEXT, slug TEXT,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false, is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(), created_at TIMESTAMPTZ DEFAULT now(),
updated_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 -- name: CreateWiki :one
INSERT INTO wikis ( INSERT INTO wikis (
id, title, slug, content, project_id id, title, slug, project_id
) VALUES ( ) VALUES (
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4 COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3
) )
RETURNING *; RETURNING *;
@@ -16,7 +16,6 @@ UPDATE wikis
SET SET
title = COALESCE(sqlc.narg('title'), title), title = COALESCE(sqlc.narg('title'), title),
slug = COALESCE(sqlc.narg('slug'), slug), slug = COALESCE(sqlc.narg('slug'), slug),
content = COALESCE(sqlc.narg('content'), content),
project_id = COALESCE(sqlc.narg('project_id'), project_id) project_id = COALESCE(sqlc.narg('project_id'), project_id)
WHERE id = sqlc.arg('id') AND is_deleted = false WHERE id = sqlc.arg('id') AND is_deleted = false
RETURNING *; RETURNING *;
@@ -93,3 +92,38 @@ WHERE slug = $1 AND is_deleted = false;
-- name: GetWikisBySlugs :many -- name: GetWikisBySlugs :many
SELECT * FROM wikis WHERE slug = ANY($1::text[]) AND is_deleted = false; 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, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT, title TEXT,
slug TEXT UNIQUE, slug TEXT UNIQUE,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false, is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(), created_at TIMESTAMPTZ DEFAULT now(),
updated_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 ( CREATE TABLE IF NOT EXISTS entity_wikis (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE, entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
wiki_id UUID REFERENCES wikis(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" "time"
) )
type WikiResponse struct { type WikiContentSample struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title,omitempty"` Title string `json:"title"`
Slug string `json:"slug,omitempty"` CreatedAt *time.Time `json:"created_at"`
Content string `json:"content,omitempty"` }
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted,omitempty"` type WikiResponse struct {
CreatedAt *time.Time `json:"created_at,omitempty"` ID string `json:"id"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` 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"` ProjectID pgtype.UUID `json:"project_id"`
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"` Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_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 const createWiki = `-- name: CreateWiki :one
INSERT INTO wikis ( INSERT INTO wikis (
id, title, slug, content, project_id id, title, slug, project_id
) VALUES ( ) 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 { type CreateWikiParams struct {
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"` Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"`
ProjectID pgtype.UUID `json:"project_id"` ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"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, row := q.db.QueryRow(ctx, createWiki,
arg.Title, arg.Title,
arg.Slug, arg.Slug,
arg.Content,
arg.ProjectID, arg.ProjectID,
arg.ID, arg.ID,
) )
@@ -97,7 +95,6 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
&i.ProjectID, &i.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -105,6 +102,41 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
return i, err 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 const deleteEntityWiki = `-- name: DeleteEntityWiki :exec
DELETE FROM entity_wikis DELETE FROM entity_wikis
WHERE entity_id = $1 AND wiki_id = $2 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 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 FROM wikis
WHERE id = $1 AND is_deleted = false 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.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -176,7 +207,7 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error)
} }
const getWikiBySlug = `-- name: GetWikiBySlug :one 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 FROM wikis
WHERE slug = $1 AND is_deleted = false 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.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -197,8 +227,146 @@ func (q *Queries) GetWikiBySlug(ctx context.Context, slug pgtype.Text) (Wiki, er
return i, err 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 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) { 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.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -231,7 +398,7 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
} }
const getWikisByProjectId = `-- name: GetWikisByProjectId :many 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 FROM wikis
WHERE project_id = $1 AND is_deleted = false 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.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -266,7 +432,7 @@ func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID
} }
const getWikisBySlugs = `-- name: GetWikisBySlugs :many 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) { 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.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -299,7 +464,7 @@ func (q *Queries) GetWikisBySlugs(ctx context.Context, dollar_1 []string) ([]Wik
} }
const searchWikis = `-- name: SearchWikis :many 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 FROM wikis w
WHERE w.is_deleted = false WHERE w.is_deleted = false
AND ($1::uuid IS NULL OR w.project_id = $1::uuid) 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.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -367,16 +531,14 @@ UPDATE wikis
SET SET
title = COALESCE($1, title), title = COALESCE($1, title),
slug = COALESCE($2, slug), slug = COALESCE($2, slug),
content = COALESCE($3, content), project_id = COALESCE($3, project_id)
project_id = COALESCE($4, project_id) WHERE id = $4 AND is_deleted = false
WHERE id = $5 AND is_deleted = false RETURNING id, project_id, title, slug, is_deleted, created_at, updated_at
RETURNING id, project_id, title, slug, content, is_deleted, created_at, updated_at
` `
type UpdateWikiParams struct { type UpdateWikiParams struct {
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"` Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"`
ProjectID pgtype.UUID `json:"project_id"` ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"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, row := q.db.QueryRow(ctx, updateWiki,
arg.Title, arg.Title,
arg.Slug, arg.Slug,
arg.Content,
arg.ProjectID, arg.ProjectID,
arg.ID, arg.ID,
) )
@@ -395,7 +556,6 @@ func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, e
&i.ProjectID, &i.ProjectID,
&i.Title, &i.Title,
&i.Slug, &i.Slug,
&i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,

View File

@@ -5,30 +5,46 @@ import (
"time" "time"
) )
type WikiEntity struct { type WikiContentSample struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` 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"` 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 { func (w *WikiEntity) ToResponse() *response.WikiResponse {
if w == nil { if w == nil {
return 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{ return &response.WikiResponse{
ID: w.ID, ID: w.ID,
Title: w.Title, Title: w.Title,
Slug: w.Slug, Slug: w.Slug,
Content: w.Content, ContentSample: contentSample,
ProjectID: w.ProjectID, ProjectID: w.ProjectID,
IsDeleted: w.IsDeleted, IsDeleted: w.IsDeleted,
CreatedAt: w.CreatedAt, CreatedAt: w.CreatedAt,
UpdatedAt: w.UpdatedAt, UpdatedAt: w.UpdatedAt,
} }
} }
@@ -45,3 +61,12 @@ func WikisEntityToResponse(ws []*WikiEntity) []*response.WikiResponse {
} }
return out 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 BulkDeleteEntityWikisByWikiID(ctx context.Context, wikiID pgtype.UUID) error
DeleteEntityWiki(ctx context.Context, entityID pgtype.UUID, wikiID pgtype.UUID) error DeleteEntityWiki(ctx context.Context, entityID pgtype.UUID, wikiID pgtype.UUID) error
DeleteEntityWikisByProjectID(ctx context.Context, projectID 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 WithTx(tx pgx.Tx) WikiRepository
} }
@@ -93,7 +97,6 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string)
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -101,6 +104,20 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string)
} }
dbMap[item.ID] = &item 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), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), 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) _ = r.c.Set(ctx, cacheId, wiki, constants.NormalCacheDuration)
return &wiki, nil return &wiki, nil
@@ -178,7 +207,6 @@ func (r *wikiRepository) Search(ctx context.Context, params sqlc.SearchWikisPara
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -209,7 +237,6 @@ func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParam
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -228,7 +255,6 @@ func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParam
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -285,7 +311,6 @@ func (r *wikiRepository) GetByProjectID(ctx context.Context, projectID pgtype.UU
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), 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), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -390,7 +414,6 @@ func (r *wikiRepository) GetBySlugs(ctx context.Context, slugs []string) ([]*mod
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -421,3 +444,117 @@ func (r *wikiRepository) GetBySlugs(ctx context.Context, slugs []string) ([]*mod
return wikis, nil 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("/", wikiController.SearchWikis)
wiki.Get("/slug/exists", wikiController.IsExistWikiSlug) wiki.Get("/slug/exists", wikiController.IsExistWikiSlug)
wiki.Get("/slug/:slug", wikiController.GetWikiBySlug) wiki.Get("/slug/:slug", wikiController.GetWikiBySlug)
wiki.Get("/content/:id", wikiController.GetWikiContentById)
wiki.Get("/:id", wikiController.GetWikiById) wiki.Get("/:id", wikiController.GetWikiById)
} }

View File

@@ -512,12 +512,29 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
ID: wikiUUID, ID: wikiUUID,
Title: convert.StringToText(wiki.Title), Title: convert.StringToText(wiki.Title),
Slug: convert.PtrToText(wiki.Slug), Slug: convert.PtrToText(wiki.Slug),
Content: convert.StringToText(wiki.Doc),
ProjectID: projectUUID, ProjectID: projectUUID,
}) })
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+err.Error()) 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]) newWikis = append(newWikis, snapshotData.Wikis[i])
} else if wiki.Source == "inline" { } else if wiki.Source == "inline" {
@@ -525,12 +542,23 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
ID: wikiUUID, ID: wikiUUID,
Title: convert.StringToText(wiki.Title), Title: convert.StringToText(wiki.Title),
Slug: convert.PtrToText(wiki.Slug), Slug: convert.PtrToText(wiki.Slug),
Content: convert.StringToText(wiki.Doc),
ProjectID: projectUUID, ProjectID: projectUUID,
}) })
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+err.Error()) 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]) newWikis = append(newWikis, snapshotData.Wikis[i])
} else if wiki.Source == "ref" { } else if wiki.Source == "ref" {

View File

@@ -19,6 +19,7 @@ type WikiService interface {
GetWikiBySlug(ctx context.Context, slug string) (*response.WikiResponse, *fiber.Error) GetWikiBySlug(ctx context.Context, slug string) (*response.WikiResponse, *fiber.Error)
IsExistWikiSlug(ctx context.Context, slug string) (bool, *fiber.Error) IsExistWikiSlug(ctx context.Context, slug string) (bool, *fiber.Error)
SearchWikis(ctx context.Context, req *request.SearchWikiDto) ([]*response.WikiResponse, *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 { type wikiService struct {
@@ -107,3 +108,22 @@ func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDt
return models.WikisEntityToResponse(wikis), nil 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
}