taxonomy

package module
v0.6.5 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 18 Imported by: 0

README

gorest-taxonomy

CI Go Report Card Go Version License

Polymorphic categories (hierarchical) and tags (flat) for any GoREST resource type. Attach taxonomy to any entity via resource + resource_id — no schema changes needed on the target table.

Schema

Table Purpose
categories Self-referential tree (parent_id = null → root)
category_resources Pivot: category ↔ any resource
tags Flat tag list
tag_resources Pivot: tag ↔ any resource

Supports PostgreSQL, MySQL, SQLite.

Installation

go get github.com/nicolasbonnici/gorest-taxonomy

Register in gorest.yaml:

plugins:
  - name: taxonomy
    config:
      allowed_types: [post, product, article]
      max_depth: 5          # max category nesting levels (default: 5)
      pagination_limit: 25
      max_pagination_limit: 100

API

Categories
POST   /categories                                    create (slug auto-generated from name)
GET    /categories                                    list (paginated, filterable)
GET    /categories/tree                               full tree
GET    /categories/:id                               get by id
PUT    /categories/:id                               full update
DELETE /categories/:id                               delete (children become root)
POST   /categories/:id/resources                     attach { resource, resource_id }
DELETE /categories/:id/resources/:resource/:rid      detach
GET    /:resource/:resource_id/categories            list categories for a resource
Tags
POST   /tags                                         create
GET    /tags                                         list (paginated, filterable)
GET    /tags/:id                                     get by id
PUT    /tags/:id                                     full update
DELETE /tags/:id                                     delete
POST   /tags/:id/resources                           attach { resource, resource_id }
DELETE /tags/:id/resources/:resource/:rid            detach
GET    /:resource/:resource_id/tags                  list tags for a resource

Examples

# Create a root category
curl -X POST /categories -d '{"name":"Technology"}'

# Create a child category
curl -X POST /categories -d '{"name":"Go","parent_id":"<uuid>"}'

# Get full tree
curl /categories/tree

# Attach a post to a category
curl -X POST /categories/<uuid>/resources \
  -d '{"resource":"post","resource_id":"<uuid>"}'

# List all categories for a post
curl /post/<uuid>/categories

# Create and attach a tag
curl -X POST /tags -d '{"name":"open-source"}'
curl -X POST /tags/<uuid>/resources \
  -d '{"resource":"post","resource_id":"<uuid>"}'

Development

make install    # deps + dev tools + git hooks
make test       # run tests
make audit      # gofmt, vet, staticcheck, errcheck, gocyclo
make lint       # golangci-lint

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewPlugin

func NewPlugin() plugin.Plugin

func RegisterCategoryRoutes

func RegisterCategoryRoutes(router fiber.Router, db database.Database, config *Config, service *TaxonomyService)

func RegisterRoutes

func RegisterRoutes(router fiber.Router, db database.Database, config *Config)

func RegisterRoutesWithService added in v0.2.4

func RegisterRoutesWithService(router fiber.Router, db database.Database, config *Config, service *TaxonomyService)

func RegisterTagRoutes

func RegisterTagRoutes(router fiber.Router, db database.Database, config *Config, service *TaxonomyService)

Types

type Category

type Category struct {
	ID          uuid.UUID  `json:"id" db:"id"`
	ParentID    *uuid.UUID `json:"parent_id,omitempty" db:"parent_id"`
	Name        string     `json:"name" db:"name"`
	Slug        string     `json:"slug" db:"slug"`
	Description string     `json:"description" db:"description"`
	CreatedAt   time.Time  `json:"created_at" db:"created_at"`
	UpdatedAt   *time.Time `json:"updated_at,omitempty" db:"updated_at"`
}

func (Category) TableName

func (Category) TableName() string

type CategoryConverter

type CategoryConverter struct{}

func (*CategoryConverter) CreateDTOToModel

func (c *CategoryConverter) CreateDTOToModel(dto CategoryCreateDTO) Category

