Documentation
¶
Index ¶
- Constants
- Variables
- func ValidateCommentStatus(status CommentStatus) error
- func ValidateCommentText(comment string) error
- func ValidateContent(content string) error
- func ValidateCustomFieldFilter(f CustomFieldFilter) error
- func ValidateMetaDescription(value string) error
- func ValidateOGDescription(value string) error
- func ValidateOGTitle(value string) error
- func ValidateSlug(slug string) error
- func ValidateTags(tags []string) ([]string, error)
- func ValidateTitle(title string) error
- type Comment
- type CommentRepository
- type CommentStatus
- type Content
- type ContentFilters
- type CreateCommentRequest
- type CreateContentRequest
- type CustomFieldFilter
- type FilterOperator
- type HookExecutor
- type PostType
- type PostTypeAdapter
- type PostTypeServiceInterface
- type Repository
- type Service
- func (s *Service) ApproveComment(ctx context.Context, commentID int) error
- func (s *Service) AuthorExists(ctx context.Context, username string) (bool, error)
- func (s *Service) Create(ctx context.Context, userID int, req CreateContentRequest) (*Content, error)
- func (s *Service) DeleteComment(ctx context.Context, commentID int) error
- func (s *Service) DeleteCommentsByUserID(ctx context.Context, userID int) error
- func (s *Service) DeleteContent(ctx context.Context, id int, userID int, role string) error
- func (s *Service) DeleteOwnComment(ctx context.Context, commentID int, userID int) error
- func (s *Service) GenerateSlug(title string) string
- func (s *Service) GenerateSlugFromTitle(ctx context.Context, title string) (string, error)
- func (s *Service) GetAll(ctx context.Context, limit int, offset int) ([]*Content, error)
- func (s *Service) GetByID(ctx context.Context, id int) (*Content, error)
- func (s *Service) GetBySlug(ctx context.Context, slug string) (*Content, error)
- func (s *Service) GetByUser(ctx context.Context, userID int, limit int, offset int) ([]*Content, error)
- func (s *Service) GetComment(ctx context.Context, commentID int) (*Comment, error)
- func (s *Service) GetCommentsByUserID(ctx context.Context, userID int) ([]*Comment, error)
- func (s *Service) GetCommentsForContent(ctx context.Context, contentID int) ([]*Comment, error)
- func (s *Service) GetCommentsForModeration(ctx context.Context, contentID int) ([]*Comment, error)
- func (s *Service) GetPublished(ctx context.Context, limit int, offset int) ([]*Content, error)
- func (s *Service) GetPublishedByAuthorUsername(ctx context.Context, username string, limit int, offset int) ([]*Content, error)
- func (s *Service) GetPublishedByID(ctx context.Context, id int) (*Content, error)
- func (s *Service) GetPublishedByPostType(ctx context.Context, postType string, limit int, offset int) ([]*Content, error)
- func (s *Service) GetPublishedBySlug(ctx context.Context, slug string, language string) (*Content, error)
- func (s *Service) GetPublishedBySlugAny(ctx context.Context, slug string) (*Content, error)
- func (s *Service) GetPublishedByTag(ctx context.Context, tag string, limit int, offset int) ([]*Content, error)
- func (s *Service) GetPublishedCustomPostTypes(ctx context.Context) ([]string, error)
- func (s *Service) GetPublishedPages(ctx context.Context) ([]*Content, error)
- func (s *Service) GetTranslations(ctx context.Context, translationGroupID int, excludeID int) ([]*Content, error)
- func (s *Service) ListByCursor(ctx context.Context, userID int, limit int, beforeID int, ...) ([]*Content, error)
- func (s *Service) ListByFilters(ctx context.Context, userID int, filters ContentFilters) ([]*Content, error)
- func (s *Service) MarkAsSpam(ctx context.Context, commentID int) error
- func (s *Service) Publish(ctx context.Context, id int, userID int, role string) (*Content, error)
- func (s *Service) RejectComment(ctx context.Context, commentID int) error
- func (s *Service) SearchPublished(ctx context.Context, query string, limit int) ([]*Content, error)
- func (s *Service) SetSystemFields(ctx context.Context, contentID int, systemFields map[string]any) (*Content, error)
- func (s *Service) SubmitComment(ctx context.Context, contentID int, userID int, req CreateCommentRequest) (*Comment, error)
- func (s *Service) Unpublish(ctx context.Context, id int, userID int, role string) (*Content, error)
- func (s *Service) Update(ctx context.Context, id int, userID int, role string, req UpdateContentRequest) (*Content, error)
- func (s *Service) UpdateCommentStatus(ctx context.Context, commentID int, status CommentStatus) error
- type ServiceInterface
- type Status
- type UpdateCommentStatusRequest
- type UpdateContentRequest
Constants ¶
const MaxCustomFieldFilters = 10
MaxCustomFieldFilters limits the number of custom field filters per query
const (
// RoleAdmin represents the admin role used for authorization checks
RoleAdmin = "Admin"
)
Variables ¶
var ( // ErrContentNotFound is returned when content cannot be found ErrContentNotFound = errors.New("content not found") // ErrInvalidTitle is returned when title validation fails ErrInvalidTitle = errors.New("title is required and must be between 1 and 200 characters") // ErrInvalidContent is returned when content validation fails ErrInvalidContent = errors.New("content must be less than 100000 characters") // ErrInvalidStatus is returned when status is not valid ErrInvalidStatus = errors.New("status must be either 'draft' or 'published'") // ErrInvalidSlug is returned when slug validation fails ErrInvalidSlug = errors.New("slug must be between 1 and 200 characters and contain only lowercase letters, numbers, and hyphens") // ErrSlugAlreadyExists is returned when slug already exists ErrSlugAlreadyExists = errors.New("slug already exists") ErrUnauthorized = errors.New("unauthorized access to content") // ErrCommentNotFound is returned when comment cannot be found ErrCommentNotFound = errors.New("comment not found") // ErrInvalidCommentText is returned when comment text validation fails ErrInvalidCommentText = errors.New("comment text is required and must be between 1 and 2000 characters") // ErrInvalidCommentStatus is returned when comment status is not valid ErrInvalidCommentStatus = errors.New("comment status must be one of: pending, approved, rejected, spam") // ErrHTMLInTitle is returned when title contains HTML ErrHTMLInTitle = errors.New("title must not contain HTML tags") // ErrInvalidTipTapContent is returned when content is not valid TipTap JSON ErrInvalidTipTapContent = errors.New("content must be valid TipTap JSON structure") // ErrHTMLInPlainText is returned when a plain text field contains HTML ErrHTMLInPlainText = errors.New("field must not contain HTML tags") // ErrInvalidFilterField is returned when a custom field filter has an empty field name ErrInvalidFilterField = errors.New("filter field must not be empty") // ErrInvalidFilterOperator is returned when a custom field filter has an unrecognized operator ErrInvalidFilterOperator = errors.New("invalid filter operator") // ErrInvalidFilterValue is returned when a custom field filter has an empty value ErrInvalidFilterValue = errors.New("filter value must not be empty") // ErrUnknownSystemFieldKey is returned when a system field key is not in the schema ErrUnknownSystemFieldKey = errors.New("unknown system field key") // ErrSystemFieldValidation is returned when a system field value fails schema validation ErrSystemFieldValidation = errors.New("system field validation failed") // ErrCustomFieldValidation is returned when a custom field value fails schema validation // (missing required field, wrong type, out-of-range, etc.). It wraps the per-field // detail so HTTP layers can map it to a 400 VALIDATION_ERROR via errors.Is. ErrCustomFieldValidation = errors.New("custom field validation failed") // ErrTranslationGroupNotFound is returned when a translation group ID does not exist ErrTranslationGroupNotFound = errors.New("translation group not found") // ErrTranslationAlreadyExists is returned when a translation for a language already exists in a group ErrTranslationAlreadyExists = errors.New("translation for this language already exists in this group") // ErrInvalidLanguage is returned when the language code is not in the configured languages ErrInvalidLanguage = errors.New("language is not in the configured languages list") )
var ErrPostTypeNotFound = errors.New("post type not found")
ErrPostTypeNotFound is returned when a post type is not found
Functions ¶
func ValidateCommentStatus ¶
func ValidateCommentStatus(status CommentStatus) error
ValidateCommentStatus validates the comment status
func ValidateCommentText ¶
ValidateCommentText validates the comment text field
func ValidateContent ¶
ValidateContent validates the content field as TipTap JSON
func ValidateCustomFieldFilter ¶
func ValidateCustomFieldFilter(f CustomFieldFilter) error
ValidateCustomFieldFilter validates a single custom field filter
func ValidateMetaDescription ¶
ValidateMetaDescription validates the meta description field
func ValidateOGDescription ¶
ValidateOGDescription validates the OG description field
func ValidateOGTitle ¶
ValidateOGTitle validates the OG title field
func ValidateTags ¶
ValidateTags validates and normalizes the tags field. Returns the sanitized tags or an error.
Types ¶
type Comment ¶
type Comment struct {
ID int `json:"id"`
ContentID int `json:"contentId"`
UserID int `json:"userId"`
Comment string `json:"comment"`
Status CommentStatus `json:"status"`
Author string `json:"author,omitempty"`
Username string `json:"username,omitempty"`
Role string `json:"role,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
Comment represents a comment on a content item
type CommentRepository ¶
type CommentRepository interface {
Create(ctx context.Context, comment *Comment) error
GetByContentID(ctx context.Context, contentID int) ([]*Comment, error)
GetByContentIDForModeration(ctx context.Context, contentID int) ([]*Comment, error)
GetByID(ctx context.Context, id int) (*Comment, error)
GetByUserID(ctx context.Context, userID int) ([]*Comment, error)
UpdateStatus(ctx context.Context, id int, status CommentStatus) error
Delete(ctx context.Context, id int) error
DeleteByUserID(ctx context.Context, userID int) error
}
CommentRepository defines the interface for comment repository operations
type CommentStatus ¶
type CommentStatus string
CommentStatus represents the moderation status of a comment
const ( // CommentStatusPending represents a comment awaiting moderation CommentStatusPending CommentStatus = "pending" // CommentStatusApproved represents an approved comment CommentStatusApproved CommentStatus = "approved" // CommentStatusRejected represents a rejected comment CommentStatusRejected CommentStatus = "rejected" // CommentStatusSpam represents a comment marked as spam CommentStatusSpam CommentStatus = "spam" )
func (CommentStatus) IsApproved ¶
func (s CommentStatus) IsApproved() bool
IsApproved returns true if the comment status is approved
func (CommentStatus) IsPending ¶
func (s CommentStatus) IsPending() bool
IsPending returns true if the comment status is pending
func (CommentStatus) IsRejected ¶
func (s CommentStatus) IsRejected() bool
IsRejected returns true if the comment status is rejected
func (CommentStatus) IsSpam ¶
func (s CommentStatus) IsSpam() bool
IsSpam returns true if the comment status is spam
func (CommentStatus) IsValid ¶
func (s CommentStatus) IsValid() bool
IsValid checks if the comment status is valid
func (CommentStatus) String ¶
func (s CommentStatus) String() string
String returns the string representation of the comment status
type Content ¶
type Content struct {
ID int `json:"id"`
UserID int `json:"userId"`
Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"`
Tags []string `json:"tags"`
Status Status `json:"status"`
PostType string `json:"postType"`
MetaDescription string `json:"metaDescription,omitempty"`
OGTitle string `json:"ogTitle,omitempty"`
OGDescription string `json:"ogDescription,omitempty"`
Author string `json:"author,omitempty"`
Username string `json:"username,omitempty"`
AllowComments bool `json:"allowComments"`
CustomFields map[string]any `json:"customFields,omitempty"`
UpdatedBy int `json:"updatedBy,omitempty"`
UpdatedByUsername string `json:"updatedByUsername,omitempty"`
Language string `json:"language"`
TranslationGroupID *int `json:"translationGroupId,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
Content represents a content item in the system
type ContentFilters ¶
type ContentFilters struct {
Limit int
Offset int
PostType string
Search string
Language string
Status string
Tags []string
Author string
CustomFieldFilters []CustomFieldFilter
}
ContentFilters holds filter parameters for listing content. All string fields use the empty value as "no filter"; a non-empty value enables the corresponding WHERE clause. Tags is AND-of-tags: a content item must carry every tag in the slice to match.
Status validation (draft/published) is the caller's responsibility — the handler rejects unknown values before constructing the filter, so a raw ContentFilters{Status: "garbage"} would still pass through this struct.
Author matches the joined users.name (with users.username as a fallback), case-insensitive equality. The agent v1 surface only honors it for admins; the agent handler returns 403 to non-admins before the filter is built.
type CreateCommentRequest ¶
type CreateCommentRequest struct {
Comment string `json:"comment"`
}
CreateCommentRequest represents a request to create a comment
type CreateContentRequest ¶
type CreateContentRequest struct {
Title string `json:"title"`
Content string `json:"content"`
Tags []string `json:"tags"`
Status Status `json:"status"`
PostType string `json:"postType"`
MetaDescription string `json:"metaDescription,omitempty"`
OGTitle string `json:"ogTitle,omitempty"`
OGDescription string `json:"ogDescription,omitempty"`
AllowComments *bool `json:"allowComments,omitempty"`
CustomFields map[string]any `json:"customFields,omitempty"`
Language string `json:"language,omitempty"`
TranslationGroupID *int `json:"translationGroupId,omitempty"`
}
CreateContentRequest represents a request to create content
type CustomFieldFilter ¶
type CustomFieldFilter struct {
Field string
Operator FilterOperator
Value string
}
CustomFieldFilter represents a filter on a custom field
type FilterOperator ¶
type FilterOperator string
FilterOperator represents the type of filter operation
const ( // FilterOpEqual filters for exact match FilterOpEqual FilterOperator = "equal" // FilterOpMin filters for minimum value (inclusive) FilterOpMin FilterOperator = "min" // FilterOpMax filters for maximum value (inclusive) FilterOpMax FilterOperator = "max" )
type HookExecutor ¶
type HookExecutor interface {
Execute(ctx context.Context, hookName plugin.HookName, data []byte) ([]byte, error)
}
HookExecutor executes plugin hooks during content lifecycle operations.
type PostType ¶
type PostType struct {
Slug string
}
PostType represents a minimal post type for validation purposes
type PostTypeAdapter ¶
type PostTypeAdapter struct {
// contains filtered or unexported fields
}
PostTypeAdapter wraps posttype.Service to implement content.PostTypeServiceInterface
func NewPostTypeAdapter ¶
func NewPostTypeAdapter(service *posttype.Service) *PostTypeAdapter
NewPostTypeAdapter creates a new adapter for post type service
func (*PostTypeAdapter) GetBySlug ¶
func (a *PostTypeAdapter) GetBySlug(slug string) (PostType, error)
GetBySlug retrieves a post type by slug and converts it to content.PostType
func (*PostTypeAdapter) GetFieldsByPostType ¶
func (a *PostTypeAdapter) GetFieldsByPostType(slug string) ([]customfield.FieldSchema, error)
func (*PostTypeAdapter) GetSystemFieldsByPostType ¶
func (a *PostTypeAdapter) GetSystemFieldsByPostType(slug string) ([]customfield.FieldSchema, error)
type PostTypeServiceInterface ¶
type PostTypeServiceInterface interface {
GetBySlug(slug string) (PostType, error)
GetFieldsByPostType(slug string) ([]customfield.FieldSchema, error)
GetSystemFieldsByPostType(slug string) ([]customfield.FieldSchema, error)
}
PostTypeServiceInterface defines the interface for post type service This allows the content service to validate post types without tight coupling
type Repository ¶
type Repository interface {
Create(ctx context.Context, content *Content) error
GetBySlug(ctx context.Context, slug string, language string) (*Content, error)
GetByUser(ctx context.Context, userID int, limit int, offset int) ([]*Content, error)
GetAll(ctx context.Context, limit int, offset int) ([]*Content, error)
// ListByCursor returns the caller's content in newest-first (id DESC) order using
// keyset pagination. beforeID <= 0 means "first page"; otherwise only rows with a
// strictly smaller id are returned. The optional filters restrict which rows
// qualify: empty string / nil fields are no-ops, so a zero ContentFilters value
// behaves exactly as the old unfiltered call. Tags is AND-of-tags. Author matches
// the joined users.name (with users.username as a fallback), case-insensitive.
// It is additive — the offset-based GetAll / GetByUser / ListByFilters methods are
// untouched (the agent v1 list contract is cursor-only; offset is unstable under
// concurrent inserts/deletes).
ListByCursor(ctx context.Context, userID int, limit int, beforeID int, filters ContentFilters) ([]*Content, error)
CheckSlugUnique(ctx context.Context, slug string, language string) (bool, error)
GetByID(ctx context.Context, id int) (*Content, error)
Update(ctx context.Context, content *Content) error
GetPublished(ctx context.Context, limit int, offset int) ([]*Content, error)
GetPublishedBySlug(ctx context.Context, slug string, language string) (*Content, error)
GetPublishedByAuthorUsername(ctx context.Context, username string, limit int, offset int) ([]*Content, error)
AuthorExists(ctx context.Context, username string) (bool, error)
Delete(ctx context.Context, id int, userID int) error
DeleteByID(ctx context.Context, id int) error
ListByFilters(ctx context.Context, userID int, filters ContentFilters) ([]*Content, error)
GetPublishedPages(ctx context.Context) ([]*Content, error)
GetPublishedCustomPostTypes(ctx context.Context) ([]string, error)
GetPublishedByPostType(ctx context.Context, postType string, limit int, offset int) ([]*Content, error)
GetPublishedByTag(ctx context.Context, tag string, limit int, offset int) ([]*Content, error)
SearchPublished(ctx context.Context, query string, limit int) ([]*Content, error)
// GetTranslations returns all content items in the same translation group, excluding the given content ID.
GetTranslations(ctx context.Context, translationGroupID int, excludeID int) ([]*Content, error)
// TranslationGroupExists checks whether a content item with the given ID exists.
TranslationGroupExists(ctx context.Context, id int) (bool, error)
// GetPublishedBySlugAny finds published content by slug regardless of language.
GetPublishedBySlugAny(ctx context.Context, slug string) (*Content, error)
}
Repository defines the interface for content repository operations
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service handles content business logic
func NewService ¶
func NewService( repo Repository, seoService *seo.Service, postTypeService PostTypeServiceInterface, ) *Service
func NewServiceWithComments ¶
func NewServiceWithComments( repo Repository, commentRepo CommentRepository, seoService *seo.Service, postTypeService PostTypeServiceInterface, ) *Service
func NewServiceWithHooks ¶
func NewServiceWithHooks( repo Repository, commentRepo CommentRepository, seoService *seo.Service, postTypeService PostTypeServiceInterface, hookExecutor HookExecutor, ) *Service
func (*Service) ApproveComment ¶
func (*Service) AuthorExists ¶
func (*Service) DeleteComment ¶
func (*Service) DeleteCommentsByUserID ¶
func (*Service) DeleteContent ¶
func (*Service) DeleteOwnComment ¶
func (*Service) GenerateSlug ¶
func (*Service) GenerateSlugFromTitle ¶
func (*Service) GetComment ¶
func (*Service) GetCommentsByUserID ¶
func (*Service) GetCommentsForContent ¶
func (*Service) GetCommentsForModeration ¶
func (*Service) GetPublished ¶
func (*Service) GetPublishedByAuthorUsername ¶
func (*Service) GetPublishedByID ¶
func (*Service) GetPublishedByPostType ¶
func (*Service) GetPublishedBySlug ¶
func (*Service) GetPublishedBySlugAny ¶
func (*Service) GetPublishedByTag ¶
func (*Service) GetPublishedCustomPostTypes ¶
func (*Service) GetPublishedPages ¶
func (*Service) GetTranslations ¶
func (*Service) ListByCursor ¶
func (*Service) ListByFilters ¶
func (*Service) Publish ¶
Publish flips a content item's status to StatusPublished via transitionStatus. Ownership/role checks mirror Update: a non-admin caller must be the owner or the service returns ErrUnauthorized. An already-published post is a no-op (200 idempotent: the row is persisted unchanged, no hook fires, no SEO regen runs) — only the actual draft→published edge triggers SEO + hook work.
Errors:
- ErrContentNotFound when the id does not exist
- ErrUnauthorized when the caller is neither owner nor admin
- wrapped repo / SEO errors on persistence / generation failure
func (*Service) RejectComment ¶
func (*Service) SearchPublished ¶
func (*Service) SetSystemFields ¶
func (*Service) SubmitComment ¶
func (*Service) Unpublish ¶
Unpublish flips a content item's status to StatusDraft via transitionStatus. Same ownership/role contract as Publish. An already-draft post is a no-op (idempotent: row is persisted unchanged, no hook fires, no SEO regen runs).
Errors:
- ErrContentNotFound when the id does not exist
- ErrUnauthorized when the caller is neither owner nor admin
- wrapped repo errors on persistence failure
func (*Service) UpdateCommentStatus ¶
type ServiceInterface ¶
type ServiceInterface interface {
SubmitComment(ctx context.Context, contentID int, userID int, req CreateCommentRequest) (*Comment, error)
GetComment(ctx context.Context, commentID int) (*Comment, error)
GetCommentsForContent(ctx context.Context, contentID int) ([]*Comment, error)
GetCommentsForModeration(ctx context.Context, contentID int) ([]*Comment, error)
GetCommentsByUserID(ctx context.Context, userID int) ([]*Comment, error)
UpdateCommentStatus(ctx context.Context, commentID int, status CommentStatus) error
DeleteComment(ctx context.Context, commentID int) error
DeleteOwnComment(ctx context.Context, commentID int, userID int) error
GetPublishedBySlug(ctx context.Context, slug string, language string) (*Content, error)
GetTranslations(ctx context.Context, translationGroupID int, excludeID int) ([]*Content, error)
}
ServiceInterface defines the interface for the content service
type UpdateCommentStatusRequest ¶
type UpdateCommentStatusRequest struct {
Status CommentStatus `json:"status"`
}
UpdateCommentStatusRequest represents a request to update comment status
type UpdateContentRequest ¶
type UpdateContentRequest struct {
Title string `json:"title"`
Content string `json:"content"`
Tags []string `json:"tags"`
Status Status `json:"status"`
PostType string `json:"postType"`
MetaDescription string `json:"metaDescription,omitempty"`
OGTitle string `json:"ogTitle,omitempty"`
OGDescription string `json:"ogDescription,omitempty"`
AllowComments *bool `json:"allowComments,omitempty"`
CustomFields map[string]any `json:"customFields,omitempty"`
Language string `json:"language,omitempty"`
TranslationGroupID *int `json:"translationGroupId,omitempty"`
}
UpdateContentRequest represents a request to update content