Files
paliad/internal/db/migrations/158_docforge_templates.up.sql
mAi 47deeaf5ed feat(docforge): slice 4 — template tables + TemplateStore (t-paliad-349)
Persistence foundation for authoring (slice 6) + generation-on-templates
(slice 7). docforge owns no tables — it defines the contract; paliad
implements it (litigationplanner pattern).

Migration 158_docforge_templates (additive, generic — NOT submission_*-named
so a second docforge consumer reuses it):
  - templates          — catalog row; current_version_id pins the live
                         version (FK added post-create to break the
                         templates<->versions cycle; ON DELETE SET NULL).
  - template_versions  — immutable snapshots; carrier .docx in a bytea
                         column (the TemplateStore bytea backend) + stylemap
                         jsonb. Versioning = snapshot-at-create (PRD A3).
  - template_slots     — variable slots per version; anchor = sentinel token
                         locating the slot in the carrier OOXML (PRD §5
                         lean), slot_key = the bound variable.
  RLS mirrors submission_bases: firm-shared SELECT for authenticated,
  mutations admin-only + gated in Go (no mutation policy = denied).

docforge root: TemplateStore interface + neutral types (TemplateMeta,
Template, TemplateSlot, *Input, TemplateFilter) + ErrTemplateNotFound.
CarrierBytes is format-opaque []byte so the root never imports the docx
adapter; the exporter wraps (CarrierBytes, Stylemap) into a docx.Carrier.

paliad: PgTemplateStore (sqlx, follows the submission_base_service pattern):
List / Get (current version) / GetVersion (pinned snapshot) / Create
(version 1 + pin) / AddVersion (next version + re-pin), all transactional.
Gated live round-trip test (TEST_DATABASE_URL) covers carrier+stylemap+slot
round-trip and the version bump. No handler wires this yet (PRD: no UI in
slice 4).

Verification: go build ./... clean, go vet clean, gofmt clean, full module
test green, migration NoDuplicateSlot structural test green.

m/paliad#157
2026-05-29 15:35:36 +02:00

128 lines
6.2 KiB
SQL

-- t-paliad-349 (m/paliad#157): docforge slice 4 — template authoring tables.
--
-- These three tables are the persistence home for the docforge authoring
-- flow (upload a base .docx → place variable slots → save as a reusable
-- template) and the generation flow (pick a template → bind data →
-- export). They are paliad's implementation of the docforge.TemplateStore
-- contract; docforge itself owns no tables (the litigationplanner pattern).
--
-- Generic on purpose (NOT submission_*-named): authoring is a
-- domain-neutral capability, so the eventual second docforge consumer can
-- reuse the same shape. submission_bases (Gitea-backed, section_spec) stays
-- for the legacy base catalog during the transition; convergence is a
-- later, separate task.
--
-- paliad.templates — one row per template (the catalog entry).
-- paliad.template_versions — immutable snapshots; editing a template
-- inserts a new version. The carrier .docx
-- bytes live here (bytea) — the TemplateStore
-- bytea backend. A draft pins a version
-- (snapshot-at-create, PRD §4 A3) so later
-- edits don't shift an in-flight draft.
-- paliad.template_slots — the variable slots placed in a version's
-- carrier. anchor is the sentinel token the
-- authoring surface injects into the carrier
-- OOXML to locate the slot (PRD §5 lean);
-- slot_key is the variable bound there.
--
-- Visibility: the template catalog is shared firm-wide (every
-- authenticated user generates from it), so SELECT is open to
-- authenticated, mirroring submission_bases. Mutations (upload, edit) are
-- admin-only and gated in Go at the handler layer — no INSERT/UPDATE/DELETE
-- RLS path means RLS denies them by default.
--
-- Slice 4 ships the schema + the TemplateStore only; no rows are seeded and
-- no UI writes here yet (authoring is slice 6, generation-on-templates is
-- slice 7).
CREATE TABLE IF NOT EXISTS paliad.templates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
slug text UNIQUE,
name_de text NOT NULL,
name_en text NOT NULL,
kind text NOT NULL DEFAULT 'submission',
source_format text NOT NULL DEFAULT 'docx',
firm text,
is_active bool NOT NULL DEFAULT true,
current_version_id uuid,
created_by uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT templates_source_format_check CHECK (source_format IN ('docx'))
);
CREATE TABLE IF NOT EXISTS paliad.template_versions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
template_id uuid NOT NULL REFERENCES paliad.templates(id) ON DELETE CASCADE,
version int NOT NULL,
carrier_blob bytea NOT NULL,
stylemap jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT template_versions_unique_per_template UNIQUE (template_id, version)
);
CREATE TABLE IF NOT EXISTS paliad.template_slots (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
template_version_id uuid NOT NULL REFERENCES paliad.template_versions(id) ON DELETE CASCADE,
slot_key text NOT NULL,
anchor text NOT NULL,
label text,
order_index int NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT template_slots_unique_anchor UNIQUE (template_version_id, anchor)
);
-- current_version_id FK is added after template_versions exists to avoid a
-- circular CREATE-TABLE dependency. ON DELETE SET NULL: dropping the
-- pinned version detaches it rather than cascading the template away.
ALTER TABLE paliad.templates
DROP CONSTRAINT IF EXISTS templates_current_version_fk;
ALTER TABLE paliad.templates
ADD CONSTRAINT templates_current_version_fk
FOREIGN KEY (current_version_id)
REFERENCES paliad.template_versions(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS templates_firm_kind_idx
ON paliad.templates (firm, kind) WHERE is_active;
CREATE INDEX IF NOT EXISTS template_versions_template_idx
ON paliad.template_versions (template_id, version);
CREATE INDEX IF NOT EXISTS template_slots_version_idx
ON paliad.template_slots (template_version_id, order_index);
ALTER TABLE paliad.templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE paliad.template_versions ENABLE ROW LEVEL SECURITY;
ALTER TABLE paliad.template_slots ENABLE ROW LEVEL SECURITY;
-- Firm-shared catalog: any authenticated user reads. Mutations are
-- admin-only, gated in Go (no mutation RLS policy = RLS denies by default).
DROP POLICY IF EXISTS templates_select ON paliad.templates;
CREATE POLICY templates_select
ON paliad.templates FOR SELECT TO authenticated
USING (true);
DROP POLICY IF EXISTS template_versions_select ON paliad.template_versions;
CREATE POLICY template_versions_select
ON paliad.template_versions FOR SELECT TO authenticated
USING (true);
DROP POLICY IF EXISTS template_slots_select ON paliad.template_slots;
CREATE POLICY template_slots_select
ON paliad.template_slots FOR SELECT TO authenticated
USING (true);
DROP TRIGGER IF EXISTS templates_set_updated_at ON paliad.templates;
CREATE TRIGGER templates_set_updated_at
BEFORE UPDATE ON paliad.templates
FOR EACH ROW EXECUTE FUNCTION paliad.tg_set_updated_at();
COMMENT ON TABLE paliad.templates IS
't-paliad-349: docforge template catalog. One row per uploaded template; current_version_id pins the live version.';
COMMENT ON TABLE paliad.template_versions IS
't-paliad-349: immutable docforge template snapshots. carrier_blob holds the base .docx bytes (TemplateStore bytea backend).';
COMMENT ON TABLE paliad.template_slots IS
't-paliad-349: variable slots placed in a template version. anchor = sentinel token locating the slot in the carrier OOXML; slot_key = the bound variable.';