# @platform-modules/content

## 0.4.0

### Minor Changes

- 1fd8651: Make the D1 atomicity capability truthful and stop advertising an interactive transaction the driver does not have.

  `@platform-modules/db` — `createD1Client` no longer exposes a `transaction` member or `TransactionIdentity`, and is no longer assignable to `TransactionalDatabase`. Cloudflare D1 has no interactive transaction callback API, so the previous shape was a false claim that failed at runtime with `Failed query: begin`; D1 gets a typed atomic batch seam over exactly one real `binding.batch(...)` call instead. Genuinely transactional adapters (Postgres) keep the callback-minted transaction, exact identity semantics and rollback.

  The transaction handle splits in two: `Transaction<S>` is now the dialect-neutral mutation handle (`execute()` only), while the Postgres query-builder surface (`select`/`insert`/`update`/`delete`) lives on `PostgresTransaction<S>`. Callers that use query builders inside a transaction must type against `PostgresTransaction<S>`.

  `withTransactionIdentity` becomes public API. It was marked internal, yet every package's real-Postgres harness needs it to produce a value satisfying `TransactionalDatabase`, and three packages had resorted to importing it through a relative path into `db/src` — which breaks the moment these packages are consumed as published `dist`. Consumers that build their own driver client now have a supported way to brand it.

  Every downstream consumer is migrated: transaction parameters re-annotated, PG test harnesses branded through the public helper or rebuilt on the first-party adapters, and `PgliteTransactionalDatabase` now declares the `$client` the driver actually attaches.

  Two latent defects surfaced and are fixed, both the same shape — code reading one result shape through a cast that hid the mismatch from the compiler:

  - `@platform-modules/content` — `taxonomy.ts` read `result.rows` behind an `as { rows?: unknown[] }` cast, so `isInSubtree` always returned false and the depth-cap query never matched. `moveTerm` would accept a parent inside its own subtree (creating a cycle) and allow a subtree past the depth cap, raising no error.
  - `@platform-modules/affiliate` — `maturity-sweep.ts` cast every `execute()` result to `{ rows }`. Its opening probe short-circuits the sweep when empty, so on a handle returning the other shape the sweep silently promoted nothing and moved no money.

  Both now normalize both shapes rather than casting to one: `execute()` yields a plain row array inside a platform-wrapped transaction and the driver's `{ rows }` envelope on an unwrapped handle, and consuming code must tolerate either.

  Breaking at the seam, released as minor: these packages are pre-1.0.0 and stay 0.x until the deliberate public release.
- ee63372: Generate `uuid` and `createdAt`/`updatedAt` column defaults client-side so inserts work on SQLite/D1 as well as Postgres.

  `.defaultRandom()` and `.defaultNow()` declare a *database-side* default, and drizzle inlines that default's SQL into the INSERT. Under the SQLite dialect that emits `values (gen_random_uuid(), …, now())` — neither function exists in SQLite, so every insert into an affected table fails at runtime with `no such function`. On mod-cms, where D1 is the default database, that meant creating a content entry, a revision, a taxonomy term, a form submission, a field value or a translation was impossible; the audit insert failed the same way but is swallowed by `logAudit`, so audit logging silently did nothing.

  The columns now use `$defaultFn` alone. `$defaultFn` binds the value as a parameter from the runtime, which both dialects accept.

  `.defaultRandom().$defaultFn(…)` does **not** work — the SQL default takes precedence and the function never runs. `@platform-modules/i18n-content` carried that combination on `translationValue.id` and was still emitting `gen_random_uuid()`; it is corrected here.

  Hand-written DDL is unchanged, so Postgres columns keep their `DEFAULT gen_random_uuid()` / `DEFAULT now()` and any direct-SQL insert path behaves exactly as before. No package in this repo generates DDL from these schemas.

  One behavioral note at the seam: timestamps now come from the application clock rather than the database clock. Keyset pagination over `createdAt` assumes a monotonic clock, so a writer with backward clock skew can insert a row that sorts behind an already-served page boundary and is not returned by the listing.

### Patch Changes

- a9f50dc: Add the lossless content-model migration floor with immutable type/status definitions, migration journals/checkpoints, final entry completion fields, and reversible D1/Postgres parity proof.
- b7c7f97: Keep content-revision inserts portable across Postgres and SQLite/D1 by generating the default empty `termIds` value client-side instead of emitting a Postgres-only `::jsonb` SQL default. The mod-cms D1 host now supplies monotonic revision sequence allocation through SQLite `AUTOINCREMENT` while the package retains its existing numeric revision cursor contract.
- Updated dependencies [1fd8651]
  - @platform-modules/db@0.4.0
  - @platform-modules/search@0.0.6

## 0.3.3

### Patch Changes

- Updated dependencies [df32439]
  - @platform-modules/db@0.3.0
  - @platform-modules/search@0.0.5

## 0.3.2

### Patch Changes

