Finish the §11.6 KV slice: declarative authoring, read-only inspection, the
runtime journey, and docs — plus a forced SDK-name fix.
- SDK name: `shared` is a Rhai RESERVED keyword, so `kv::shared(...)` is a parse
error. Renamed the handle constructor to `kv::shared_collection(...)`
(keeps the user's "shared" intent; unambiguous). Updated everywhere.
- CLI manifest: `collections = [...]` on `[group]` only (ManifestApp has no such
field + deny_unknown_fields → an app manifest carrying it is a hard error);
Manifest::collections() accessor; build_bundle emits it.
- plan/apply: a `collection` row group in `pic plan`; created/deleted counts in
the apply summary; collections threaded through PlanDto/NodePlanDto/
ApplyReportDto.
- read-only `pic collections ls --group` → GET /admin/groups/{id}/collections
(viewer+), backed by ApplyService::collection_report + CollectionInfo.
- journey: a group declares `catalog`; app A writes, app B (same subtree) reads
it back; a sibling-subtree app gets CollectionNotShared; `collections ls` +
re-apply NoOp. Full suite 114/114.
- docs: groups-and-project-tool §11.6 (KV-only MVP shipped + deferrals),
sdk-shape.md (the shared-collection handle + trust model), CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
2.6 KiB
SQL
48 lines
2.6 KiB
SQL
-- §11.6 (v1.2 Hierarchies): group-collection registry — shared cross-app data.
|
|
--
|
|
-- A row marks a collection NAME at a node as group-SHARED: every app in the
|
|
-- owning group's subtree may read it (and, with an authenticated editor+,
|
|
-- write it) via the explicit `kv::shared_collection("name")` SDK handle. Resolution is
|
|
-- nearest-ancestor-group-wins, walking the reading app's chain — that ancestry
|
|
-- walk is the isolation boundary (a foreign app's chain never contains the
|
|
-- owning group, so the name simply does not resolve). See docs §11.6.
|
|
--
|
|
-- This table holds only the MARKER (owner, name, kind) — the data itself lives
|
|
-- in a per-kind storage table (`group_kv_entries`, 0053, for kind='kv'). It is
|
|
-- pure declaration, structurally identical to an `extension_points` marker
|
|
-- (0051). A marker is config, not code → ON DELETE CASCADE.
|
|
--
|
|
-- Ownership is polymorphic (mirrors vars/secrets/scripts/extension_points):
|
|
-- exactly one of (app_id, group_id) is set. MVP authoring is restricted to
|
|
-- GROUP owners at the manifest layer (an app-declared shared collection is
|
|
-- degenerate — only that app would read it); the polymorphic shape keeps the
|
|
-- owner-generic reconcile path uniform with the other inheritable kinds.
|
|
--
|
|
-- `kind` is the storage discriminator. Only 'kv' ships in the MVP; the column
|
|
-- + CHECK exist now so docs/files/topics/queue slot in later without a
|
|
-- migration, and so a future `docs` collection of the same name is a distinct
|
|
-- row (the unique index covers `kind`).
|
|
|
|
CREATE TABLE group_collections (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
group_id UUID REFERENCES groups(id) ON DELETE CASCADE,
|
|
app_id UUID REFERENCES apps(id) ON DELETE CASCADE,
|
|
CONSTRAINT group_collections_owner_exactly_one
|
|
CHECK ((group_id IS NULL) <> (app_id IS NULL)),
|
|
name TEXT NOT NULL,
|
|
kind TEXT NOT NULL DEFAULT 'kv' CHECK (kind IN ('kv')),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- One marker per (owner, name, kind), case-insensitive. Partial because the
|
|
-- owner is split across two nullable columns.
|
|
CREATE UNIQUE INDEX group_collections_group_uidx
|
|
ON group_collections (group_id, LOWER(name), kind) WHERE group_id IS NOT NULL;
|
|
CREATE UNIQUE INDEX group_collections_app_uidx
|
|
ON group_collections (app_id, LOWER(name), kind) WHERE app_id IS NOT NULL;
|
|
|
|
-- Lookup indexes for the resolver's chain join + list-by-owner.
|
|
CREATE INDEX group_collections_group_id_idx ON group_collections (group_id) WHERE group_id IS NOT NULL;
|
|
CREATE INDEX group_collections_app_id_idx ON group_collections (app_id) WHERE app_id IS NOT NULL;
|