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:
Jakub Zych
2026-10-06 18:28:17 +02:00
parent 445022ef71
commit 9ae4b9141e
11 changed files with 420 additions and 0 deletions

View 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...)
}

View 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...)
}

View File

@@ -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...)
}

View 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...)
}

View 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...)
}

View 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...)
}

View File

@@ -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)
}
}