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 ¶
- Variables
- func ChangedPageStates(before, after []PageState) ([]PageState, []PageState)
- func CheckPageBatch(state PageSet, batch PageBatch, allowRestore bool) error
- func WithPageWriteActor(ctx context.Context, actor string) context.Context
- type Domain
- type MemoryPageStore
- func (s *MemoryPageStore) ApplyPageBatch(ctx context.Context, tenantID int64, batch PageBatch) (PageBatchReceipt, error)
- func (s *MemoryPageStore) Create(ctx context.Context, tenantID int64, p *Page) error
- func (s *MemoryPageStore) Delete(ctx context.Context, tenantID, id int64, expectedVersion int) error
- func (s *MemoryPageStore) Get(_ context.Context, tenantID, id int64) (*Page, error)
- func (s *MemoryPageStore) GetByPath(_ context.Context, tenantID int64, subsite, path string) (*Page, error)
- func (s *MemoryPageStore) List(_ context.Context, tenantID int64, subsite string) ([]*Page, error)
- func (s *MemoryPageStore) ReadPageHistory(_ context.Context, tenantID int64, q PageHistoryQuery) (PageHistory, error)
- func (s *MemoryPageStore) ReadPageState(_ context.Context, tenantID int64) (PageSet, error)
- func (s *MemoryPageStore) RollbackPageBatch(ctx context.Context, tenantID int64, receipt PageBatchReceipt) (PageBatchReceipt, error)
- func (s *MemoryPageStore) Update(ctx context.Context, tenantID int64, p *Page) error
- type MemoryTenantAdminStore
- func (s *MemoryTenantAdminStore) CreateDomain(_ context.Context, d *Domain) error
- func (s *MemoryTenantAdminStore) CreateTenant(_ context.Context, t *Tenant) error
- func (s *MemoryTenantAdminStore) DeleteDomain(_ context.Context, tenantID, id int64) error
- func (s *MemoryTenantAdminStore) DeleteTenant(_ context.Context, id int64) error
- func (s *MemoryTenantAdminStore) GetTenant(_ context.Context, id int64) (*Tenant, error)
- func (s *MemoryTenantAdminStore) ListDomains(_ context.Context, tenantID int64) ([]*Domain, error)
- func (s *MemoryTenantAdminStore) ListTenants(_ context.Context) ([]*Tenant, error)
- func (s *MemoryTenantAdminStore) UpdateTenant(_ context.Context, t *Tenant) error
- type Page
- type PageBaseline
- type PageBatch
- type PageBatchReceipt
- type PageBatchStore
- type PageContent
- type PageHistory
- type PageHistoryEntry
- type PageHistoryQuery
- type PageHistoryStore
- type PageMutation
- type PageSet
- type PageState
- type PageStatus
- type PageStore
- type Tenant
- type TenantAdminStore
Constants ¶
This section is empty.
Variables ¶
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.
var ErrBatchInvalid = errors.New("page batch invalid")
var ErrHistoryQuery = errors.New("invalid page history query")
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.
var ErrPathConflict = errors.New("page path conflict")
ErrPathConflict is returned when a Create or Update would produce a duplicate (tenant_id, subsite, path) tuple.
var ErrVersionConflict = errors.New("page changed; reload before saving or deleting")
ErrVersionConflict refuses stale or missing write preconditions.
Functions ¶
func ChangedPageStates ¶ added in v0.1.8
ChangedPageStates includes version-only saves and excludes untouched pages.
func CheckPageBatch ¶ added in v0.1.8
CheckPageBatch compares the entire tenant baseline and checks the final paths before any mutation. Omitted pages remain untouched. restore is never accepted by ordinary apply; it is generated only by the guarded rollback implementation.
func WithPageWriteActor ¶ added in v0.1.8
WithPageWriteActor carries the host's authenticated stable identity into a store transaction. It grants no authority. Never populate it from a request body, arbitrary header, or unverified token claim.
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) ApplyPageBatch ¶ added in v0.1.8
func (s *MemoryPageStore) ApplyPageBatch(ctx context.Context, tenantID int64, batch PageBatch) (PageBatchReceipt, error)
func (*MemoryPageStore) Create ¶
Create inserts the page. Returns ErrPathConflict if (tenant, subsite, path) is already taken.
func (*MemoryPageStore) Delete ¶
func (s *MemoryPageStore) Delete(ctx context.Context, tenantID, id int64, expectedVersion int) error
Delete removes the page. ErrNotFound on miss / wrong tenant.
func (*MemoryPageStore) Get ¶
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 ¶
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) ReadPageHistory ¶ added in v0.1.8
func (s *MemoryPageStore) ReadPageHistory(_ context.Context, tenantID int64, q PageHistoryQuery) (PageHistory, error)
func (*MemoryPageStore) ReadPageState ¶ added in v0.1.8
func (*MemoryPageStore) RollbackPageBatch ¶ added in v0.1.8
func (s *MemoryPageStore) RollbackPageBatch(ctx context.Context, tenantID int64, receipt PageBatchReceipt) (PageBatchReceipt, error)
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) ListDomains ¶
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
TemplateID string
PublishAt *time.Time
UnpublishAt *time.Time
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: legacy/rendered HTML fallback for serve-time output
- BodyBlocks: provider-specific block JSON (source of truth when present)
- TemplateID: optional template shell identifier
- PublishAt: optional first public-render timestamp
- UnpublishAt: optional timestamp after which public rendering stops
- Status: draft | published | scheduled | archived
- Version: monotonic per-page edit counter
type PageBaseline ¶ added in v0.1.8
type PageBaseline struct {
ID int64 `json:"id"`
Version int `json:"version"`
Digest string `json:"digest"`
}
func Baseline ¶ added in v0.1.8
func Baseline(states []PageState) []PageBaseline
type PageBatch ¶ added in v0.1.8
type PageBatch struct {
TargetScope string `json:"target_scope"`
BaselineRevision int64 `json:"baseline_revision"`
Baseline []PageBaseline `json:"baseline"`
Mutations []PageMutation `json:"mutations"`
}
func BatchFor ¶ added in v0.1.8
func BatchFor(state PageSet, mutations []PageMutation) PageBatch
func RollbackBatch ¶ added in v0.1.8
func RollbackBatch(r PageBatchReceipt) (PageBatch, error)
type PageBatchReceipt ¶ added in v0.1.8
type PageBatchReceipt struct {
TargetScope string `json:"target_scope"`
BeforeRevision int64 `json:"before_revision"`
AfterRevision int64 `json:"after_revision"`
Before []PageState `json:"before"`
After []PageState `json:"after"`
Mapping map[string]int64 `json:"mapping"`
Digest string `json:"digest"`
}
func (*PageBatchReceipt) Seal ¶ added in v0.1.8
func (r *PageBatchReceipt) Seal()
Seal detects archive corruption. It is not a signature or authorization.
type PageBatchStore ¶ added in v0.1.8
type PageBatchStore interface {
ReadPageState(context.Context, int64) (PageSet, error)
ApplyPageBatch(context.Context, int64, PageBatch) (PageBatchReceipt, error)
RollbackPageBatch(context.Context, int64, PageBatchReceipt) (PageBatchReceipt, error)
}
PageBatchStore is internal persistence, not an authorized publication API. The host must provide approval, fencing, verified bundles and durable backups.
type PageContent ¶ added in v0.1.8
type PageContent struct {
Subsite string `json:"subsite"`
Path string `json:"path"`
Title string `json:"title"`
BodyHTML string `json:"body_html"`
BodyBlocks json.RawMessage `json:"body_blocks,omitempty"`
Status PageStatus `json:"status"`
TemplateID string `json:"template_id,omitempty"`
PublishAt *time.Time `json:"publish_at,omitempty"`
UnpublishAt *time.Time `json:"unpublish_at,omitempty"`
}
PageContent is an explicit content allowlist; it carries no tenant authority.
func ContentOf ¶ added in v0.1.8
func ContentOf(p *Page) PageContent
func (PageContent) Digest ¶ added in v0.1.8
func (c PageContent) Digest() string
func (PageContent) Page ¶ added in v0.1.8
func (c PageContent) Page(tenantID, id int64, version int) *Page
Page reconstructs content in the caller's authorized tenant, not a payload tenant.
func (PageContent) Validate ¶ added in v0.1.8
func (c PageContent) Validate() error
type PageHistory ¶ added in v0.1.8
type PageHistory struct {
Scope string `json:"scope"`
HistoryStartRevision int64 `json:"history_start_revision"`
CurrentRevision int64 `json:"current_revision"`
Entries []PageHistoryEntry `json:"entries"`
NextRevision int64 `json:"next_revision"`
HasMore bool `json:"has_more"`
}
HistoryStartRevision is the head at adoption. No entries before it are claimed or fabricated. CurrentRevision is read atomically with this page of history. NextRevision is an exclusive cursor; HasMore signals another page.
type PageHistoryEntry ¶ added in v0.1.8
type PageHistoryEntry struct {
Scope string `json:"scope"`
Revision int64 `json:"revision"`
CommittedAt time.Time `json:"committed_at"`
Actor string `json:"actor"`
Operation string `json:"operation"`
Before []PageState `json:"before"`
After []PageState `json:"after"`
Digest string `json:"digest"`
}
PageHistoryEntry records only pages touched by one committed write. Deleted pages remain in Before. Versions, blocks, schedules and templates are saved content; Scope and Revision identify the durable tenant content namespace. Digest detects corruption; it is not a signature or proof of authorization.
func NewPageHistoryEntry ¶ added in v0.1.8
func (PageHistoryEntry) ContentDigest ¶ added in v0.1.8
func (e PageHistoryEntry) ContentDigest() string
type PageHistoryQuery ¶ added in v0.1.8
func (PageHistoryQuery) Validate ¶ added in v0.1.8
func (q PageHistoryQuery) Validate() error
type PageHistoryStore ¶ added in v0.1.8
type PageHistoryStore interface {
ReadPageHistory(context.Context, int64, PageHistoryQuery) (PageHistory, error)
}
type PageMutation ¶ added in v0.1.8
type PageMutation struct {
Key string `json:"key"`
Kind string `json:"kind"` // create, update, delete; restore is rollback-only
TargetID int64 `json:"target_id"`
ExpectedVersion int `json:"expected_version"`
Content *PageContent `json:"content,omitempty"`
}
PageMutation never copies a source ID into a target ID implicitly.
type PageSet ¶ added in v0.1.8
type PageSet struct {
Scope string `json:"scope"`
Revision int64 `json:"revision"`
Pages []PageState `json:"pages"`
}
PageSet is read atomically in the invocation's authorized tenant. Scope is a durable content namespace, never a permission or copied tenant setting.
type PageState ¶ added in v0.1.8
type PageState struct {
ID int64 `json:"id"`
Version int `json:"version"`
Content PageContent `json:"content"`
}
type PageStatus ¶
type PageStatus string
PageStatus is the publish state of a CMS page.
const ( StatusDraft PageStatus = "draft" StatusPublished PageStatus = "published" StatusScheduled PageStatus = "scheduled" StatusArchived PageStatus = "archived" )
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, expectedVersion int) 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).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package postgres provides a Postgres-backed CMS store.
|
Package postgres provides a Postgres-backed CMS store. |
|
Package storetest provides the same mutation behavior contract for memory and real Postgres tests.
|
Package storetest provides the same mutation behavior contract for memory and real Postgres tests. |