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
128 lines
6.2 KiB
SQL
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.';
|