An extension point (§5.5) marks a module name a node offers for descendants to provide/override — resolved dynamically against the inheriting app rather than lexically sealed. Lay the storage: - migration 0051: owner-polymorphic `extension_points(id, group_id?, app_id?, name)` marker table — exactly-one CHECK + per-owner partial-unique LOWER(name) indexes + lookup indexes (mirrors 0050). ON DELETE CASCADE (a marker is config, not code). No default-body column — the optional default body is a co-located kind=module script (Phase 4b stores/resolves/caches those). - extension_point_repo: `list_for_owner` + idempotent `insert_extension_point_tx` / `delete_extension_point_tx`, keyed by the shared `ScriptOwner` (mirrors the var/secret tx-fn style). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
41 lines
2.1 KiB
SQL
41 lines
2.1 KiB
SQL
-- §5.5 (v1.2 Hierarchies): extension-point markers — opt-in module polymorphism.
|
|
--
|
|
-- An extension point marks a module NAME at a node (app or group) as one
|
|
-- descendants are *expected* to provide or override. Imports of an EP name
|
|
-- resolve DYNAMICALLY against the inheriting app's view (the app can supply
|
|
-- its own module), instead of the Phase 4b lexical default (sealed to the
|
|
-- importing script's defining node). See docs §5.5.
|
|
--
|
|
-- This table holds only the MARKER (owner, name) — it is pure declaration,
|
|
-- structurally identical to a `secrets` name. The optional DEFAULT BODY is
|
|
-- just a co-located `kind = 'module'` script of the same name at this node
|
|
-- (Phase 4b already stores/resolves/caches those); there is no body column
|
|
-- here. So a marker is config, not code → ON DELETE CASCADE (unlike the
|
|
-- module body's RESTRICT in 0050).
|
|
--
|
|
-- Ownership is polymorphic (mirrors vars/secrets/scripts): exactly one of
|
|
-- (app_id, group_id) is set, with per-owner partial-unique LOWER(name)
|
|
-- indexes for case-insensitive uniqueness.
|
|
|
|
CREATE TABLE extension_points (
|
|
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 extension_points_owner_exactly_one
|
|
CHECK ((group_id IS NULL) <> (app_id IS NULL)),
|
|
name TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- One marker per (owner, name), case-insensitive. Partial because the owner
|
|
-- is split across two nullable columns.
|
|
CREATE UNIQUE INDEX extension_points_group_uidx
|
|
ON extension_points (group_id, LOWER(name)) WHERE group_id IS NOT NULL;
|
|
CREATE UNIQUE INDEX extension_points_app_uidx
|
|
ON extension_points (app_id, LOWER(name)) WHERE app_id IS NOT NULL;
|
|
|
|
-- Lookup indexes for the resolver's chain join + list-by-owner.
|
|
CREATE INDEX extension_points_group_id_idx ON extension_points (group_id) WHERE group_id IS NOT NULL;
|
|
CREATE INDEX extension_points_app_id_idx ON extension_points (app_id) WHERE app_id IS NOT NULL;
|