func (*CategoryConverter) ModelToResponseDTO

func (c *CategoryConverter) ModelToResponseDTO(model Category) CategoryResponseDTO

func (*CategoryConverter) ModelsToResponseDTOs

func (c *CategoryConverter) ModelsToResponseDTOs(models []Category) []CategoryResponseDTO

func (*CategoryConverter) UpdateDTOToModel

func (c *CategoryConverter) UpdateDTOToModel(dto CategoryUpdateDTO) Category

type CategoryCreateDTO

type CategoryCreateDTO struct {
	ParentID    *string `json:"parent_id,omitempty"`
	Name        string  `json:"name"`
	Slug        string  `json:"slug,omitempty"`
	Description string  `json:"description,omitempty"`
}

type CategoryResource

type CategoryResource struct {
	ID         uuid.UUID `json:"id" db:"id"`
	CategoryID uuid.UUID `json:"category_id" db:"category_id"`
	Resource   string    `json:"resource" db:"resource"`
	ResourceID uuid.UUID `json:"resource_id" db:"resource_id"`
	CreatedAt  time.Time `json:"created_at" db:"created_at"`
}

func (CategoryResource) TableName

func (CategoryResource) TableName() string

type CategoryResponseDTO

type CategoryResponseDTO struct {
	ID          uuid.UUID  `json:"id"`
	Parent      *string    `json:"parent,omitempty"`
	Name        string     `json:"name"`
	Slug        string     `json:"slug"`
	Description string     `json:"description,omitempty"`
	CreatedAt   time.Time  `json:"created_at"`
	UpdatedAt   *time.Time `json:"updated_at,omitempty"`
}

type CategoryTreeNode

type CategoryTreeNode struct {
	ID          uuid.UUID           `json:"id"`
	ParentID    *uuid.UUID          `json:"parent_id,omitempty"`
	Name        string              `json:"name"`
	Slug        string              `json:"slug"`
	Description string              `json:"description,omitempty"`
	Children    []*CategoryTreeNode `json:"children,omitempty"`
	CreatedAt   time.Time           `json:"created_at"`
	UpdatedAt   *time.Time          `json:"updated_at,omitempty"`
}

type CategoryUpdateDTO

type CategoryUpdateDTO struct {
	ParentID    *string `json:"parent_id,omitempty"`
	Name        string  `json:"name"`
	Slug        string  `json:"slug,omitempty"`
	Description string  `json:"description,omitempty"`
}

type Config

type Config struct {
	Database           database.Database
	AllowedTypes       []string `json:"allowed_types" yaml:"allowed_types"`
	MaxDepth           int      `json:"max_depth" yaml:"max_depth"`
	PaginationLimit    int      `json:"pagination_limit" yaml:"pagination_limit"`
	MaxPaginationLimit int      `json:"max_pagination_limit" yaml:"max_pagination_limit"`
}

func DefaultConfig

func DefaultConfig() Config

func (*Config) IsAllowedType

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

func (*Config) Validate

func (c *Config) Validate() error

type ResourceAttachDTO

type ResourceAttachDTO struct {
	Resource   string `json:"resource"`
	ResourceID string `json:"resource_id"`
}

type ServiceProvider added in v0.2.4

type ServiceProvider interface {
	GetService() *TaxonomyService
}

type Tag

type Tag struct {
	ID        uuid.UUID  `json:"id" db:"id"`
	Name      string     `json:"name" db:"name"`
	Slug      string     `json:"slug" db:"slug"`
	CreatedAt time.Time  `json:"created_at" db:"created_at"`
	UpdatedAt *time.Time `json:"updated_at,omitempty" db:"updated_at"`
}

func (Tag) TableName

func (Tag) TableName() string

type TagConverter

type TagConverter struct{}

func (*TagConverter) CreateDTOToModel

func (c *TagConverter) CreateDTOToModel(dto TagCreateDTO) Tag

func (*TagConverter) ModelToResponseDTO

