feat(15-01): add journal categories, tags, settings, and author slug
Squash the remaining golem15_journal_* tables plus the backend_users author slug so a host migrate matches the frozen PHP schema. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
36
models/category.go
Normal file
36
models/category.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Category is a journal category stored on golem15_journal_categories.
|
||||
type Category struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
Slug string `gorm:"column:slug"`
|
||||
Code *string `gorm:"column:code"`
|
||||
Description *string `gorm:"column:description"`
|
||||
ParentID *uint `gorm:"column:parent_id"`
|
||||
NestLeft *int `gorm:"column:nest_left"`
|
||||
NestRight *int `gorm:"column:nest_right"`
|
||||
NestDepth *int `gorm:"column:nest_depth"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (Category) TableName() string { return "golem15_journal_categories" }
|
||||
|
||||
// MorphName is the PHP class string stored on translate attributes.
|
||||
func (Category) MorphName() string { return `Golem15\Journal\Models\Category` }
|
||||
|
||||
func (Category) Translatable() []string { return []string{"name", "description", "slug"} }
|
||||
|
||||
func (Category) TranslatableIndexes() []string { return []string{"slug"} }
|
||||
|
||||
// Fillable is JOURNAL-002: nest_* stay out of mass assignment.
|
||||
func (Category) Fillable() []string {
|
||||
return []string{"name", "slug", "code", "description", "parent_id"}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Category{})
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
||||
)
|
||||
|
||||
// Post is a journal entry stored on golem15_journal_posts.
|
||||
@@ -21,6 +24,10 @@ type Post struct {
|
||||
Sources *string `gorm:"column:sources;type:jsonb"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||||
|
||||
User *cabana.BackendUser `gorm:"foreignKey:UserID" json:"-"`
|
||||
Categories []Category `gorm:"many2many:golem15_journal_posts_categories;" json:"-"`
|
||||
Tags []Tag `gorm:"many2many:golem15_journal_posts_tags;" json:"-"`
|
||||
}
|
||||
|
||||
func (Post) TableName() string { return "golem15_journal_posts" }
|
||||
@@ -43,6 +50,13 @@ func (Post) Fillable() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (Post) AttachRelations() []attach.Relation {
|
||||
return []attach.Relation{
|
||||
{Name: "featured_images", Many: true, Public: true},
|
||||
{Name: "content_images", Many: true, Public: true},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Post{})
|
||||
}
|
||||
|
||||
54
models/settings.go
Normal file
54
models/settings.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package models
|
||||
|
||||
// Settings is the singleton journal settings row (ID=1) on golem15_journal_settings.
|
||||
type Settings struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
ShowAllPosts bool `gorm:"column:show_all_posts"`
|
||||
UseRichEditor bool `gorm:"column:use_rich_editor"`
|
||||
SearchUseTypesense bool `gorm:"column:search_use_typesense"`
|
||||
SearchTitleWeight int `gorm:"column:search_title_weight"`
|
||||
SearchExcerptWeight int `gorm:"column:search_excerpt_weight"`
|
||||
SearchContentWeight int `gorm:"column:search_content_weight"`
|
||||
RSSEnabled bool `gorm:"column:rss_enabled"`
|
||||
RSSIncludeContent bool `gorm:"column:rss_include_content"`
|
||||
RSSTitle *string `gorm:"column:rss_title"`
|
||||
RSSDescription *string `gorm:"column:rss_description"`
|
||||
RSSLanguage string `gorm:"column:rss_language"`
|
||||
RSSCopyright *string `gorm:"column:rss_copyright"`
|
||||
RSSImageURL *string `gorm:"column:rss_image_url"`
|
||||
RSSPostsPerFeed int `gorm:"column:rss_posts_per_feed"`
|
||||
}
|
||||
|
||||
func (Settings) TableName() string { return "golem15_journal_settings" }
|
||||
|
||||
func (Settings) Fillable() []string {
|
||||
return []string{
|
||||
"show_all_posts", "use_rich_editor", "search_use_typesense",
|
||||
"search_title_weight", "search_excerpt_weight", "search_content_weight",
|
||||
"rss_enabled", "rss_include_content", "rss_title", "rss_description",
|
||||
"rss_language", "rss_copyright", "rss_image_url", "rss_posts_per_feed",
|
||||
}
|
||||
}
|
||||
|
||||
func (Settings) Rules() map[string]string {
|
||||
return map[string]string{
|
||||
"show_all_posts": "boolean",
|
||||
"use_rich_editor": "boolean",
|
||||
"search_use_typesense": "boolean",
|
||||
"search_title_weight": "nullable|integer|min:0|max:100",
|
||||
"search_excerpt_weight": "nullable|integer|min:0|max:100",
|
||||
"search_content_weight": "nullable|integer|min:0|max:100",
|
||||
"rss_enabled": "boolean",
|
||||
"rss_include_content": "boolean",
|
||||
"rss_title": "nullable|string|max:255",
|
||||
"rss_description": "nullable|string|max:1000",
|
||||
"rss_language": "nullable|string|max:10",
|
||||
"rss_copyright": "nullable|string|max:255",
|
||||
"rss_image_url": "nullable|url|max:500",
|
||||
"rss_posts_per_feed": "nullable|integer|min:1|max:100",
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Settings{})
|
||||
}
|
||||
26
models/tag.go
Normal file
26
models/tag.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Tag is a journal tag stored on golem15_journal_tags. It is not translatable.
|
||||
type Tag struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
Slug string `gorm:"column:slug"`
|
||||
Description *string `gorm:"column:description"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (Tag) TableName() string { return "golem15_journal_tags" }
|
||||
|
||||
func (Tag) MorphName() string { return `Golem15\Journal\Models\Tag` }
|
||||
|
||||
// Fillable is JOURNAL-001.
|
||||
func (Tag) Fillable() []string {
|
||||
return []string{"name", "slug", "description"}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Tag{})
|
||||
}
|
||||
45
updates/202610060002_create_golem15_journal_categories.go
Normal file
45
updates/202610060002_create_golem15_journal_categories.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var createCategories = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610060002_create_golem15_journal_categories",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
for _, stmt := range []string{
|
||||
`CREATE TABLE golem15_journal_categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NULL,
|
||||
slug TEXT NOT NULL,
|
||||
code TEXT NULL,
|
||||
description TEXT NULL,
|
||||
parent_id INTEGER NULL,
|
||||
nest_left INTEGER NULL,
|
||||
nest_right INTEGER NULL,
|
||||
nest_depth INTEGER NULL,
|
||||
created_at TIMESTAMPTZ NULL,
|
||||
updated_at TIMESTAMPTZ NULL
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX golem15_journal_categories_slug_unique ON golem15_journal_categories (slug)`,
|
||||
`CREATE INDEX golem15_journal_categories_parent_id_index ON golem15_journal_categories (parent_id)`,
|
||||
`INSERT INTO golem15_journal_categories (name, slug, nest_left, nest_right, nest_depth)
|
||||
VALUES ('Uncategorized', 'uncategorized', 1, 2, 0)`,
|
||||
} {
|
||||
if err := tx.Exec(stmt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS golem15_journal_categories`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(createCategories...)
|
||||
}
|
||||
37
updates/202610060003_create_golem15_journal_tags.go
Normal file
37
updates/202610060003_create_golem15_journal_tags.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var createTags = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610060003_create_golem15_journal_tags",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
for _, stmt := range []string{
|
||||
`CREATE TABLE golem15_journal_tags (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NULL,
|
||||
slug TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
created_at TIMESTAMPTZ NULL,
|
||||
updated_at TIMESTAMPTZ NULL
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX golem15_journal_tags_slug_unique ON golem15_journal_tags (slug)`,
|
||||
} {
|
||||
if err := tx.Exec(stmt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS golem15_journal_tags`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(createTags...)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var createPostsCategories = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610060004_create_golem15_journal_posts_categories",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`CREATE TABLE golem15_journal_posts_categories (
|
||||
post_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, category_id)
|
||||
)`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS golem15_journal_posts_categories`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(createPostsCategories...)
|
||||
}
|
||||
26
updates/202610060005_create_golem15_journal_posts_tags.go
Normal file
26
updates/202610060005_create_golem15_journal_posts_tags.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var createPostsTags = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610060005_create_golem15_journal_posts_tags",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`CREATE TABLE golem15_journal_posts_tags (
|
||||
post_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, tag_id)
|
||||
)`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS golem15_journal_posts_tags`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(createPostsTags...)
|
||||
}
|
||||
46
updates/202610060006_create_golem15_journal_settings.go
Normal file
46
updates/202610060006_create_golem15_journal_settings.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var createSettings = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610060006_create_golem15_journal_settings",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
for _, stmt := range []string{
|
||||
`CREATE TABLE golem15_journal_settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
show_all_posts BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
use_rich_editor BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
search_use_typesense BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
search_title_weight INTEGER NOT NULL DEFAULT 5,
|
||||
search_excerpt_weight INTEGER NOT NULL DEFAULT 3,
|
||||
search_content_weight INTEGER NOT NULL DEFAULT 1,
|
||||
rss_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
rss_include_content BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
rss_title TEXT NULL,
|
||||
rss_description TEXT NULL,
|
||||
rss_language TEXT NOT NULL DEFAULT 'en-us',
|
||||
rss_copyright TEXT NULL,
|
||||
rss_image_url TEXT NULL,
|
||||
rss_posts_per_feed INTEGER NOT NULL DEFAULT 20
|
||||
)`,
|
||||
`INSERT INTO golem15_journal_settings (id) VALUES (1)`,
|
||||
} {
|
||||
if err := tx.Exec(stmt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS golem15_journal_settings`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(createSettings...)
|
||||
}
|
||||
22
updates/202610060007_add_author_slug_to_backend_users.go
Normal file
22
updates/202610060007_add_author_slug_to_backend_users.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var addAuthorSlug = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610060007_add_author_slug_to_backend_users",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`ALTER TABLE backend_users ADD COLUMN IF NOT EXISTS golem15_bloghub_author_slug TEXT UNIQUE`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`ALTER TABLE backend_users DROP COLUMN IF EXISTS golem15_bloghub_author_slug`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(addAuthorSlug...)
|
||||
}
|
||||
@@ -6,10 +6,14 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-journal-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
@@ -131,3 +135,87 @@ func dedicatedDB(t *testing.T, name string) *sql.DB {
|
||||
// from reporting [no tests to run] in this package. The production-path
|
||||
// proof lives in package models.
|
||||
func TestPostTranslatableSmoke(t *testing.T) {}
|
||||
|
||||
func TestJournalTables(t *testing.T) {
|
||||
sqlDB := dedicatedDB(t, "journal_tables")
|
||||
gdb, err := lagoon.Use(t.Context(), sqlDB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend := gormigrate.New(gdb, &gormigrate.Options{
|
||||
TableName: "summer_migrations_summercms_cabana",
|
||||
UseTransaction: true,
|
||||
}, lagoon.BackendAdminMigrations)
|
||||
if err := backend.Migrate(); err != nil {
|
||||
t.Fatalf("backend_users migrations: %v", err)
|
||||
}
|
||||
journal := gormigrate.New(gdb, &gormigrate.Options{
|
||||
TableName: "summer_migrations_golem15_journal",
|
||||
UseTransaction: true,
|
||||
}, All())
|
||||
if err := journal.Migrate(); err != nil {
|
||||
t.Fatalf("journal migrations: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range []string{
|
||||
"golem15_journal_posts",
|
||||
"golem15_journal_categories",
|
||||
"golem15_journal_tags",
|
||||
"golem15_journal_posts_categories",
|
||||
"golem15_journal_posts_tags",
|
||||
"golem15_journal_settings",
|
||||
} {
|
||||
if !gdb.Migrator().HasTable(name) {
|
||||
t.Fatalf("missing table %s", name)
|
||||
}
|
||||
}
|
||||
var leftover []string
|
||||
if err := gdb.WithContext(t.Context()).
|
||||
Raw(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'rainlab\_journal\_%'`).
|
||||
Scan(&leftover).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(leftover) != 0 {
|
||||
t.Fatalf("rainlab-era journal tables present: %v", leftover)
|
||||
}
|
||||
if !gdb.Migrator().HasColumn("backend_users", "golem15_bloghub_author_slug") {
|
||||
t.Fatal("backend_users.golem15_bloghub_author_slug missing")
|
||||
}
|
||||
|
||||
var typesense bool
|
||||
if err := gdb.WithContext(t.Context()).
|
||||
Raw(`SELECT search_use_typesense FROM golem15_journal_settings WHERE id = 1`).
|
||||
Scan(&typesense).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if typesense {
|
||||
t.Fatal("search_use_typesense default must be false")
|
||||
}
|
||||
|
||||
var catCount int64
|
||||
if err := gdb.WithContext(t.Context()).
|
||||
Table("golem15_journal_categories").
|
||||
Where("slug = ?", "uncategorized").
|
||||
Count(&catCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if catCount != 1 {
|
||||
t.Fatalf("Uncategorized rows = %d, want 1", catCount)
|
||||
}
|
||||
var postCount int64
|
||||
if err := gdb.WithContext(t.Context()).
|
||||
Table("golem15_journal_posts").
|
||||
Count(&postCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if postCount != 0 {
|
||||
t.Fatalf("seeded posts = %d, want 0", postCount)
|
||||
}
|
||||
|
||||
if got := (models.Category{}).Fillable(); !reflect.DeepEqual(got, []string{"name", "slug", "code", "description", "parent_id"}) {
|
||||
t.Fatalf("Category.Fillable = %#v", got)
|
||||
}
|
||||
if got := (models.Tag{}).Fillable(); !reflect.DeepEqual(got, []string{"name", "slug", "description"}) {
|
||||
t.Fatalf("Tag.Fillable = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user