Implement public FAQ read endpoints and admin CRUD with RBAC, persistence, and migrations, then regenerate Swagger and add a complete Postman collection so frontend/admin teams can integrate and validate the feature end-to-end. Co-authored-by: Cursor <cursoragent@cursor.com>
200 lines
5.4 KiB
Go
200 lines
5.4 KiB
Go
package repository
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func NewFAQStore(s *Store) ports.FAQStore { return s }
|
|
|
|
func faqToDomain(
|
|
id int64,
|
|
question string,
|
|
answer string,
|
|
category pgtype.Text,
|
|
displayOrder int32,
|
|
status string,
|
|
createdAt pgtype.Timestamptz,
|
|
updatedAt pgtype.Timestamptz,
|
|
) domain.FAQ {
|
|
return domain.FAQ{
|
|
ID: id,
|
|
Question: question,
|
|
Answer: answer,
|
|
Category: fromPgText(category),
|
|
DisplayOrder: displayOrder,
|
|
Status: status,
|
|
CreatedAt: createdAt.Time,
|
|
UpdatedAt: timePtr(updatedAt),
|
|
}
|
|
}
|
|
|
|
func (s *Store) CreateFAQ(ctx context.Context, input domain.CreateFAQInput) (domain.FAQ, error) {
|
|
displayOrder := int32(0)
|
|
if input.DisplayOrder != nil {
|
|
displayOrder = *input.DisplayOrder
|
|
}
|
|
status := domain.FAQStatusActive
|
|
if input.Status != nil {
|
|
status = *input.Status
|
|
}
|
|
|
|
row := s.conn.QueryRow(ctx, `
|
|
INSERT INTO faqs (question, answer, category, display_order, status)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, question, answer, category, display_order, status, created_at, updated_at
|
|
`, input.Question, input.Answer, toPgText(input.Category), displayOrder, status)
|
|
|
|
var (
|
|
id int64
|
|
question string
|
|
answer string
|
|
category pgtype.Text
|
|
orderVal int32
|
|
faqStatus string
|
|
createdAt pgtype.Timestamptz
|
|
updatedAt pgtype.Timestamptz
|
|
)
|
|
if err := row.Scan(&id, &question, &answer, &category, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
return domain.FAQ{}, err
|
|
}
|
|
|
|
return faqToDomain(id, question, answer, category, orderVal, faqStatus, createdAt, updatedAt), nil
|
|
}
|
|
|
|
func (s *Store) UpdateFAQ(ctx context.Context, id int64, input domain.UpdateFAQInput) (domain.FAQ, error) {
|
|
categorySet := input.Category != nil
|
|
var categoryValue pgtype.Text
|
|
if categorySet {
|
|
if *input.Category == "" {
|
|
categoryValue = pgtype.Text{Valid: false}
|
|
} else {
|
|
categoryValue = pgtype.Text{String: *input.Category, Valid: true}
|
|
}
|
|
}
|
|
|
|
row := s.conn.QueryRow(ctx, `
|
|
UPDATE faqs
|
|
SET question = COALESCE($2, question),
|
|
answer = COALESCE($3, answer),
|
|
category = CASE WHEN $4::boolean THEN $5::text ELSE category END,
|
|
display_order = COALESCE($6, display_order),
|
|
status = COALESCE($7, status),
|
|
updated_at = NOW()
|
|
WHERE id = $1
|
|
RETURNING id, question, answer, category, display_order, status, created_at, updated_at
|
|
`,
|
|
id,
|
|
input.Question,
|
|
input.Answer,
|
|
categorySet,
|
|
categoryValue,
|
|
input.DisplayOrder,
|
|
input.Status,
|
|
)
|
|
|
|
var (
|
|
faqID int64
|
|
question string
|
|
answer string
|
|
rowCategory pgtype.Text
|
|
orderVal int32
|
|
faqStatus string
|
|
createdAt pgtype.Timestamptz
|
|
updatedAt pgtype.Timestamptz
|
|
)
|
|
if err := row.Scan(&faqID, &question, &answer, &rowCategory, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
return domain.FAQ{}, err
|
|
}
|
|
|
|
return faqToDomain(faqID, question, answer, rowCategory, orderVal, faqStatus, createdAt, updatedAt), nil
|
|
}
|
|
|
|
func (s *Store) GetFAQByID(ctx context.Context, id int64, includeInactive bool) (domain.FAQ, error) {
|
|
row := s.conn.QueryRow(ctx, `
|
|
SELECT id, question, answer, category, display_order, status, created_at, updated_at
|
|
FROM faqs
|
|
WHERE id = $1
|
|
AND ($2::boolean = TRUE OR status = 'ACTIVE')
|
|
`, id, includeInactive)
|
|
|
|
var (
|
|
faqID int64
|
|
question string
|
|
answer string
|
|
category pgtype.Text
|
|
orderVal int32
|
|
faqStatus string
|
|
createdAt pgtype.Timestamptz
|
|
updatedAt pgtype.Timestamptz
|
|
)
|
|
if err := row.Scan(&faqID, &question, &answer, &category, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
return domain.FAQ{}, err
|
|
}
|
|
|
|
return faqToDomain(faqID, question, answer, category, orderVal, faqStatus, createdAt, updatedAt), nil
|
|
}
|
|
|
|
func (s *Store) ListFAQs(ctx context.Context, status *string, category *string, limit int32, offset int32) ([]domain.FAQ, int64, error) {
|
|
rows, err := s.conn.Query(ctx, `
|
|
SELECT id, question, answer, category, display_order, status, created_at, updated_at
|
|
FROM faqs
|
|
WHERE ($1::text IS NULL OR status = $1)
|
|
AND ($2::text IS NULL OR category = $2)
|
|
ORDER BY display_order ASC, id ASC
|
|
LIMIT $3 OFFSET $4
|
|
`, toPgText(status), toPgText(category), limit, offset)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
faqs := make([]domain.FAQ, 0)
|
|
for rows.Next() {
|
|
var (
|
|
faqID int64
|
|
question string
|
|
answer string
|
|
rowCategory pgtype.Text
|
|
orderVal int32
|
|
faqStatus string
|
|
createdAt pgtype.Timestamptz
|
|
updatedAt pgtype.Timestamptz
|
|
)
|
|
if err := rows.Scan(&faqID, &question, &answer, &rowCategory, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
faqs = append(faqs, faqToDomain(faqID, question, answer, rowCategory, orderVal, faqStatus, createdAt, updatedAt))
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var totalCount int64
|
|
if err := s.conn.QueryRow(ctx, `
|
|
SELECT COUNT(*)
|
|
FROM faqs
|
|
WHERE ($1::text IS NULL OR status = $1)
|
|
AND ($2::text IS NULL OR category = $2)
|
|
`, toPgText(status), toPgText(category)).Scan(&totalCount); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return faqs, totalCount, nil
|
|
}
|
|
|
|
func (s *Store) DeleteFAQ(ctx context.Context, id int64) error {
|
|
cmd, err := s.conn.Exec(ctx, `DELETE FROM faqs WHERE id = $1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cmd.RowsAffected() == 0 {
|
|
return pgx.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|