store

package
v0.0.0-...-809e4d2 Latest Latest
Warning

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

Go to latest
Published: May 21, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package store defines persistence interfaces + an in-memory test implementation for the CMS engine.

Per gocodealone-multisite SPEC.md:

V12: per-tenant data writes ! include tenant_id WHERE clause.
V15: CMS-rendered page → tenant_id from session; ⊥ from URL/header.
V22: page filtered by (tenant_id, subsite_label).

Production wiring: postgres-backed implementation lives in gocodealone-multisite (per the host's schema in migrations/0001). This package owns the interface and an in-memory store for tests + local dev.

Package store persists CMS records.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrTenantNotFound  = errors.New("store: tenant not found")
	ErrTenantSlugTaken = errors.New("store: tenant slug taken")
	ErrDomainNotFound  = errors.New("store: domain not found")
	ErrDomainTaken     = errors.New("store: domain host taken")
)

Sentinel errors.

View Source
var ErrNotFound = errors.New("page not found")

ErrNotFound is returned when a page does not exist OR exists in a different tenant scope. Callers ! NOT distinguish the two cases — leaking tenant existence cross-tenant violates V12/V16.

View Source
var ErrPathConflict = errors.New("page path conflict")

ErrPathConflict is returned when a Create or Update would produce a duplicate (tenant_id, subsite, path) tuple.

Functions

This section is empty.

Types

type Domain

type Domain struct {
	ID           int64
	TenantID     int64
	Host         string // exact, lowercase, e.g. "cool-band.com"
	SubsiteLabel string // empty = root subsite
	Kind         string // "vanity" | "preview" | "subdomain"
}

Domain is a vanity / preview / subdomain owned by a tenant.

Per SPEC V22: subsite scope is host-level (set on the Domain), not path-level — `cool-band.com` and `cool-band.com/tour` resolve to the same tenant but different subsites.

type MemoryPageStore

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

MemoryPageStore is an in-memory PageStore for tests + local dev. Production uses a postgres-backed implementation that wires the same interface.

func NewMemoryPageStore

func NewMemoryPageStore() *MemoryPageStore

NewMemoryPageStore returns an empty in-memory store.

func (*MemoryPageStore) Create

func (s *MemoryPageStore) Create(_ context.Context, tenantID int64, p *Page) error

Create inserts the page. Returns ErrPathConflict if (tenant, subsite, path) is already taken.

func (*MemoryPageStore) Delete

func (s *MemoryPageStore) Delete(_ context.Context, tenantID, id int64) error

Delete removes the page. ErrNotFound on miss / wrong tenant.

func (*MemoryPageStore) Get

func (s *MemoryPageStore) Get(_ context.Context, tenantID, id int64) (*Page, error)

Get returns a copy of the page. tenant_id mismatch → ErrNotFound (no leak — V12/V16).

func (*MemoryPageStore) GetByPath

func (s *MemoryPageStore) GetByPath(_ context.Context, tenantID int64, subsite, path string) (*Page, error)

GetByPath looks up by (tenant, subsite, path).

func (*MemoryPageStore) List

func (s *MemoryPageStore) List(_ context.Context, tenantID int64, subsite string) ([]*Page, error)

List returns pages for the tenant, optionally filtered by subsite. Empty subsite arg = ALL pages for the tenant (including NULL-subsite + every subsite).

func (*MemoryPageStore) Update

func (s *MemoryPageStore) Update(_ context.Context, tenantID int64, p *Page) error

Update writes the page. Returns ErrNotFound if no record matches (tenant_id, id). Bumps Version + UpdatedAt.

type MemoryTenantAdminStore

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

MemoryTenantAdminStore is the in-memory implementation used for tests and as the default until a persistent store is wired.

func NewMemoryTenantAdminStore

func NewMemoryTenantAdminStore() *MemoryTenantAdminStore

func (*MemoryTenantAdminStore) CreateDomain

func (s *MemoryTenantAdminStore) CreateDomain(_ context.Context, d *Domain) error

func (*MemoryTenantAdminStore) CreateTenant

func (s *MemoryTenantAdminStore) CreateTenant(_ context.Context, t *Tenant) error

