commentable

package module
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: Feb 16, 2026 License: MIT Imports: 17 Imported by: 0

README

GoREST Commentable Plugin

Test Go Report Card License

A polymorphic commenting plugin for GoREST that allows adding comments to any resource type.

Features

  • Polymorphic Comments: Add comments to any resource type
  • Nested Comments: Support for hierarchical comment threads
  • Configurable Allowed Types: Control which resource types can be commented on
  • Content Validation: XSS protection and content length limits
  • User Association: Optional user authentication integration
  • Pagination: Built-in pagination support for comment lists
  • Go Migrations: Database schema managed via Go code (not SQL files)

Installation

go get github.com/nicolasbonnici/gorest-commentable

Development Environment

To set up your development environment:

make install

This will:

  • Install Go dependencies
  • Install development tools (golangci-lint)
  • Set up git hooks (pre-commit linting and tests)

Configuration

plugins:
  - name: commentable
    enabled: true
    config:
      allowed_types: ["post", "article", "product"]
      max_content_length: 10000
      pagination_limit: 20
      max_pagination_limit: 100
      enable_nesting: true
      max_nesting_depth: 10
      default_status: "awaiting"
Configuration Options
Option Type Default Description
allowed_types []string ["post"] Resource types that can receive comments
max_content_length int 10000 Maximum comment content length in bytes
pagination_limit int 20 Default pagination limit
max_pagination_limit int 100 Maximum allowed pagination limit
enable_nesting bool true Allow nested/threaded comments
max_nesting_depth int 10 Maximum nesting depth for replies
default_status string "awaiting" Default status for new comments (awaiting, published, draft, moderated)

API Endpoints

List Comments
GET /comments?commentable=post&commentableId={id}
Get Comment
GET /comments/:id
Create Comment
POST /comments
Content-Type: application/json

{
  "commentableId": "uuid",
  "commentable": "post",
  "parentId": "uuid",  // optional, for nested comments
  "content": "Comment text"
}
Update Comment
PUT /comments/:id
Content-Type: application/json

{
  "content": "Updated comment text"
}
Delete Comment
DELETE /comments/:id

Advanced Filtering

Array Filters (Multiple Values)

Get comments for multiple resource types using either syntax:

Without brackets (recommended for simplicity):

GET /comments?commentable=post&commentable=article

With brackets (explicit array syntax):

GET /comments?commentable[]=post&commentable[]=article

Both generate SQL: WHERE commentable IN ('post', 'article')

Exclusion Filters (NOT IN)

Exclude specific types (requires GoREST v0.5.0+):

GET /comments?commentable[nin]=draft&commentable[nin]=deleted

Generates SQL: WHERE commentable NOT IN ('draft', 'deleted')

Combining Filters

Filters are combined with AND logic:

GET /comments?commentable=post&user_id=123e4567-e89b-12d3-a456-426614174000

Generates SQL: WHERE commentable IN ('post') AND user_id = '123e4567...'

Available Filter Fields
  • commentable - Resource type (validates against configured allowed_types)
  • commentable_id - Resource UUID
  • user_id - Comment author UUID
  • parent_id - Parent comment UUID (null for top-level comments)
  • created_at[gte] - Created on or after date
  • created_at[lte] - Created on or before date
  • updated_at[gte] - Updated on or after date
  • updated_at[lte] - Updated on or before date
Filter Operators
  • field=value - Equality (single value)
  • field=val1&field=val2 - IN operator (multiple values)
  • field[]=val1&field[]=val2 - IN operator (explicit array syntax)
  • field[nin]=val1&field[nin]=val2 - NOT IN operator
  • field[gt]=value - Greater than
  • field[gte]=value - Greater than or equal
  • field[lt]=value - Less than
  • field[lte]=value - Less than or equal
  • field[like]=pattern - Pattern match (case-sensitive)
  • field[ilike]=pattern - Pattern match (case-insensitive)