func (c *TagConverter) ModelToResponseDTO(model Tag) TagResponseDTO

func (*TagConverter) ModelsToResponseDTOs

func (c *TagConverter) ModelsToResponseDTOs(models []Tag) []TagResponseDTO

func (*TagConverter) UpdateDTOToModel

func (c *TagConverter) UpdateDTOToModel(dto TagUpdateDTO) Tag

type TagCreateDTO

type TagCreateDTO struct {
	Name string `json:"name"`
	Slug string `json:"slug,omitempty"`
}

type TagResource

type TagResource struct {
	ID         uuid.UUID `json:"id" db:"id"`
	TagID      uuid.UUID `json:"tag_id" db:"tag_id"`
	Resource   string    `json:"resource" db:"resource"`
	ResourceID uuid.UUID `json:"resource_id" db:"resource_id"`
	CreatedAt  time.Time `json:"created_at" db:"created_at"`
}

func (TagResource) TableName

func (TagResource) TableName() string

type TagResponseDTO

type TagResponseDTO struct {
	ID        uuid.UUID  `json:"id"`
	Name      string     `json:"name"`
	Slug      string     `json:"slug"`
	CreatedAt time.Time  `json:"created_at"`
	UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

type TagUpdateDTO

type TagUpdateDTO struct {
	Name string `json:"name"`
	Slug string `json:"slug,omitempty"`
}

type TaxonomyHooks

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

func NewTaxonomyHooks

func NewTaxonomyHooks(db database.Database, config *Config, service *TaxonomyService) *TaxonomyHooks

func (*TaxonomyHooks) CategoryCreateHook

func (h *TaxonomyHooks) CategoryCreateHook(c fiber.Ctx, dto CategoryCreateDTO, model *Category) error

func (*TaxonomyHooks) CategoryDeleteHook

func (h *TaxonomyHooks) CategoryDeleteHook(_ fiber.Ctx, _ any) error

func (*TaxonomyHooks) CategoryGetAllHook

func (h *TaxonomyHooks) CategoryGetAllHook(_ fiber.Ctx, _ *[]query.Condition, _ *[]crud.OrderByClause) error

func (*TaxonomyHooks) CategoryGetByIDHook

func (h *TaxonomyHooks) CategoryGetByIDHook(_ fiber.Ctx, _ any) error

func (*TaxonomyHooks) CategoryUpdateHook

func (h *TaxonomyHooks) CategoryUpdateHook(c fiber.Ctx, dto CategoryUpdateDTO, model *Category) error

func (*TaxonomyHooks) TagCreateHook

func (h *TaxonomyHooks) TagCreateHook(c fiber.Ctx, dto TagCreateDTO, model *Tag) error

func (*TaxonomyHooks) TagDeleteHook

func (h *TaxonomyHooks) TagDeleteHook(_ fiber.Ctx, _ any) error

func (*TaxonomyHooks) TagGetAllHook

func (h *TaxonomyHooks) TagGetAllHook(_ fiber.Ctx, _ *[]query.Condition, _ *[]crud.OrderByClause) error

func (*TaxonomyHooks) TagGetByIDHook

func (h *TaxonomyHooks) TagGetByIDHook(_ fiber.Ctx, _ any) error

func (*TaxonomyHooks) TagUpdateHook

func (h *TaxonomyHooks) TagUpdateHook(_ fiber.Ctx, dto TagUpdateDTO, model *Tag) error

type TaxonomyPlugin

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

func (*TaxonomyPlugin) Dependencies

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

func (*TaxonomyPlugin) GetOpenAPIResources

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

func (*TaxonomyPlugin) GetService added in v0.2.4

func (p *TaxonomyPlugin) GetService() *TaxonomyService

func (*TaxonomyPlugin) Handler

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

func (*TaxonomyPlugin) Initialize

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

func (*TaxonomyPlugin) MigrationDependencies

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

func (*TaxonomyPlugin) MigrationSource

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

func (*TaxonomyPlugin) Name

func (p *TaxonomyPlugin) Name() string

func (*TaxonomyPlugin) SetupEndpoints

func (p *TaxonomyPlugin) SetupEndpoints(router fiber.Router) error

type TaxonomyService

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

func NewTaxonomyService

func NewTaxonomyService(db database.Database, config *Config) *TaxonomyService

func (*TaxonomyService) AttachCategory

func (s *TaxonomyService) AttachCategory(ctx context.Context, categoryID uuid.UUID, resource string, resourceID uuid.UUID) error

func (*TaxonomyService) AttachTag

func (s *TaxonomyService) AttachTag(ctx context.Context, tagID uuid.UUID, resource string, resourceID uuid.UUID) error

func (*TaxonomyService) BuildCategoryTree

func (s *TaxonomyService) BuildCategoryTree(categories []Category) []*CategoryTreeNode

func (*TaxonomyService) DetachCategory

func (s *TaxonomyService) DetachCategory(ctx context.Context, categoryID uuid.UUID, resource string, resourceID uuid.UUID) error

func (*TaxonomyService) DetachTag

func (s *TaxonomyService) DetachTag(ctx context.Context, tagID uuid.UUID, resource string, resourceID uuid.UUID) error

func (*TaxonomyService) GetAllCategories

func (s *TaxonomyService) GetAllCategories(ctx context.Context) ([]Category, error)

func (*TaxonomyService) GetCategoriesForResource

func (s *TaxonomyService) GetCategoriesForResource(ctx context.Context, resource string, resourceID uuid.UUID) ([]Category, error)

GetCategoriesForResource returns the categories attached to a single resource. It delegates to the batch loader so both paths share one query shape.

func (*TaxonomyService) GetCategoriesForResources added in v0.6.0

func (s *TaxonomyService) GetCategoriesForResources(ctx context.Context, resource string, resourceIDs []uuid.UUID) (map[uuid.UUID][]Category, error)

GetCategoriesForResources loads categories for a batch of resources of the same type in a single query, keyed by resource id. This is the N+1 escape hatch for consumers that render categories across a page of resources: one round trip instead of one per resource.

func (*TaxonomyService) GetCategoryDepth

func (s *TaxonomyService) GetCategoryDepth(ctx context.Context, id uuid.UUID) (int, error)

GetCategoryDepth returns how many ancestors a category has. It loads the id→parent map in one query and walks it in memory instead of issuing a query per level, which turned the depth check into up to MaxDepth round trips. The map is read straight from the database (not the cache) so a parent created concurrently is always visible to the depth guard.

func (*TaxonomyService) GetResourceIDsByCategorySlug added in v0.2.4

func (s *TaxonomyService) GetResourceIDsByCategorySlug(ctx context.Context, resource, slug string) ([]uuid.UUID, error)

func (*TaxonomyService) GetResourceIDsByTagSlug added in v0.2.4

func (s *TaxonomyService) GetResourceIDsByTagSlug(ctx context.Context, resource, slug string) ([]uuid.UUID, error)

func (*TaxonomyService) GetTagsForResource

func (s *TaxonomyService) GetTagsForResource(ctx context.Context, resource string, resourceID uuid.UUID) ([]Tag, error)

GetTagsForResource returns the tags attached to a single resource, delegating to the batch loader.

func (*TaxonomyService) GetTagsForResources added in v0.6.0

func (s *TaxonomyService) GetTagsForResources(ctx context.Context, resource string, resourceIDs []uuid.UUID) (map[uuid.UUID][]Tag, error)

GetTagsForResources loads tags for a batch of resources of the same type in a single query, keyed by resource id.

func (*TaxonomyService) InvalidateCategoryCache added in v0.6.0

func (s *TaxonomyService) InvalidateCategoryCache()

InvalidateCategoryCache drops the memoized category set. It must be called after any committed create/update/delete of a category so the next read reloads fresh data.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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