diff --git a/db/migrations/000008_wiki.up.sql b/db/migrations/000008_wiki.up.sql index d07f0ea..ec34fb3 100644 --- a/db/migrations/000008_wiki.up.sql +++ b/db/migrations/000008_wiki.up.sql @@ -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() diff --git a/db/migrations/000016_wiki_content.down.sql b/db/migrations/000016_wiki_content.down.sql new file mode 100644 index 0000000..0a30a1a --- /dev/null +++ b/db/migrations/000016_wiki_content.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS wiki_content; diff --git a/db/migrations/000016_wiki_content.up.sql b/db/migrations/000016_wiki_content.up.sql new file mode 100644 index 0000000..0e6bd25 --- /dev/null +++ b/db/migrations/000016_wiki_content.up.sql @@ -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); diff --git a/db/query/wiki.sql b/db/query/wiki.sql index 0c57f36..3310526 100644 --- a/db/query/wiki.sql +++ b/db/query/wiki.sql @@ -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; diff --git a/db/schema.sql b/db/schema.sql index 7dd52f3..db26285 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -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, diff --git a/internal/controllers/wikiController.go b/internal/controllers/wikiController.go index ea16c1e..65dde24 100644 --- a/internal/controllers/wikiController.go +++ b/internal/controllers/wikiController.go @@ -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, + }) +} + diff --git a/internal/dtos/response/wiki.go b/internal/dtos/response/wiki.go index b34b7dc..4ee9fbe 100644 --- a/internal/dtos/response/wiki.go +++ b/internal/dtos/response/wiki.go @@ -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"` } diff --git a/internal/gen/sqlc/models.go b/internal/gen/sqlc/models.go index 63e5539..3edb291 100644 --- a/internal/gen/sqlc/models.go +++ b/internal/gen/sqlc/models.go @@ -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"` +} diff --git a/internal/gen/sqlc/wiki.sql.go b/internal/gen/sqlc/wiki.sql.go index f263c47..763c9bd 100644 --- a/internal/gen/sqlc/wiki.sql.go +++ b/internal/gen/sqlc/wiki.sql.go @@ -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, diff --git a/internal/models/wiki.go b/internal/models/wiki.go index 8c8900c..5103d4e 100644 --- a/internal/models/wiki.go +++ b/internal/models/wiki.go @@ -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"` +} diff --git a/internal/repositories/wikiRepository.go b/internal/repositories/wikiRepository.go index f6de4e3..cf6a3ee 100644 --- a/internal/repositories/wikiRepository.go +++ b/internal/repositories/wikiRepository.go @@ -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) +} diff --git a/internal/routes/wikiRoute.go b/internal/routes/wikiRoute.go index 6a3dcfc..cf03a11 100644 --- a/internal/routes/wikiRoute.go +++ b/internal/routes/wikiRoute.go @@ -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) } diff --git a/internal/services/submissionService.go b/internal/services/submissionService.go index c768593..79e7c5a 100644 --- a/internal/services/submissionService.go +++ b/internal/services/submissionService.go @@ -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" { diff --git a/internal/services/wikiService.go b/internal/services/wikiService.go index 2b902c8..566e6d5 100644 --- a/internal/services/wikiService.go +++ b/internal/services/wikiService.go @@ -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 +}