Limits
  • Maximum 50 values per filter field
  • Invalid commentable types return 400 error with allowed types list
Examples

Get comments on posts and articles:

GET /comments?commentable=post&commentable=article

Get recent comments (last 7 days):

GET /comments?created_at[gte]=2024-01-20T00:00:00Z

Get comments excluding drafts:

GET /comments?commentable[nin]=draft

Combine filters with pagination and ordering:

GET /comments?commentable=post&limit=20&order[created_at]=desc

Database Schema

CREATE TABLE comment (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES users(id) ON DELETE SET NULL,
    commentable_id UUID NOT NULL,
    commentable TEXT NOT NULL,
    parent_id UUID REFERENCES comment(id) ON DELETE CASCADE,
    content TEXT NOT NULL,
    updated_at TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Indexes
CREATE INDEX idx_commentable ON comment(commentable, commentable_id, created_at);
CREATE INDEX idx_user_id ON comment(user_id);
CREATE INDEX idx_parent_id ON comment(parent_id);

Usage Example

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/nicolasbonnici/gorest"
    "github.com/nicolasbonnici/gorest-commentable"
)

func main() {
    app := fiber.New()

    // Initialize plugin with configuration
    plugin := commentable.NewPlugin()

    config := map[string]interface{}{
        "database": db,
        "allowed_types": []interface{}{"post", "article"},
        "max_content_length": 5000,
        "pagination_limit": 20,
    }

    if err := plugin.Initialize(config); err != nil {
        panic(err)
    }

    plugin.SetupEndpoints(app)

    app.Listen(":3000")
}

Development

Run Tests
make test
Run Linter
make lint
Build
make build
Coverage Report
make coverage

Security

  • XSS Protection: All comment content is HTML-escaped
  • Content Length Limits: Prevents extremely large payloads
  • Type Validation: Only configured resource types are allowed
  • Foreign Key Constraints: Maintains referential integrity where possible

Git Hooks

This directory contains git hooks for the GoREST plugin to maintain code quality.

Available Hooks
pre-commit

Runs before each commit to ensure code quality:

  • Linting: Runs make lint to check code style and potential issues
  • Tests: Runs make test to verify all tests pass
Installation
Automatic Installation

Run the install script from the project root:

./.githooks/install.sh
Manual Installation

Copy the hooks to your .git/hooks directory:

cp .githooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

License

See LICENSE file for details.

Contributing

Contributions are welcome! Please ensure:

  • All tests pass
  • Code is linted
  • New features have test coverage
  • Documentation is updated

Part of GoREST Ecosystem

Documentation

Index

Constants

View Source
const (
	StatusAwaiting  = "awaiting"
	StatusPublished = "published"
	StatusDraft     = "draft"
	StatusModerated = "moderated"
)
View Source
const MaxFilterValuesPerField = 50

Variables

Functions

func NewPlugin

func NewPlugin() plugin.Plugin

func RegisterCommentRoutes

func RegisterCommentRoutes(app *fiber.App, db database.Database, config *Config)

func RegisterRoutes

func RegisterRoutes(app *fiber.App, db database.Database, config *Config)

Types

type Comment

type Comment struct {
	Id            string     `json:"id,omitempty" db:"id" rbac:"read:*;write:none"`
	UserId        *string    `json:"userId,omitempty" db:"user_id" rbac:"read:*;write:reader"`
	CommentableId string     `json:"commentableId" db:"commentable_id" rbac:"read:*;write:reader"`
	Commentable   string     `json:"commentable" db:"commentable" rbac:"read:*;write:reader"`
	ParentId      *string    `json:"parentId,omitempty" db:"parent_id" rbac:"read:*;write:reader"`
	Content       string     `json:"content" db:"content" rbac:"read:*;write:reader"`
	Status        string     `json:"status" db:"status" rbac:"read:*;write:moderator"`
	IpAddress     *string    `json:"ipAddress,omitempty" db:"ip_address" rbac:"read:moderator;write:none"`
	UserAgent     *string    `json:"userAgent,omitempty" db:"user_agent" rbac:"read:moderator;write:none"`
	UpdatedAt     *time.Time `json:"updatedAt,omitempty" db:"updated_at" rbac:"read:*;write:none"`
	CreatedAt     *time.Time `json:"createdAt,omitempty" db:"created_at" rbac:"read:*;write:none"`
}

