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:
@@ -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"`
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user