func (*MemoryTenantAdminStore) DeleteDomain

func (s *MemoryTenantAdminStore) DeleteDomain(_ context.Context, tenantID, id int64) error

func (*MemoryTenantAdminStore) DeleteTenant

func (s *MemoryTenantAdminStore) DeleteTenant(_ context.Context, id int64) error

func (*MemoryTenantAdminStore) GetTenant

func (s *MemoryTenantAdminStore) GetTenant(_ context.Context, id int64) (*Tenant, error)

func (*MemoryTenantAdminStore) ListDomains

func (s *MemoryTenantAdminStore) ListDomains(_ context.Context, tenantID int64) ([]*Domain, error)

func (*MemoryTenantAdminStore) ListTenants

func (s *MemoryTenantAdminStore) ListTenants(_ context.Context) ([]*Tenant, error)

func (*MemoryTenantAdminStore) UpdateTenant

func (s *MemoryTenantAdminStore) UpdateTenant(_ context.Context, t *Tenant) error

type Page

type Page struct {
	ID         int64
	TenantID   int64
	Subsite    string
	Path       string
	Title      string
	BodyHTML   string
	BodyBlocks json.RawMessage
	Status     PageStatus
	Version    int
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

Page is a CMS-managed dynamic page for a tenant.

Field semantics:

  • TenantID: ! non-zero (V12 multi-tenancy guard)
  • Subsite: "" → applies to all subsites; "<label>" → subsite-scoped
  • Path: URL path (e.g. "/blog/welcome"); ! unique per (tenant, subsite)
  • BodyHTML: rendered HTML for serve-time output (Render result)
  • BodyBlocks: provider-specific block JSON (source of truth)
  • Status: draft | published
  • Version: monotonic per-page edit counter

func (*Page) Validate

func (p *Page) Validate() error

Validate returns nil iff the page satisfies persistence invariants.

type PageStatus

type PageStatus string

PageStatus is the publish state of a CMS page.

const (
	StatusDraft     PageStatus = "draft"
	StatusPublished PageStatus = "published"
)

type PageStore

type PageStore interface {
	Create(ctx context.Context, tenantID int64, p *Page) error
	Get(ctx context.Context, tenantID int64, id int64) (*Page, error)
	GetByPath(ctx context.Context, tenantID int64, subsite, path string) (*Page, error)
	Update(ctx context.Context, tenantID int64, p *Page) error
	Delete(ctx context.Context, tenantID int64, id int64) error
	List(ctx context.Context, tenantID int64, subsite string) ([]*Page, error)
}

PageStore persists Page records scoped by tenant.

Every method takes tenantID as the FIRST arg AFTER ctx — making tenant-scoping impossible to forget at the call site (V12 / V15).

type Tenant

type Tenant struct {
	ID        int64
	Slug      string
	Label     string
	ThemeID   string
	CreatedAt time.Time
	UpdatedAt time.Time
}

Tenant is a multisite tenant.

type TenantAdminStore

type TenantAdminStore interface {
	// CreateTenant assigns an ID + timestamps. Slug must be unique;
	// returns ErrTenantSlugTaken otherwise.
	CreateTenant(ctx context.Context, t *Tenant) error
	GetTenant(ctx context.Context, id int64) (*Tenant, error)
	UpdateTenant(ctx context.Context, t *Tenant) error
	DeleteTenant(ctx context.Context, id int64) error
	ListTenants(ctx context.Context) ([]*Tenant, error)

	// CreateDomain attaches a Domain to an existing tenant. Host must be
	// globally unique across all tenants; returns ErrDomainTaken otherwise.
	CreateDomain(ctx context.Context, d *Domain) error
	DeleteDomain(ctx context.Context, tenantID, id int64) error
	ListDomains(ctx context.Context, tenantID int64) ([]*Domain, error)
}

TenantAdminStore is the CRUD surface for the multisite admin. It is separate from TenantStore (the resolver's read-only interface) so reads can be optimised independently from writes.

All operations are tenant-scoped via TenantID (no cross-tenant leak).

Jump to

Keyboard shortcuts

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