func (Comment) TableName

func (Comment) TableName() string

type CommentResource

type CommentResource struct {
	DB                 database.Database
	CRUD               *crud.CRUD[Comment]
	Config             *Config
	PaginationLimit    int
	PaginationMaxLimit int
	Voter              rbac.Voter
}

func (*CommentResource) Create

func (r *CommentResource) Create(c *fiber.Ctx) error

func (*CommentResource) Delete

func (r *CommentResource) Delete(c *fiber.Ctx) error

func (*CommentResource) Get

func (r *CommentResource) Get(c *fiber.Ctx) error

func (*CommentResource) List

func (r *CommentResource) List(c *fiber.Ctx) error

func (*CommentResource) Update

func (r *CommentResource) Update(c *fiber.Ctx) error

type CommentablePlugin

type CommentablePlugin struct {
	// contains filtered or unexported fields
}

func (*CommentablePlugin) Dependencies added in v0.1.1

func (p *CommentablePlugin) Dependencies() []string

func (*CommentablePlugin) GetOpenAPIResources added in v0.1.3

func (p *CommentablePlugin) GetOpenAPIResources() []plugin.OpenAPIResource

func (*CommentablePlugin) Handler

func (p *CommentablePlugin) Handler() fiber.Handler

func (*CommentablePlugin) Initialize

func (p *CommentablePlugin) Initialize(config map[string]interface{}) error

func (*CommentablePlugin) MigrationDependencies

func (p *CommentablePlugin) MigrationDependencies() []string

func (*CommentablePlugin) MigrationSource

func (p *CommentablePlugin) MigrationSource() interface{}

func (*CommentablePlugin) Name

func (p *CommentablePlugin) Name() string

func (*CommentablePlugin) SetupEndpoints

func (p *CommentablePlugin) SetupEndpoints(app *fiber.App) error

type Config

type Config struct {
	Database           database.Database
	AllowedTypes       []string `json:"allowed_types" yaml:"allowed_types"`
	MaxContentLength   int      `json:"max_content_length" yaml:"max_content_length"`
	PaginationLimit    int      `json:"pagination_limit" yaml:"pagination_limit"`
	MaxPaginationLimit int      `json:"max_pagination_limit" yaml:"max_pagination_limit"`
	EnableNesting      bool     `json:"enable_nesting" yaml:"enable_nesting"`
	MaxNestingDepth    int      `json:"max_nesting_depth" yaml:"max_nesting_depth"`
	DefaultStatus      string   `json:"default_status" yaml:"default_status"`
}

func DefaultConfig

func DefaultConfig() Config

func (*Config) IsAllowedType

func (c *Config) IsAllowedType(commentableType string) bool

func (*Config) Validate

func (c *Config) Validate() error

type CreateCommentRequest

type CreateCommentRequest struct {
	CommentableId string  `json:"commentableId" validate:"required,uuid"`
	Commentable   string  `json:"commentable" validate:"required"`
	ParentId      *string `json:"parentId,omitempty" validate:"omitempty,uuid"`
	Content       string  `json:"content" validate:"required"`
}

func (*CreateCommentRequest) Validate

func (r *CreateCommentRequest) Validate(config *Config) error

type UpdateCommentRequest

type UpdateCommentRequest struct {
	Content *string `json:"content,omitempty"`
	Status  *string `json:"status,omitempty"`
}

func (*UpdateCommentRequest) Validate

func (r *UpdateCommentRequest) Validate(config *Config) error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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