Add revision-aware CMS taxonomies and relationships

This commit is contained in:
2026-09-10 12:37:21 -04:00
parent a16283efd7
commit 57d74bf601
15 changed files with 986 additions and 4 deletions
+74
View File
@@ -0,0 +1,74 @@
<!-- SPDX-License-Identifier: MPL-2.0 -->
# Classification without a page builder
`cms` defines small editorial values. `cmssqlite` stores them in caller-owned
SQLite transactions. Neither package owns your content, router, templates,
permissions or billing model. Use them alongside typed Go records and coded
templates, not as a database-defined application builder.
## Two distinct relationships
- A **taxonomy** names a flat vocabulary, such as Categories or Topics. A **term**
has a stable ID, editable name/description and an address. Renaming its address
preserves aliases; retiring it hides discovery without rewriting old revisions.
- An **explicit link** joins two `{kind, id}` references. `Related` reads that
single edge in either direction. Sharing a term alone does not assert a link.
References contain no titles, URLs, application data or authorization. Resolve
each public result through the owning repository using its current published
revision and availability. Never render the latest draft merely because an older
revision is published. Products may have additional availability/approval rules;
editorial links do not bypass them or change prices, entitlements or purchases.
## Transactions and publication
Call `CreateSchema(ctx, tx)` during an explicit application migration. It creates
the `gwf_cms_*` tables, independently of the authentication adapter's schema.
There is no automatic migration, connection, worker or request middleware.
Enable foreign keys on every connection and use the application's existing
SQLite write discipline. Keep backups and schema compatibility in that owner.
Every reader/writer takes a validated namespace. A namespace separates data; it
does not authorize the caller. Check permissions before reads and inside the
application's mutation boundary where concurrent revocation matters.
Within the same transaction as your content revision and audit:
1. `PutRevision` stores the exact revision's immutable term/link selections.
2. For publication, `SetPublished` points at that revision. Zero unpublishes.
3. Commit content, associations, publication and audit together.
Saving a draft does not advance publication. Restore by copying the selected
historical association document into a new revision, then publish separately.
Terms must exist in the namespace; retired terms remain valid historical values.
Applications decide which retired selections may be retained in new edits.
External targets can be indexed with an empty published snapshot, but their
owning module still determines whether a public link is available.
Taxonomy/term writes use expected revisions (zero for creation). Keep immutable
IDs across name changes. Taxonomy addresses are fixed after creation; term
addresses retain redirect history. A collision or stale revision returns
`cms.ErrConflict`, not a successful overwrite. Transactions must be rolled back
after any mutation error, including a later application audit failure.
## Bounds and discovery
- Each snapshot accepts at most 24 distinct terms and 16 distinct links, without
self-links. Validation rejects invalid IDs, control characters and duplicate
selections. Text fields have explicit byte bounds; names are not HTML.
- A namespace has at most 100 taxonomies. `Terms` pages by stable term ID, at
most 200 results per request. Applications should choose their own overall
editor limits and search UI rather than loading an unbounded catalog.
- `Members` and `Related` return at most 100 published references per page,
ordered by kind/ID. Pass `Next` for the following page. Publication filtering
happens before pagination; never use a mutable title as a cursor.
- Owning-module visibility can filter further. Continue fetching bounded index
pages to fill a visible page and construct a cursor from the last visible
item; do not expose private titles or identifiers through error messages.
The package tests use real SQLite transactions, including WAL, race execution,
scope isolation, delayed publication, reverse discovery, aliases, retirement,
pagination and rollback. Consumer tests still need to prove actual HTTP/API
permissions, public visibility, escaping, editor usability and application data
preservation. These packages do not claim to provide an entire CMS.
+9
View File
@@ -8,6 +8,15 @@ application concern belongs in the shared module.
## Gamertan accounts and commerce
- Typed content needs shared categories and relationships without becoming a
page builder. The `cms`/`cmssqlite` boundary separates application-owned bodies
and products from immutable editorial associations. Keeping associations and
publication in the content transaction prevents a draft save from changing
public reverse links. Stable references avoid rewriting purchases on a project
rename. Consumer HTTP tests caught compiled article snapshots shadowing CMS
revisions and catalog pickers showing newer draft titles; those are application
routing/visibility responsibilities, not extra policy in this package.
- Personal identity editing is not instance administration. `OwnProfileRepository`
derives self-access from the active session; `ProfileEdit` is a trusted internal
command, never a browser request model. SQLite schema 11 adds a monotonic
+1 -1
View File
@@ -26,7 +26,7 @@ The packages are ordinary Go imports. Pin the current preview and verify its
module checksum:
```bash
go get gamertan.com/web/requestmeta@v0.1.0-preview.26
go get gamertan.com/web/requestmeta@v0.1.0-preview.27
go mod verify
```