feat: add categories migration and model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
41c8d1e2f5
commit
641b402d4a
61
models/categories.go
Normal file
61
models/categories.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Category struct {
|
||||
ID int64 `json:"id"`
|
||||
SiteID int64 `json:"site_id"`
|
||||
GUID string `json:"guid"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CategoryWithCount is a Category plus the count of published posts in it.
|
||||
type CategoryWithCount struct {
|
||||
Category
|
||||
PostCount int
|
||||
DescriptionBrief string
|
||||
}
|
||||
|
||||
// GenerateCategorySlug creates a URL-safe slug from a category name.
|
||||
// e.g. "Go Programming" -> "go-programming"
|
||||
func GenerateCategorySlug(name string) string {
|
||||
var sb strings.Builder
|
||||
prevDash := false
|
||||
for _, c := range strings.TrimSpace(name) {
|
||||
if unicode.IsLetter(c) || unicode.IsNumber(c) {
|
||||
sb.WriteRune(unicode.ToLower(c))
|
||||
prevDash = false
|
||||
} else if unicode.IsSpace(c) || c == '-' || c == '_' {
|
||||
if !prevDash && sb.Len() > 0 {
|
||||
sb.WriteRune('-')
|
||||
prevDash = true
|
||||
}
|
||||
}
|
||||
}
|
||||
result := sb.String()
|
||||
return strings.TrimRight(result, "-")
|
||||
}
|
||||
|
||||
// BriefDescription returns the first sentence or line of the description.
|
||||
func BriefDescription(desc string) string {
|
||||
if desc == "" {
|
||||
return ""
|
||||
}
|
||||
for i, c := range desc {
|
||||
if c == '\n' {
|
||||
return desc[:i]
|
||||
}
|
||||
if c == '.' && i+1 < len(desc) {
|
||||
return desc[:i+1]
|
||||
}
|
||||
}
|
||||
return desc
|
||||
}
|
||||
28
models/categories_test.go
Normal file
28
models/categories_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package models_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
)
|
||||
|
||||
func TestGenerateCategorySlug(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"Go Programming", "go-programming"},
|
||||
{" Travel ", "travel"},
|
||||
{"hello---world", "hello-world"},
|
||||
{"UPPER CASE", "upper-case"},
|
||||
{"one", "one"},
|
||||
{"with_underscores", "with-underscores"},
|
||||
{"special!@#chars", "specialchars"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, models.GenerateCategorySlug(tt.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,3 +7,4 @@ var PermissionError = errors.New("permission denied")
|
|||
var NotFoundError = errors.New("not found")
|
||||
var SiteRequiredError = errors.New("site required")
|
||||
var DeleteDebounceError = errors.New("permanent delete too soon, try again in a few seconds")
|
||||
var SlugConflictError = errors.New("a category with this slug already exists")
|
||||
|
|
|
|||
23
sql/schema/04_categories.up.sql
Normal file
23
sql/schema/04_categories.up.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id INTEGER NOT NULL,
|
||||
guid TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_categories_site ON categories (site_id);
|
||||
CREATE UNIQUE INDEX idx_categories_guid ON categories (guid);
|
||||
CREATE UNIQUE INDEX idx_categories_site_slug ON categories (site_id, slug);
|
||||
|
||||
CREATE TABLE post_categories (
|
||||
post_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, category_id),
|
||||
FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_post_categories_category ON post_categories (category_id);
|
||||
Loading…
Reference in a new issue