- @platform-modules/db@0.2.2
- @platform-modules/search@0.0.4

## 0.3.1

### Patch Changes

- @platform-modules/db@0.2.1
- @platform-modules/search@0.0.3

## 0.3.0

### Minor Changes

- a24b674: U4 hierarchical taxonomy — relational `content_terms` (self-FK parent, stored depth, COALESCE sibling-slug uniqueness) + `content_entry_terms` M:N join, replacing the flat jsonb `category`/`tag` columns on `content_entries` and `content_revisions` (revisions now snapshot `term_ids`).

  BREAKING (pre-1.0.0, no compat tax): `ContentEntry.taxonomy.{category,tag}` → `ContentEntry.terms: TermRef[]`; `ContentInput.taxonomy` → `ContentInput.termIds?: string[]`; `list()` `{category,tag}` filter → `{term, includeDescendants}`. New `./taxonomy` + `./taxonomy/migrate` subpaths: createTerm/updateTerm/moveTerm/deleteTerm/listTerms/assignTerms/termsForEntry, MAX_TERM_DEPTH=6, typed errors (TermConflict/TermCycle/TermHasChildren/TermNotFound/TermValidation), `contentTaxonomyMigrationSql`/`contentTaxonomyBackfillSql`.

  `moveTerm` requires a `TransactionalDatabase` (per-taxonomy advisory lock serializes concurrent reparents to prevent cycles) and is unavailable on `neon-http`; all other term ops run on `Querier`.

- 4ff8dca: Content revisions subpath (`@platform-modules/content/revisions`): append-only per-entry snapshot history with `snapshotRevision`, `listRevisions`, `getRevision`, `restoreRevision`, and `contentRevisionsMigrationSql`. FK cascade purges revisions on permanent delete.
- ba342c2: Add content full-text search provider (`createContentSearchProvider`), FTS migration SQL (`contentSearchMigrationSql`), and `ContentSearchCtx` for `@platform-modules/search` registry wiring.
- be159e4: content: trash/remove now require `canPublish` to take a LIVE (published/scheduled) entry offline — closes the take-offline bypass of `unpublish`'s authz. Non-live (draft/trashed) entries unchanged (canModify only). restore unchanged.

## 0.2.0

### Minor Changes

- e6db44b: Lifecycle axis: `promoteScheduled` runner (promote due scheduled entries — fixes scheduled posts never publishing) + `trash`/`restore` soft-delete (`'trashed'` status, excluded from default `list`). Zero-DDL.

## 0.1.0

### Minor Changes

- d323ab5: Add the visibility/access read-authz axis (`public` | `private` | `members`) enforced in SQL at the store seam.

  **BREAKING (0.x minor convention):** `list` and `getBySlug` gain an optional `viewer?: Actor | null` parameter before `opts`. Anonymous/missing viewers now see only `status='published' AND visibility='public'` — admin draft lists must pass a `canEditAny` viewer. `ContentEntry` gains `visibility`. New exports: `setVisibility`, `contentVisibilityMigrationSql`, `ContentVisibility` type. `Actor` gains optional `canViewMembers`.

- 5cd9136: W2 capability extensions — three new subpaths on `@platform-modules/content`:

  - `./settings` — typed KV site-options store (`createDbSettingsStore`, `settingsTableSql`, `SETTINGS_KEYS`, `SettingsValidationError`, `settingsSchema`). Prototype-pollution floor: rejects `__proto__`/`constructor`/`prototype` keys; null-proto result objects; 256-char key cap; parse-tolerant reads.
  - `./privacy` — consent parser + policy generator (`parseConsent`, `generatePrivacyPolicy`, `defaultConsent`). `parseConsent` forces `categories.necessary:true` even when the attacker-controlled blob sets it false (trust boundary).
  - `./migrate` — backup-restore data half (`exportContent`, `importContent`, `ContentArchive`, `ContentMigrateError`). **Admin-gated AT THE SEAM:** export requires `actor.canEditAny` (fail-closed full dump — info-disclosure floor); import requires `actor.canEditAny && actor.canPublish` (writes published state — closes force-publish-via-restore escalation). Restore validates + sanitizes each entry (XSS floor); `replace` mode requires a `TransactionalDatabase` (atomic delete+insert — data-loss floor); status-literal + UUID-id validation; DoS caps; generic error messages (no driver-error leak).

  - `getById(db, id, viewer?, opts?)` — new core read primitive (peer of `getBySlug`, keyed on the stable `content_entries.id` PK; first consumer: the mod-cms admin editor). Applies the IDENTICAL `visibilityPredicate(viewer)` in SQL → admin loads any status/visibility; a non-permitted viewer gets `null` with no existence oracle (read-authz info-disclosure floor, enforced in-store).

  Additive: `'read'` added to the `ContentAction` union (for the export authz error). No change to existing exports.

### Patch Changes

- Updated dependencies [f895518]
  - @platform-modules/db@0.2.0
