Add CMS Management Functionality

- Introduced a new CMS management feature, including the ability to create, retrieve, update, search, and delete CMS entries.
- Implemented the CMS entity, repository, service, and handler layers following Clean Architecture principles.
- Added API endpoints for CMS operations in Swagger documentation, ensuring comprehensive API specifications.
- Enhanced error handling and validation for CMS data, improving robustness and user experience.
- Updated the main application to initialize the CMS repository and service, integrating them into the existing system.
This commit is contained in:
n.nakhostin
2025-11-03 13:20:58 +03:30
parent 875447ac58
commit 9a444a1e7d
9 changed files with 1021 additions and 13 deletions
+110
View File
@@ -0,0 +1,110 @@
package cms
import (
"tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
)
// CMS represents the content management system for the landing page
type CMS struct {
mongo.Model `bson:",inline"`
Key string `bson:"key" json:"key" validate:"required"`
Hero heroSection `bson:"hero" json:"hero"`
Chart chartSection `bson:"chart" json:"chart"`
Features cardSection `bson:"features" json:"features"`
Challenges cardSection `bson:"challenges" json:"challenges"`
Advantages cardSection `bson:"advantages" json:"advantages"`
Contact contactSection `bson:"contact" json:"contact"`
Footer footerSection `bson:"footer" json:"footer"`
}
// SetID sets the CMS ID (implements IDSetter interface)
func (c *CMS) SetID(id string) {
c.ID, _ = bson.ObjectIDFromHex(id)
}
// GetID returns the CMS ID (implements IDGetter interface)
func (c *CMS) GetID() string {
return c.ID.Hex()
}
// SetCreatedAt sets the created timestamp (implements Timestamp interface)
func (c *CMS) SetCreatedAt(timestamp int64) {
c.CreatedAt = timestamp
}
// SetUpdatedAt sets the updated timestamp (implements Timestamp interface)
func (c *CMS) SetUpdatedAt(timestamp int64) {
c.UpdatedAt = timestamp
}
// GetCreatedAt returns the created timestamp (implements Timestamp interface)
func (c *CMS) GetCreatedAt() int64 {
return c.CreatedAt
}
// GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
func (c *CMS) GetUpdatedAt() int64 {
return c.UpdatedAt
}
// heroSection represents the hero section of the landing page
type heroSection struct {
Title string `bson:"title"`
Description string `bson:"description"`
ButtonText string `bson:"button_text"`
ButtonLink string `bson:"button_link"`
GifFile string `bson:"gif_file"`
}
// chartSection represents the chart section of the landing page
type chartSection struct {
Title string `bson:"title" json:"title"`
MissedAmount string `bson:"missed_amount" json:"missedAmount"`
Data []chartPoint `bson:"data" json:"data"`
}
type chartPoint struct {
Key string `bson:"key" json:"key"`
Value int `bson:"value" json:"value"`
}
// cardSection represents the card section of the landing page
type cardSection struct {
Title string `bson:"title"`
Description string `bson:"description"`
Cards []card `bson:"cards"`
}
// card represents a card in the card section
type card struct {
Icon string `bson:"icon"`
Title string `bson:"title"`
Description string `bson:"description"`
}
// contactSection represents the contact section of the landing page
type contactSection struct {
Title string `bson:"title"`
Description string `bson:"description"`
SubmitButtonText string `bson:"submit_button_text"`
Fields []formField `bson:"fields"`
}
// formField represents a form field in the contact section
type formField struct {
Name string `bson:"name"`
Label string `bson:"label"`
Placeholder string `bson:"placeholder"`
Required bool `bson:"required"`
}
// footerSection represents the footer section of the landing page
type footerSection struct {
Email string `bson:"email"`
Phone string `bson:"phone"`
Location string `bson:"location"`
Tagline string `bson:"tagline"`
Copyright string `bson:"copyright"`
}
+13
View File
@@ -0,0 +1,13 @@
package cms
import "errors"
// Custom errors for cms domain
var (
ErrCMSNotFound = errors.New("cms not found")
ErrCMSKeyRequired = errors.New("cms key is required")
ErrCMSKeyExists = errors.New("cms with this key already exists")
ErrInvalidCMSData = errors.New("invalid cms data")
ErrUnauthorizedAccess = errors.New("unauthorized access to cms")
ErrInvalidKeyFormat = errors.New("invalid cms key format")
)
+120
View File
@@ -0,0 +1,120 @@
package cms
import "tm/pkg/response"
// CMSForm represents the form for creating/updating the CMS
type CMSForm struct {
Key string `json:"key" valid:"required,length(2|100)" example:"SW-001"`
Hero heroSectionForm `json:"hero" valid:"optional"`
Chart chartSectionForm `json:"chart" valid:"optional"`
Features cardSectionForm `json:"features" valid:"optional"`
Challenges cardSectionForm `json:"challenges" valid:"optional"`
Advantages cardSectionForm `json:"advantages" valid:"optional"`
Contact contactSectionForm `json:"contact" valid:"optional"`
Footer footerSectionForm `json:"footer" valid:"optional"`
}
// heroSectionForm represents the form for creating/updating the hero section
type heroSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Hero Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Hero Description"`
ButtonText string `json:"button_text" valid:"optional,length(2|100)" example:"Button Text"`
ButtonLink string `json:"button_link" valid:"optional,url" example:"https://example.com"`
GifFile string `json:"gif_file" valid:"optional,url" example:"https://example.com/gif.gif"`
}
// chartSectionForm represents the form for creating/updating the chart section
type chartSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Chart Title"`
MissedAmount string `json:"missed_amount" valid:"optional,length(2|100)" example:"Missed Amount"`
Data []chartPointForm `json:"data" valid:"optional"`
}
// chartPointForm represents the form for creating/updating a chart point
type chartPointForm struct {
Key string `json:"key" valid:"optional,length(2|100)" example:"Key"`
Value int `json:"value" valid:"optional,range(0|1000000)" example:"1000000"`
}
// cardSectionForm represents the form for creating/updating the card section
type cardSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
Cards []cardForm `json:"cards" valid:"optional"`
}
// cardForm represents the form for creating/updating a card
type cardForm struct {
Icon string `json:"icon" valid:"optional,url" example:"https://example.com/icon.png"`
Title string `json:"title" valid:"optional,length(2|100)" example:"Card Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Card Description"`
}
// contactSectionForm represents the form for creating/updating the contact section
type contactSectionForm struct {
Title string `json:"title" valid:"optional,length(2|100)" example:"Contact Title"`
Description string `json:"description" valid:"optional,length(2|1000)" example:"Contact Description"`
SubmitButtonText string `json:"submit_button_text" valid:"optional,length(2|100)" example:"Submit Button Text"`
Fields []formFieldForm `json:"fields" valid:"optional"`
}
// formFieldForm represents the form for creating/updating a form field
type formFieldForm struct {
Name string `json:"name" valid:"optional,length(2|100)" example:"Name"`
Label string `json:"label" valid:"optional,length(2|100)" example:"Label"`
Placeholder string `json:"placeholder" valid:"optional,length(2|100)" example:"Placeholder"`
Required bool `json:"required" valid:"optional" example:"true"`
}
// footerSectionForm represents the form for creating/updating the footer section
type footerSectionForm struct {
Email string `json:"email" valid:"optional,email" example:"info@example.com"`
Phone string `json:"phone" valid:"optional,length(10|20)" example:"+1234567890"`
Location string `json:"location" valid:"optional,length(2|100)" example:"Location"`
Tagline string `json:"tagline" valid:"optional,length(2|100)" example:"Tagline"`
Copyright string `json:"copyright" valid:"optional,length(2|100)" example:"Copyright"`
}
// SearchCMSForm represents the form for searching CMS entries
type SearchCMSForm struct {
Search *string `query:"q" valid:"optional"`
Key *string `query:"key" valid:"optional,length(2|100)"`
}
// CMSResponse represents the CMS data sent in API responses
type CMSResponse struct {
ID string `json:"id"`
Key string `json:"key"`
Hero heroSection `json:"hero"`
Chart chartSection `json:"chart"`
Features cardSection `json:"features"`
Challenges cardSection `json:"challenges"`
Advantages cardSection `json:"advantages"`
Contact contactSection `json:"contact"`
Footer footerSection `json:"footer"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// CMSListResponse represents the response for listing CMS entries
type CMSListResponse struct {
CMS []*CMSResponse `json:"cms"`
Meta *response.Meta `json:"-"`
}
// ToResponse converts CMS to CMSResponse
func (c *CMS) ToResponse() *CMSResponse {
return &CMSResponse{
ID: c.GetID(),
Key: c.Key,
Hero: c.Hero,
Chart: c.Chart,
Features: c.Features,
Challenges: c.Challenges,
Advantages: c.Advantages,
Contact: c.Contact,
Footer: c.Footer,
CreatedAt: c.GetCreatedAt(),
UpdatedAt: c.GetUpdatedAt(),
}
}
+190
View File
@@ -0,0 +1,190 @@
package cms
import (
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for CMS operations
type Handler struct {
service Service
}
// NewHandler creates a new CMS handler
func NewHandler(service Service) *Handler {
return &Handler{
service: service,
}
}
// GetByKey handles retrieving CMS content by key (public API)
// @Summary Get CMS content by key
// @Description Retrieve CMS content for public display by its key
// @Tags CMS
// @Accept json
// @Produce json
// @Param key path string true "CMS key"
// @Success 200 {object} response.APIResponse{data=CMSResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/cms/{key} [get]
func (h *Handler) GetByKey(c echo.Context) error {
key := c.Param("key")
cms, err := h.service.GetByKey(c.Request().Context(), key)
if err != nil {
return response.NotFound(c, "CMS content not found")
}
return response.Success(c, cms, "CMS content retrieved successfully")
}
// Create handles creating a new CMS entry (admin only)
// @Summary Create a new CMS entry
// @Description Create a new CMS entry for content management (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param cms body CMSForm true "CMS information"
// @Success 201 {object} response.APIResponse{data=CMSResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 409 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms [post]
func (h *Handler) Create(c echo.Context) error {
form, err := response.Parse[CMSForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Create CMS
cms, err := h.service.Create(c.Request().Context(), form)
if err != nil {
if err == ErrCMSKeyExists {
return response.Conflict(c, "CMS with this key already exists")
}
return response.InternalServerError(c, "Failed to create CMS")
}
return response.Created(c, cms, "CMS entry created successfully")
}
// GetByID handles retrieving a CMS entry by ID (admin only)
// @Summary Get CMS by ID
// @Description Retrieve a CMS entry by its ID for editing (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param id path string true "CMS ID"
// @Success 200 {object} response.APIResponse{data=CMSResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms/{id} [get]
func (h *Handler) GetByID(c echo.Context) error {
id := c.Param("id")
cms, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err.Error() == "cms not found" {
return response.NotFound(c, "CMS entry not found")
}
return response.InternalServerError(c, "Failed to get CMS entry")
}
return response.Success(c, cms, "CMS entry retrieved successfully")
}
// Update handles updating a CMS entry (admin only)
// @Summary Update CMS entry
// @Description Update an existing CMS entry by its ID (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param id path string true "CMS ID"
// @Param cms body CMSForm true "Updated CMS information"
// @Success 200 {object} response.APIResponse{data=CMSResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 409 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms/{id} [put]
func (h *Handler) Update(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[CMSForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Update CMS
cms, err := h.service.Update(c.Request().Context(), id, form)
if err != nil {
if err == ErrCMSKeyExists {
return response.Conflict(c, "CMS with this key already exists")
}
if err.Error() == "cms not found" {
return response.NotFound(c, "CMS entry not found")
}
return response.InternalServerError(c, "Failed to update CMS entry")
}
return response.Success(c, cms, "CMS entry updated successfully")
}
// Delete handles deleting a CMS entry (admin only)
// @Summary Delete CMS entry
// @Description Delete a CMS entry by its ID (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param id path string true "CMS ID"
// @Success 200 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms/{id} [delete]
func (h *Handler) Delete(c echo.Context) error {
id := c.Param("id")
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "cms not found" {
return response.NotFound(c, "CMS entry not found")
}
return response.InternalServerError(c, "Failed to delete CMS entry")
}
return response.Success(c, nil, "CMS entry deleted successfully")
}
// Search handles searching CMS entries (admin only)
// @Summary Search CMS entries
// @Description Search CMS entries with filters and pagination (admin only)
// @Tags Admin-CMS
// @Accept json
// @Produce json
// @Param q query string false "Search query"
// @Param key query string false "CMS key filter"
// @Param limit query integer false "Number of CMS entries per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of CMS entries to skip for pagination" minimum(0) default(0)
// @Param sort_by query string false "Field to sort by" Enums(key, created_at, updated_at) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=CMSListResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/cms [get]
func (h *Handler) Search(c echo.Context) error {
form, err := response.Parse[SearchCMSForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination := response.NewPagination(c)
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
return response.InternalServerError(c, "Failed to search CMS entries")
}
return response.SuccessWithMeta(c, result, result.Meta, "CMS entries retrieved successfully")
}
+195
View File
@@ -0,0 +1,195 @@
package cms
import (
"context"
"errors"
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
)
// Repository defines methods for CMS data access
type Repository interface {
Create(ctx context.Context, cms *CMS) error
Update(ctx context.Context, cms *CMS) error
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*CMS, error)
GetByKey(ctx context.Context, key string) (*CMS, error)
Search(ctx context.Context, search *SearchCMSForm, pagination orm.Pagination) (*orm.PaginatedResult[CMS], error)
}
// cmsRepository implements the Repository interface using the MongoDB ORM
type cmsRepository struct {
ormRepo orm.Repository[CMS]
logger logger.Logger
}
// NewRepository creates a new CMS repository
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management
indexes := []orm.Index{
*orm.CreateTextIndex("cms_search_idx", "key"),
*orm.NewIndex("key_idx", bson.D{{Key: "key", Value: 1}}),
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
}
// Create indexes
err := mongoManager.CreateIndexes("cms", indexes)
if err != nil {
logger.Warn("Failed to create CMS indexes", map[string]interface{}{
"error": err.Error(),
})
}
// Create ORM repository
ormRepo := orm.NewRepository[CMS](mongoManager.GetCollection("cms"), logger)
return &cmsRepository{
ormRepo: ormRepo,
logger: logger,
}
}
// Create creates a new CMS entry
func (r *cmsRepository) Create(ctx context.Context, cms *CMS) error {
// Set created timestamp using Unix timestamp
now := time.Now().Unix()
cms.SetCreatedAt(now)
// Use ORM to create CMS
err := r.ormRepo.Create(ctx, cms)
if err != nil {
r.logger.Error("Failed to create CMS", map[string]interface{}{
"error": err.Error(),
"key": cms.Key,
})
return err
}
r.logger.Info("CMS created successfully", map[string]interface{}{
"id": cms.GetID(),
"key": cms.Key,
})
return nil
}
// Update updates a CMS entry
func (r *cmsRepository) Update(ctx context.Context, cms *CMS) error {
// Set updated timestamp using Unix timestamp
cms.SetUpdatedAt(time.Now().Unix())
// Use ORM to update CMS
err := r.ormRepo.Update(ctx, cms)
if err != nil {
r.logger.Error("Failed to update CMS", map[string]interface{}{
"error": err.Error(),
"id": cms.GetID(),
"key": cms.Key,
})
return err
}
r.logger.Info("CMS updated successfully", map[string]interface{}{
"id": cms.GetID(),
"key": cms.Key,
})
return nil
}
// GetByID retrieves a CMS entry by ID
func (r *cmsRepository) GetByID(ctx context.Context, id string) (*CMS, error) {
cms, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, ErrCMSNotFound
}
r.logger.Error("Failed to get CMS by ID", map[string]interface{}{
"error": err.Error(),
"id": id,
})
return nil, err
}
return cms, nil
}
// GetByKey retrieves a CMS entry by key
func (r *cmsRepository) GetByKey(ctx context.Context, key string) (*CMS, error) {
cms, err := r.ormRepo.FindOne(ctx, bson.M{"key": key})
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, ErrCMSNotFound
}
r.logger.Error("Failed to get CMS by key", map[string]interface{}{
"error": err.Error(),
"key": key,
})
return nil, err
}
return cms, nil
}
// Delete deletes a CMS entry
func (r *cmsRepository) Delete(ctx context.Context, id string) error {
// Get CMS first to log the key
cms, err := r.GetByID(ctx, id)
if err != nil {
r.logger.Error("Failed to delete CMS", map[string]interface{}{
"error": err.Error(),
"id": id,
})
if errors.Is(err, ErrCMSNotFound) {
return ErrCMSNotFound
}
return err
}
// Delete from database
err = r.ormRepo.Delete(ctx, id)
if err != nil {
r.logger.Error("Failed to delete CMS", map[string]interface{}{
"error": err.Error(),
"id": id,
"key": cms.Key,
})
return err
}
r.logger.Info("CMS deleted successfully", map[string]interface{}{
"id": id,
"key": cms.Key,
})
return nil
}
// Search searches CMS entries with filters and pagination
func (r *cmsRepository) Search(ctx context.Context, search *SearchCMSForm, pagination orm.Pagination) (*orm.PaginatedResult[CMS], error) {
filter := bson.M{}
if search.Search != nil && *search.Search != "" {
filter["$or"] = bson.A{
bson.M{"key": bson.M{"$regex": search.Search, "$options": "i"}},
}
}
if search.Key != nil && *search.Key != "" {
filter["key"] = bson.M{"$regex": search.Key, "$options": "i"}
}
// Use ORM to find CMS entries
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to search CMS entries", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return result, nil
}
+328
View File
@@ -0,0 +1,328 @@
package cms
import (
"context"
"fmt"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
)
// Service defines the interface for CMS business operations
type Service interface {
// Create creates a new CMS entry
Create(ctx context.Context, form *CMSForm) (*CMSResponse, error)
// GetByID retrieves a CMS entry by ID
GetByID(ctx context.Context, id string) (*CMSResponse, error)
// GetByKey retrieves a CMS entry by key
GetByKey(ctx context.Context, key string) (*CMSResponse, error)
// Update updates a CMS entry
Update(ctx context.Context, id string, form *CMSForm) (*CMSResponse, error)
// Delete deletes a CMS entry by ID
Delete(ctx context.Context, id string) error
// Search searches CMS entries with filters and pagination
Search(ctx context.Context, form *SearchCMSForm, pagination *response.Pagination) (*CMSListResponse, error)
}
// cmsService implements Service interface
type cmsService struct {
repo Repository
logger logger.Logger
}
// NewService creates a new CMS service
func NewService(repo Repository, logger logger.Logger) Service {
return &cmsService{
repo: repo,
logger: logger,
}
}
// Create creates a new CMS entry
func (s *cmsService) Create(ctx context.Context, form *CMSForm) (*CMSResponse, error) {
s.logger.Info("Creating new CMS entry", map[string]interface{}{
"key": form.Key,
})
// Check if CMS with this key already exists
existing, err := s.repo.GetByKey(ctx, form.Key)
if err == nil && existing != nil {
s.logger.Warn("CMS with this key already exists", map[string]interface{}{
"key": form.Key,
})
return nil, ErrCMSKeyExists
}
// Create CMS entity
cms := &CMS{
Key: form.Key,
}
// Copy form data to entity
s.copyFormToEntity(form, cms)
// Save to database
if err := s.repo.Create(ctx, cms); err != nil {
s.logger.Error("Failed to create CMS", map[string]interface{}{
"key": form.Key,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to create CMS: %w", err)
}
s.logger.Info("CMS entry created successfully", map[string]interface{}{
"id": cms.GetID(),
"key": form.Key,
})
return cms.ToResponse(), nil
}
// GetByID retrieves a CMS entry by ID
func (s *cmsService) GetByID(ctx context.Context, id string) (*CMSResponse, error) {
s.logger.Debug("Retrieving CMS by ID", map[string]interface{}{
"id": id,
})
cms, err := s.repo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get CMS by ID", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get CMS: %w", err)
}
return cms.ToResponse(), nil
}
// GetByKey retrieves a CMS entry by key
func (s *cmsService) GetByKey(ctx context.Context, key string) (*CMSResponse, error) {
s.logger.Debug("Retrieving CMS by key", map[string]interface{}{
"key": key,
})
cms, err := s.repo.GetByKey(ctx, key)
if err != nil {
s.logger.Error("Failed to get CMS by key", map[string]interface{}{
"key": key,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get CMS: %w", err)
}
return cms.ToResponse(), nil
}
// Update updates a CMS entry
func (s *cmsService) Update(ctx context.Context, id string, form *CMSForm) (*CMSResponse, error) {
s.logger.Info("Updating CMS entry", map[string]interface{}{
"id": id,
"key": form.Key,
})
// Get existing CMS
cms, err := s.repo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get existing CMS for update", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get CMS for update: %w", err)
}
// Check if key is being changed and if new key already exists
if cms.Key != form.Key {
existing, err := s.repo.GetByKey(ctx, form.Key)
if err == nil && existing != nil && existing.GetID() != id {
s.logger.Warn("CMS with this key already exists", map[string]interface{}{
"key": form.Key,
})
return nil, ErrCMSKeyExists
}
cms.Key = form.Key
}
// Copy form data to entity
s.copyFormToEntity(form, cms)
// Update in database
if err := s.repo.Update(ctx, cms); err != nil {
s.logger.Error("Failed to update CMS", map[string]interface{}{
"id": id,
"key": form.Key,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to update CMS: %w", err)
}
s.logger.Info("CMS entry updated successfully", map[string]interface{}{
"id": id,
"key": form.Key,
})
return cms.ToResponse(), nil
}
// Delete deletes a CMS entry by ID
func (s *cmsService) Delete(ctx context.Context, id string) error {
s.logger.Info("Deleting CMS entry", map[string]interface{}{
"id": id,
})
if err := s.repo.Delete(ctx, id); err != nil {
s.logger.Error("Failed to delete CMS", map[string]interface{}{
"id": id,
"error": err.Error(),
})
return fmt.Errorf("failed to delete CMS: %w", err)
}
s.logger.Info("CMS entry deleted successfully", map[string]interface{}{
"id": id,
})
return nil
}
// Search searches CMS entries with filters and pagination
func (s *cmsService) Search(ctx context.Context, form *SearchCMSForm, pagination *response.Pagination) (*CMSListResponse, error) {
s.logger.Debug("Searching CMS entries", map[string]interface{}{
"filters": form,
"pagination": pagination,
})
// Convert response.Pagination to orm.Pagination
ormPagination := orm.Pagination{
Limit: pagination.Limit,
Skip: pagination.Offset,
SortField: pagination.SortBy,
SortOrder: 1, // Default ascending
}
if pagination.SortOrder == "desc" {
ormPagination.SortOrder = -1
}
result, err := s.repo.Search(ctx, form, ormPagination)
if err != nil {
s.logger.Error("Failed to search CMS entries", map[string]interface{}{
"filters": form,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to search CMS entries: %w", err)
}
// Convert to response format
cmsResponses := make([]*CMSResponse, len(result.Items))
for i, cms := range result.Items {
cmsResponses[i] = cms.ToResponse()
}
s.logger.Info("CMS search completed", map[string]interface{}{
"total_count": result.TotalCount,
"items_count": len(result.Items),
})
return &CMSListResponse{
CMS: cmsResponses,
Meta: pagination.Response(result.TotalCount),
}, nil
}
// copyFormToEntity copies data from form to entity
func (s *cmsService) copyFormToEntity(form *CMSForm, cms *CMS) {
// Hero section
if form.Hero.Title != "" {
cms.Hero.Title = form.Hero.Title
}
if form.Hero.Description != "" {
cms.Hero.Description = form.Hero.Description
}
if form.Hero.ButtonText != "" {
cms.Hero.ButtonText = form.Hero.ButtonText
}
if form.Hero.ButtonLink != "" {
cms.Hero.ButtonLink = form.Hero.ButtonLink
}
if form.Hero.GifFile != "" {
cms.Hero.GifFile = form.Hero.GifFile
}
// Chart section
if form.Chart.Title != "" {
cms.Chart.Title = form.Chart.Title
}
if form.Chart.MissedAmount != "" {
cms.Chart.MissedAmount = form.Chart.MissedAmount
}
if len(form.Chart.Data) > 0 {
cms.Chart.Data = make([]chartPoint, len(form.Chart.Data))
for i, point := range form.Chart.Data {
cms.Chart.Data[i] = chartPoint(point)
}
}
// Features section
s.copyCardSectionForm(&form.Features, &cms.Features)
// Challenges section
s.copyCardSectionForm(&form.Challenges, &cms.Challenges)
// Advantages section
s.copyCardSectionForm(&form.Advantages, &cms.Advantages)
// Contact section
if form.Contact.Title != "" {
cms.Contact.Title = form.Contact.Title
}
if form.Contact.Description != "" {
cms.Contact.Description = form.Contact.Description
}
if form.Contact.SubmitButtonText != "" {
cms.Contact.SubmitButtonText = form.Contact.SubmitButtonText
}
if len(form.Contact.Fields) > 0 {
cms.Contact.Fields = make([]formField, len(form.Contact.Fields))
for i, field := range form.Contact.Fields {
cms.Contact.Fields[i] = formField(field)
}
}
// Footer section
if form.Footer.Email != "" {
cms.Footer.Email = form.Footer.Email
}
if form.Footer.Phone != "" {
cms.Footer.Phone = form.Footer.Phone
}
if form.Footer.Location != "" {
cms.Footer.Location = form.Footer.Location
}
if form.Footer.Tagline != "" {
cms.Footer.Tagline = form.Footer.Tagline
}
if form.Footer.Copyright != "" {
cms.Footer.Copyright = form.Footer.Copyright
}
}
// copyCardSectionForm copies card section form data to entity
func (s *cmsService) copyCardSectionForm(formSection *cardSectionForm, entitySection *cardSection) {
if formSection.Title != "" {
entitySection.Title = formSection.Title
}
if formSection.Description != "" {
entitySection.Description = formSection.Description
}
if len(formSection.Cards) > 0 {
entitySection.Cards = make([]card, len(formSection.Cards))
for i, cardForm := range formSection.Cards {
entitySection.Cards[i] = card(cardForm)
}
}
}
+6 -6
View File
@@ -21,7 +21,7 @@ func NewHandler(service Service) *Handler {
// Create handles creating a new contact message
// @Summary Create a new contact message
// @Description Create a new contact message from a user
// @Tags contacts
// @Tags Contacts
// @Accept json
// @Produce json
// @Param contact body CreateContactForm true "Contact information"
@@ -52,7 +52,7 @@ func (h *Handler) Create(c echo.Context) error {
// GetByID handles retrieving a contact by ID (admin only)
// @Summary Get contact by ID
// @Description Retrieve a contact message by its ID (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"
@@ -78,7 +78,7 @@ func (h *Handler) GetByID(c echo.Context) error {
// Search handles searching contacts with filters (admin only)
// @Summary Search contacts
// @Description Search contacts with filters and pagination (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param q query string false "Search query"
@@ -112,7 +112,7 @@ func (h *Handler) Search(c echo.Context) error {
// UpdateStatus handles updating contact status (admin only)
// @Summary Update contact status
// @Description Update the status of a contact message (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"
@@ -152,7 +152,7 @@ func (h *Handler) UpdateStatus(c echo.Context) error {
// GetStats handles retrieving contact statistics (admin only)
// @Summary Get contact statistics
// @Description Get contact statistics by status (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=ContactStats}
@@ -170,7 +170,7 @@ func (h *Handler) GetStats(c echo.Context) error {
// Delete handles deleting a contact (admin only)
// @Summary Delete contact
// @Description Delete a contact message by ID (admin only)
// @Tags contacts
// @Tags Admin-Contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact ID"