content

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
const MaxCustomFieldFilters = 10

MaxCustomFieldFilters limits the number of custom field filters per query

View Source
const (
	// RoleAdmin represents the admin role used for authorization checks
	RoleAdmin = "Admin"
)

Variables

View Source
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 is returned when user doesn't have permission to access content
	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")
)
View Source
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

func ValidateCommentText(comment string) error

ValidateCommentText validates the comment text field

func ValidateContent

func ValidateContent(content string) error

ValidateContent validates the content field as TipTap JSON

func ValidateCustomFieldFilter

func ValidateCustomFieldFilter(f CustomFieldFilter) error

ValidateCustomFieldFilter validates a single custom field filter

func ValidateMetaDescription

func ValidateMetaDescription(value string) error

ValidateMetaDescription validates the meta description field

func ValidateOGDescription

func ValidateOGDescription(value string) error

ValidateOGDescription validates the OG description field

func ValidateOGTitle

func ValidateOGTitle(value string) error

ValidateOGTitle validates the OG title field

func ValidateSlug

func ValidateSlug(slug string) error

ValidateSlug validates the slug field

func ValidateTags

func ValidateTags(tags []string) ([]string, error)

ValidateTags validates and normalizes the tags field. Returns the sanitized tags or an error.

func ValidateTitle

func ValidateTitle(title string) error

ValidateTitle validates the title field

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 (s *Service) ApproveComment(ctx context.Context, commentID int) error

func (*Service) AuthorExists

func (s *Service) AuthorExists(ctx context.Context, username string) (bool, error)

func (*Service) Create

func (s *Service) Create(ctx context.Context, userID int, req CreateContentRequest) (*Content, error)

func (*Service) DeleteComment

func (s *Service) DeleteComment(ctx context.Context, commentID int) error

func (*Service) DeleteCommentsByUserID

func (s *Service) DeleteCommentsByUserID(ctx context.Context, userID int) error

func (*Service) DeleteContent

func (s *Service) DeleteContent(ctx context.Context, id int, userID int, role string) error

func (*Service) DeleteOwnComment

func (s *Service) DeleteOwnComment(ctx context.Context, commentID int, userID int) error

func (*Service) GenerateSlug

func (s *Service) GenerateSlug(title string) string

func (*Service) GenerateSlugFromTitle

func (s *Service) GenerateSlugFromTitle(ctx context.Context, title string) (string, error)

func (*Service) GetAll

func (s *Service) GetAll(ctx context.Context, limit int, offset int) ([]*Content, error)

func (*Service) GetByID

func (s *Service) GetByID(ctx context.Context, id int) (*Content, error)

func (*Service) GetBySlug

func (s *Service) GetBySlug(ctx context.Context, slug string) (*Content, error)

func (*Service) GetByUser

func (s *Service) GetByUser(ctx context.Context, userID int, limit int, offset int) ([]*Content, error)

func (*Service) GetComment

func (s *Service) GetComment(ctx context.Context, commentID int) (*Comment, error)

func (*Service) GetCommentsByUserID

func (s *Service) GetCommentsByUserID(ctx context.Context, userID int) ([]*Comment, error)

func (*Service) GetCommentsForContent

func (s *Service) GetCommentsForContent(ctx context.Context, contentID int) ([]*Comment, error)

func (*Service) GetCommentsForModeration

func (s *Service) GetCommentsForModeration(ctx context.Context, contentID int) ([]*Comment, error)

func (*Service) GetPublished

func (s *Service) GetPublished(ctx context.Context, limit int, offset int) ([]*Content, error)

func (*Service) GetPublishedByAuthorUsername

func (s *Service) GetPublishedByAuthorUsername(ctx context.Context, username string, limit int, offset int) ([]*Content, error)

func (*Service) GetPublishedByID

func (s *Service) GetPublishedByID(ctx context.Context, id int) (*Content, error)

func (*Service) GetPublishedByPostType

func (s *Service) GetPublishedByPostType(ctx context.Context, postType string, limit int, offset int) ([]*Content, error)

func (*Service) GetPublishedBySlug

func (s *Service) GetPublishedBySlug(ctx context.Context, slug string, language string) (*Content, error)

func (*Service) GetPublishedBySlugAny

func (s *Service) GetPublishedBySlugAny(ctx context.Context, slug string) (*Content, error)

func (*Service) GetPublishedByTag

func (s *Service) GetPublishedByTag(ctx context.Context, tag string, limit int, offset int) ([]*Content, error)

func (*Service) GetPublishedCustomPostTypes

func (s *Service) GetPublishedCustomPostTypes(ctx context.Context) ([]string, error)

func (*Service) GetPublishedPages

func (s *Service) GetPublishedPages(ctx context.Context) ([]*Content, error)

func (*Service) GetTranslations

func (s *Service) GetTranslations(ctx context.Context, translationGroupID int, excludeID int) ([]*Content, error)

func (*Service) ListByCursor

func (s *Service) ListByCursor(ctx context.Context, userID int, limit int, beforeID int, filters ContentFilters) ([]*Content, error)

func (*Service) ListByFilters

func (s *Service) ListByFilters(ctx context.Context, userID int, filters ContentFilters) ([]*Content, error)

func (*Service) MarkAsSpam

func (s *Service) MarkAsSpam(ctx context.Context, commentID int) error

func (*Service) Publish

func (s *Service) Publish(ctx context.Context, id int, userID int, role string) (*Content, error)

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 (s *Service) RejectComment(ctx context.Context, commentID int) error

func (*Service) SearchPublished

func (s *Service) SearchPublished(ctx context.Context, query string, limit int) ([]*Content, error)

func (*Service) SetSystemFields

func (s *Service) SetSystemFields(
	ctx context.Context,
	contentID int,
	systemFields map[string]any,
) (*Content, error)

func (*Service) SubmitComment

func (s *Service) SubmitComment(ctx context.Context, contentID int, userID int, req CreateCommentRequest) (*Comment, error)

func (*Service) Unpublish

func (s *Service) Unpublish(ctx context.Context, id int, userID int, role string) (*Content, error)

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) Update

func (s *Service) Update(ctx context.Context, id int, userID int, role string, req UpdateContentRequest) (*Content, error)

func (*Service) UpdateCommentStatus

func (s *Service) UpdateCommentStatus(ctx context.Context, commentID int, status CommentStatus) error

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 Status

type Status string

Status represents the publication status of content

const (
	// StatusDraft represents content that is not yet published
	StatusDraft Status = "draft"
	// StatusPublished represents content that is published
	StatusPublished Status = "published"
)

func (Status) IsValid

func (s Status) IsValid() bool

IsValid checks if the status is valid

func (Status) String

func (s Status) String() string

String returns the string representation of the status

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

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL