feat: add admin publishing workflow and yar theme
Add Go/Postgres admin APIs, Angular admin UI, manual build flow, asset uploads, markdown import/export, configurable slug generation, and the Yar reading theme. Exclude local docs and generated development artifacts from version control.
This commit is contained in:
parent
b78f4b39c9
commit
f0b50d13ea
121 changed files with 27139 additions and 550 deletions
128
backend/internal/admin/migrations.go
Normal file
128
backend/internal/admin/migrations.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Migration struct {
|
||||
Version string
|
||||
Path string
|
||||
Checksum string
|
||||
}
|
||||
|
||||
func RunMigrations(ctx context.Context, db *pgxpool.Pool, dir string) error {
|
||||
if db == nil {
|
||||
return errors.New("database is required")
|
||||
}
|
||||
|
||||
migrations, err := LoadMigrationFiles(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin migration transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS admin_schema_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)`); err != nil {
|
||||
return fmt.Errorf("ensure migration table: %w", err)
|
||||
}
|
||||
|
||||
for _, migration := range migrations {
|
||||
if err := applyMigration(ctx, tx, migration); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit migrations: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadMigrationFiles(dir string) ([]Migration, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read migrations dir: %w", err)
|
||||
}
|
||||
|
||||
var migrations []Migration
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, entry.Name())
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read migration %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
migrations = append(migrations, Migration{
|
||||
Version: entry.Name(),
|
||||
Path: path,
|
||||
Checksum: checksum(content),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(migrations, func(i, j int) bool {
|
||||
return migrations[i].Version < migrations[j].Version
|
||||
})
|
||||
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
func applyMigration(ctx context.Context, tx pgx.Tx, migration Migration) error {
|
||||
var appliedChecksum string
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT checksum
|
||||
FROM admin_schema_migrations
|
||||
WHERE version = $1`, migration.Version).Scan(&appliedChecksum)
|
||||
|
||||
if err == nil {
|
||||
if appliedChecksum != migration.Checksum {
|
||||
return fmt.Errorf("migration %s checksum changed", migration.Version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return fmt.Errorf("check migration %s: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(migration.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read migration %s: %w", migration.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, string(content)); err != nil {
|
||||
return fmt.Errorf("apply migration %s: %w", migration.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO admin_schema_migrations (version, checksum)
|
||||
VALUES ($1, $2)`, migration.Version, migration.Checksum); err != nil {
|
||||
return fmt.Errorf("record migration %s: %w", migration.Version, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checksum(content []byte) string {
|
||||
sum := sha256.Sum256(content)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue