@fayz-ai/plugin-forms 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,8 @@ export declare const MIGRATION_001_FRM_BASE = "-- ==============================
3
3
  export declare const MIGRATION_002_DOCUMENT_ARCHETYPE = "-- ============================================================\n-- Document Archetype \u2014 public.documents\n-- A document is any record associated with a person (or standalone):\n-- forms, images, attachments, prescriptions, contracts, etc.\n-- The `kind` column discriminates the type.\n--\n-- DATA SAFETY (converted pools): plg_forms_documents may already hold REAL\n-- rows in its PRE-archetype shape (its own `id` PK, no `document_id` column \u2014\n-- see 001_frm_base.sql). This migration MIGRATES those rows into the new\n-- document archetype instead of dropping them:\n-- * pre-archetype + rows -> copy each legacy row into public.documents\n-- (reusing its id) + into the new extension\n-- table, migrate its files, then drop legacy.\n-- * pre-archetype + empty -> plain drop + create (fresh shape).\n-- * already new shape -> no-op.\n-- ============================================================\n\nCREATE TABLE IF NOT EXISTS public.documents (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n kind text NOT NULL DEFAULT 'attachment',\n -- kind: 'form', 'image', 'attachment', 'prescription', 'contract', etc.\n person_id uuid REFERENCES public.people(id) ON DELETE SET NULL,\n title text,\n description text,\n status text NOT NULL DEFAULT 'draft',\n -- status: 'draft', 'completed', 'signed', 'archived'\n file_url text,\n file_name text,\n file_size integer,\n mime_type text,\n tags text[] DEFAULT '{}',\n notes text,\n is_active boolean NOT NULL DEFAULT true,\n metadata jsonb DEFAULT '{}',\n created_by uuid,\n updated_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_documents_tenant ON public.documents(tenant_id, kind);\nCREATE INDEX IF NOT EXISTS idx_documents_person ON public.documents(person_id);\nCREATE INDEX IF NOT EXISTS idx_documents_status ON public.documents(tenant_id, status);\n\nDROP POLICY IF EXISTS \"documents_select\" ON public.documents;\nCREATE POLICY \"documents_select\" ON public.documents\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"documents_insert\" ON public.documents;\nCREATE POLICY \"documents_insert\" ON public.documents\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"documents_update\" ON public.documents;\nCREATE POLICY \"documents_update\" ON public.documents\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"documents_delete\" ON public.documents;\nCREATE POLICY \"documents_delete\" ON public.documents\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- ============================================================\n-- Reconcile plg_forms_documents / plg_forms_document_files with the archetype.\n-- ============================================================\n\n-- Step 1: if a PRE-archetype plg_forms_documents is present (no document_id\n-- column), rename it (and its files table + dependent view) out of the way so\n-- the new archetype-shaped tables can be created and \u2014 when it holds rows \u2014\n-- its data copied across. Fully guarded, so already-migrated pools skip this.\nDO $$\nBEGIN\n IF to_regclass('public.plg_forms_documents') IS NOT NULL\n AND NOT EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public'\n AND table_name = 'plg_forms_documents'\n AND column_name = 'document_id'\n ) THEN\n DROP VIEW IF EXISTS public.v_frm_documents;\n ALTER TABLE public.plg_forms_documents RENAME TO plg_forms_documents_legacy;\n IF to_regclass('public.plg_forms_document_files') IS NOT NULL THEN\n ALTER TABLE public.plg_forms_document_files RENAME TO plg_forms_document_files_legacy;\n END IF;\n END IF;\nEND $$;\n\n-- Step 2: create the new archetype-shaped extension tables. IF NOT EXISTS makes\n-- this a no-op on pools already migrated to the new shape; after Step 1 the\n-- names are free on pre-archetype pools.\n\n-- plg_forms_documents: extension table for form-type documents\nCREATE TABLE IF NOT EXISTS public.plg_forms_documents (\n document_id uuid PRIMARY KEY REFERENCES public.documents(id) ON DELETE CASCADE,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n template_id uuid NOT NULL REFERENCES public.plg_forms_templates(id),\n data jsonb NOT NULL DEFAULT '{}',\n signed_at timestamptz,\n signed_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.plg_forms_documents ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_forms_documents_template ON public.plg_forms_documents(template_id);\n\nDROP POLICY IF EXISTS \"plg_forms_documents_select\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_select\" ON public.plg_forms_documents\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_insert\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_insert\" ON public.plg_forms_documents\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_update\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_update\" ON public.plg_forms_documents\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_documents_delete\" ON public.plg_forms_documents;\nCREATE POLICY \"plg_forms_documents_delete\" ON public.plg_forms_documents\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- plg_forms_document_files: file attachments for form fields\nCREATE TABLE IF NOT EXISTS public.plg_forms_document_files (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n document_id uuid NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,\n field_key text NOT NULL,\n file_url text NOT NULL,\n file_name text,\n file_size integer,\n mime_type text,\n sort_order integer DEFAULT 0,\n metadata jsonb DEFAULT '{}',\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nALTER TABLE public.plg_forms_document_files ENABLE ROW LEVEL SECURITY;\n\nCREATE INDEX IF NOT EXISTS idx_plg_forms_document_files_document ON public.plg_forms_document_files(document_id);\n\nDROP POLICY IF EXISTS \"plg_forms_document_files_select\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_select\" ON public.plg_forms_document_files\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_insert\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_insert\" ON public.plg_forms_document_files\n FOR INSERT TO authenticated\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_update\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_update\" ON public.plg_forms_document_files\n FOR UPDATE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nDROP POLICY IF EXISTS \"plg_forms_document_files_delete\" ON public.plg_forms_document_files;\nCREATE POLICY \"plg_forms_document_files_delete\" ON public.plg_forms_document_files\n FOR DELETE TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- Step 3: if legacy rows exist, migrate them, then drop the quarantined legacy\n-- tables. Reusing each legacy id as the documents.id keeps every legacy file's\n-- document_id FK valid after the copy. ON CONFLICT DO NOTHING makes a partial\n-- re-run safe.\nDO $$\nBEGIN\n IF to_regclass('public.plg_forms_documents_legacy') IS NOT NULL THEN\n -- 3a. base document (one per legacy form document; is_active mirrors NOT is_deleted)\n INSERT INTO public.documents (\n id, tenant_id, kind, person_id, title, status, notes, metadata,\n is_active, created_by, updated_by, created_at, updated_at\n )\n SELECT\n id, tenant_id, 'form', person_id, title, status, notes, metadata,\n NOT COALESCE(is_deleted, false), created_by, updated_by, created_at, updated_at\n FROM public.plg_forms_documents_legacy\n ON CONFLICT (id) DO NOTHING;\n\n -- 3b. form extension row (keyed by the reused document id)\n INSERT INTO public.plg_forms_documents (\n document_id, tenant_id, template_id, data, signed_at, signed_by, created_at, updated_at\n )\n SELECT\n id, tenant_id, template_id, data, signed_at, signed_by, created_at, updated_at\n FROM public.plg_forms_documents_legacy\n ON CONFLICT (document_id) DO NOTHING;\n\n -- 3c. file attachments (document_id already points at the reused id)\n IF to_regclass('public.plg_forms_document_files_legacy') IS NOT NULL THEN\n INSERT INTO public.plg_forms_document_files (\n id, tenant_id, document_id, field_key, file_url, file_name,\n file_size, mime_type, sort_order, metadata, created_at\n )\n SELECT\n id, tenant_id, document_id, field_key, file_url, file_name,\n file_size, mime_type, sort_order, metadata, created_at\n FROM public.plg_forms_document_files_legacy\n ON CONFLICT (id) DO NOTHING;\n END IF;\n END IF;\n\n -- Drop legacy remnants now that any data has been copied across.\n DROP TABLE IF EXISTS public.plg_forms_document_files_legacy;\n DROP TABLE IF EXISTS public.plg_forms_documents_legacy;\nEND $$;\n\n-- View: all documents for a person with form data joined when applicable\nCREATE OR REPLACE VIEW public.v_documents AS\nSELECT\n d.*,\n f.template_id,\n f.data AS form_data,\n f.signed_at,\n f.signed_by,\n t.name AS template_name,\n t.category AS template_category,\n p.name AS person_name\nFROM public.documents d\nLEFT JOIN public.plg_forms_documents f ON f.document_id = d.id\nLEFT JOIN public.plg_forms_templates t ON t.id = f.template_id\nLEFT JOIN public.people p ON p.id = d.person_id;\n";
4
4
  export declare const MIGRATION_003_AGENT_RPCS = "-- ============================================================================\n-- plugin-forms 003: server-plane agent write RPC.\n--\n-- public.agent_forms_upsert_template \u2014 the assistant BUILDS or EDITS a form\n-- template from any surface or channel (in-app FAB, WhatsApp, MCP). Same\n-- contract as every agent_* RPC (agenda 005, financial 009):\n-- (p_tenant_id, p_actor_user_id, p_payload jsonb) \u2192 jsonb\n-- {ok:true, id, record:{...}} | {ok:false, denial:{...}} | {ok:false, error}\n--\n-- The grid layout is computed HERE (col 0, sequential rows, full-width span) so\n-- the model only ever sends {type,label,required?,options?} \u2014 never positions.\n--\n-- Payload:\n-- template_id? uuid \u2014 omit to CREATE, provide to EDIT\n-- name? text \u2014 required on create\n-- category? text \u2014 anamnesis|evolution|report|contract|general\n-- description? text\n-- fields? [{type,label,required?,placeholder?,options?[]}] \u2014 REPLACE all\n-- append_fields? [{...}] \u2014 APPEND to existing\n--\n-- GRANT authenticated+service_role \u2014 NEVER anon (the broker calls with the pool\n-- service key and injects tenant/actor; in-browser the signed-in user calls it\n-- directly and spine RLS/actor guards bind them to their own tenant).\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.agent_forms_upsert_template(\n p_tenant_id uuid,\n p_actor_user_id uuid,\n p_payload jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path = public\nAS $$\nDECLARE\n v_denial jsonb;\n v_used int;\n v_template_id uuid;\n v_is_update boolean;\n v_name text;\n v_category text;\n v_description text;\n v_existing jsonb;\n v_input jsonb;\n v_merged jsonb;\n v_fields jsonb;\n v_schema jsonb;\n v_final_id uuid;\nBEGIN\n -- \u2500\u2500 payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n BEGIN\n v_template_id := NULLIF(p_payload->>'template_id','')::uuid;\n EXCEPTION WHEN OTHERS THEN\n RETURN jsonb_build_object('ok', false, 'error', 'invalid template_id');\n END;\n v_is_update := v_template_id IS NOT NULL;\n v_name := btrim(p_payload->>'name');\n v_category := lower(COALESCE(NULLIF(p_payload->>'category',''), 'general'));\n v_description := left(p_payload->>'description', 1000);\n IF v_category NOT IN ('anamnesis','evolution','report','contract','general') THEN\n v_category := 'general';\n END IF;\n\n IF NOT v_is_update AND (v_name IS NULL OR v_name = '') THEN\n RETURN jsonb_build_object('ok', false, 'error', 'name is required to create a form');\n END IF;\n\n -- \u2500\u2500 authorization: role \u2192 plan \u2192 form_templates cap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n SELECT count(*) INTO v_used FROM plg_forms_templates\n WHERE tenant_id = p_tenant_id AND is_deleted = false;\n v_denial := agent_guard(p_tenant_id, p_actor_user_id, 'custom_forms',\n CASE WHEN v_is_update THEN 'update' ELSE 'create' END,\n 'form_templates', v_used, CASE WHEN v_is_update THEN 0 ELSE 1 END);\n IF v_denial IS NOT NULL THEN\n RETURN jsonb_build_object('ok', false, 'denial', v_denial);\n END IF;\n\n -- \u2500\u2500 current fields (for APPEND / to reject an unknown id) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF v_is_update THEN\n SELECT COALESCE(schema->'fields','[]'::jsonb) INTO v_existing\n FROM plg_forms_templates\n WHERE id = v_template_id AND tenant_id = p_tenant_id AND is_deleted = false;\n IF v_existing IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'error', 'unknown template for this tenant');\n END IF;\n ELSE\n v_existing := '[]'::jsonb;\n END IF;\n\n -- \u2500\u2500 normalize the incoming fields to the builder's FormFieldDef shape \u2500\u2500\u2500\u2500\n -- (id, type, label, required, placeholder, col, colSpan, options) \u2014 row is\n -- assigned last, across the merged list.\n WITH src AS (\n SELECT CASE\n WHEN p_payload ? 'fields' THEN COALESCE(p_payload->'fields','[]'::jsonb)\n WHEN p_payload ? 'append_fields' THEN COALESCE(p_payload->'append_fields','[]'::jsonb)\n ELSE '[]'::jsonb\n END AS arr\n ),\n norm AS (\n SELECT f.ord, jsonb_strip_nulls(jsonb_build_object(\n 'id', COALESCE(NULLIF(f.value->>'id',''), gen_random_uuid()::text),\n 'type', COALESCE(NULLIF(f.value->>'type',''), 'text'),\n 'label', COALESCE(NULLIF(f.value->>'label',''), 'Campo'),\n 'required', COALESCE((f.value->>'required')::boolean, false),\n 'placeholder', NULLIF(f.value->>'placeholder',''),\n 'col', 0,\n 'colSpan', 12,\n 'options', CASE\n WHEN jsonb_typeof(f.value->'options') = 'array' AND jsonb_array_length(f.value->'options') > 0\n THEN (SELECT jsonb_agg(jsonb_build_object(\n 'label', opt,\n 'value', lower(regexp_replace(btrim(opt), '\\s+', '_', 'g'))))\n FROM jsonb_array_elements_text(f.value->'options') opt)\n ELSE NULL END\n )) AS field\n FROM src, jsonb_array_elements(src.arr) WITH ORDINALITY AS f(value, ord)\n )\n SELECT COALESCE(jsonb_agg(field ORDER BY ord), '[]'::jsonb) INTO v_input FROM norm;\n\n -- REPLACE with `fields`; otherwise APPEND onto what's already there.\n IF p_payload ? 'fields' THEN\n v_merged := v_input;\n ELSE\n v_merged := v_existing || v_input;\n END IF;\n\n -- sequential rows across the final list\n SELECT COALESCE(jsonb_agg(jsonb_set(e.value, '{row}', to_jsonb((e.ord - 1))) ORDER BY e.ord), '[]'::jsonb)\n INTO v_fields\n FROM jsonb_array_elements(v_merged) WITH ORDINALITY AS e(value, ord);\n\n v_schema := jsonb_build_object('layout', jsonb_build_object('columns', 12), 'fields', v_fields);\n\n -- \u2500\u2500 write + audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF v_is_update THEN\n UPDATE plg_forms_templates SET\n name = COALESCE(NULLIF(v_name,''), name),\n category = v_category,\n description = COALESCE(v_description, description),\n schema = v_schema,\n updated_by = p_actor_user_id,\n updated_at = now()\n WHERE id = v_template_id AND tenant_id = p_tenant_id AND is_deleted = false\n RETURNING id INTO v_final_id;\n ELSE\n INSERT INTO plg_forms_templates (tenant_id, name, category, description, schema, created_by, updated_by)\n VALUES (p_tenant_id, v_name, v_category, v_description, v_schema, p_actor_user_id, p_actor_user_id)\n RETURNING id INTO v_final_id;\n END IF;\n\n INSERT INTO audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)\n VALUES (p_tenant_id, p_actor_user_id,\n CASE WHEN v_is_update THEN 'agent.updateFormTemplate' ELSE 'agent.createFormTemplate' END,\n 'form_template', v_final_id::text,\n jsonb_build_object('payload', p_payload));\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_final_id,\n 'record', jsonb_build_object(\n 'ref', jsonb_build_object('id', v_final_id, 'resource', 'plg_forms_templates',\n 'archetype', 'custom_forms:template'),\n 'name', COALESCE(NULLIF(v_name,''), (SELECT name FROM plg_forms_templates WHERE id = v_final_id)),\n 'category', v_category,\n 'fieldCount', jsonb_array_length(v_fields)\n )\n );\nEND;\n$$;\n\nREVOKE ALL ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb) FROM public;\nREVOKE EXECUTE ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb) FROM anon;\nGRANT EXECUTE ON FUNCTION public.agent_forms_upsert_template(uuid, uuid, jsonb)\n TO authenticated, service_role;\n";
5
5
  export declare const MIGRATION_004_TEMPLATE_USAGE = "-- ============================================================================\n-- 004_template_usage.sql \u2014 quanto cada modelo foi usado.\n--\n-- A lista de modelos mostrava nome, categoria e \"33 campos\". Nada disso diz o\n-- que a pessoa quer saber ao abrir a tela: quais modelos o sal\u00E3o realmente usa.\n-- Um formul\u00E1rio com 38 campos e zero preenchimentos e outro com 3 campos e 400\n-- preenchimentos apareciam do mesmo jeito \u2014 e \u00E9 o segundo que n\u00E3o se mexe sem\n-- pensar duas vezes.\n--\n-- Uma view agregada e n\u00E3o uma contagem por modelo na interface: treze modelos\n-- seriam treze consultas de contagem mais treze de \"qual a \u00FAltima\", a cada vez\n-- que a tela abre. Aqui \u00E9 uma leitura s\u00F3, e o Postgres agrega onde os dados j\u00E1\n-- est\u00E3o.\n--\n-- `security_invoker` para a view herdar a RLS de `documents` /\n-- `plg_forms_documents`: sem isso ela contaria, para qualquer usu\u00E1rio, os\n-- documentos de todos os tenants do pool.\n--\n-- `is_active = false` (arquivado) n\u00E3o conta: a pergunta \u00E9 quantos documentos o\n-- modelo tem VIVOS, e um arquivo morto inflaria o n\u00FAmero que decide se o modelo\n-- pode ser mexido.\n-- ============================================================================\n\nCREATE OR REPLACE VIEW public.v_forms_template_usage\nWITH (security_invoker = true) AS\nSELECT\n f.template_id,\n f.tenant_id,\n count(*)::bigint AS document_count,\n max(d.created_at) AS last_document_at\nFROM public.plg_forms_documents f\nJOIN public.documents d\n ON d.id = f.document_id\n AND d.is_active = true\nGROUP BY f.template_id, f.tenant_id;\n\nGRANT SELECT ON public.v_forms_template_usage TO authenticated;\n";
6
+ export declare const MIGRATION_005_V_DOCUMENTS_INVOKER = "-- 005_v_documents_invoker.sql \u2014 the forms plugin carries the fix forward.\n--\n-- Core `043_view_invoker` swept nine owner-rights views, `public.v_documents`\n-- among them, because on a shared pool any signed-in user could read another\n-- tenant's patient documents. Its own header asked the plugins to carry the fix\n-- into their next migration, and this is that migration.\n--\n-- It matters more than tidiness: plugin migrations run AFTER the core chain, so\n-- `002_document_archetype` re-creates this view every time and hands the hole\n-- straight back. The core sweep can never win that race \u2014 only the file that\n-- owns the view can.\n--\n-- 900_verification/001 N16 is what caught it: green on the core chain, red the\n-- moment the optional plugins joined.\n--\n-- Idempotent: setting the option twice is a no-op.\nDO $$\nBEGIN\n IF to_regclass('public.v_documents') IS NOT NULL THEN\n ALTER VIEW public.v_documents SET (security_invoker = true);\n END IF;\nEND $$;\n\nCOMMENT ON VIEW public.v_documents IS\n 'Documents with their template joined. security_invoker: the reader''s own RLS decides, not the migration role''s (core 043, carried forward here so re-creating 002 cannot undo it).';\n";
7
+ export declare const MIGRATION_006_FORMS_ANALYTICS_READ_MODELS = "-- ============================================================================\n-- Forms \u2014 read model for the analytics engine (spine 024_analytics_engine)\n--\n-- A form submission had nowhere to be READ in bulk: it lives on the person it\n-- belongs to, one document at a time, so \"todo mundo que preencheu a ficha de\n-- matr\u00EDcula em agosto\" was a question the product could not answer. This view\n-- is that answer, and because it is a registered read model it is at the same\n-- time the KPI, the breakdown by template and the exportable list behind them.\n--\n-- WHY THE INNER JOIN. public.documents holds every attachment a person has \u2014\n-- a photo, a PDF, a contract. A row only counts as a SUBMISSION when it has a\n-- form behind it (plg_forms_documents), so the join is inner on purpose: the\n-- report would otherwise report uploads as answers.\n--\n-- WHY FLAG COLUMNS. analytics_run has no conditional aggregate (a free-form\n-- expression would be an injection point), so \"assinadas\" has to be a column.\n--\n-- Idempotent.\n-- ============================================================================\n\nCREATE OR REPLACE VIEW public.plg_forms_rep_submissions\nWITH (security_invoker = true) AS\nSELECT\n d.id AS submission_id,\n d.tenant_id,\n d.created_at,\n d.updated_at,\n d.title,\n d.status,\n d.kind,\n d.person_id,\n p.name AS person_name,\n p.email AS person_email,\n p.phone AS person_phone,\n f.template_id,\n COALESCE(NULLIF(t.name, ''), 'Sem modelo') AS template_name,\n COALESCE(NULLIF(t.category, ''), 'Sem categoria') AS template_category,\n f.signed_at,\n f.data AS form_data,\n 1 AS submissions,\n CASE WHEN f.signed_at IS NOT NULL THEN 1 ELSE 0 END AS is_signed,\n CASE WHEN d.status = 'completed' THEN 1 ELSE 0 END AS is_completed,\n CASE WHEN d.status = 'draft' THEN 1 ELSE 0 END AS is_draft\nFROM public.documents d\nJOIN public.plg_forms_documents f ON f.document_id = d.id\nLEFT JOIN public.plg_forms_templates t ON t.id = f.template_id\nLEFT JOIN public.people p ON p.id = d.person_id\nWHERE d.is_active;\n\nGRANT SELECT ON public.plg_forms_rep_submissions TO authenticated;\n\nCOMMENT ON VIEW public.plg_forms_rep_submissions IS\n 'One row per form submission (a document WITH a form behind it), joined to its template and the person who filled it.';\n\n-- Register. A view outside the allowlist cannot be reached by analytics_run at\n-- all. Guarded: a pool without the spine's 024 skips instead of failing.\nDO $$\nBEGIN\n IF to_regclass('public.plg_analytics_read_models') IS NULL THEN\n RAISE NOTICE 'forms: analytics engine not installed \u2014 read model not registered';\n RETURN;\n END IF;\n\n INSERT INTO public.plg_analytics_read_models (name, date_column, tenant_column, description) VALUES\n ('plg_forms_rep_submissions', 'created_at', 'tenant_id',\n 'One row per form submission, with template and the person who filled it')\n ON CONFLICT (name) DO UPDATE\n SET date_column = EXCLUDED.date_column,\n tenant_column = EXCLUDED.tenant_column,\n description = EXCLUDED.description;\nEND $$;\n";
6
8
  export declare const MIGRATIONS: Array<{
7
9
  id: string;
8
10
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,iyCAqBpC,CAAA;AAED,eAAO,MAAM,sBAAsB,y1PA8KlC,CAAA;AAED,eAAO,MAAM,gCAAgC,0xUA+N5C,CAAA;AAED,eAAO,MAAM,wBAAwB,20QA8KpC,CAAA;AAED,eAAO,MAAM,4BAA4B,6sDAqCxC,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAMzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,iyCAqBpC,CAAA;AAED,eAAO,MAAM,sBAAsB,y1PA8KlC,CAAA;AAED,eAAO,MAAM,gCAAgC,0xUA+N5C,CAAA;AAED,eAAO,MAAM,wBAAwB,20QA8KpC,CAAA;AAED,eAAO,MAAM,4BAA4B,6sDAqCxC,CAAA;AAED,eAAO,MAAM,iCAAiC,8sCAyB7C,CAAA;AAED,eAAO,MAAM,yCAAyC,+qGAuErD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQzD,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"TemplateListView.d.ts","sourceRoot":"","sources":["../../src/views/TemplateListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAA;AAUxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,UAAU,CAAA;AAE9D,UAAU,qBAAqB;IAC7B,KAAK,EAAE,gBAAgB,CAAA;IACvB,MAAM,EAAE,iBAAiB,CAAA;IACzB,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IACxC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,qBAAqB,qBAmNvF"}
1
+ {"version":3,"file":"TemplateListView.d.ts","sourceRoot":"","sources":["../../src/views/TemplateListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAA;AASxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,UAAU,CAAA;AAE9D,UAAU,qBAAqB;IAC7B,KAAK,EAAE,gBAAgB,CAAA;IACvB,MAAM,EAAE,iBAAiB,CAAA;IACzB,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IACxC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,qBAAqB,qBA4NvF"}
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@fayz-ai/plugin-forms",
3
3
  "fayz": {
4
- "status": "beta"
4
+ "status": "beta",
5
+ "dependencies": []
5
6
  },
6
- "version": "0.10.0",
7
+ "version": "0.10.1",
7
8
  "description": "Fayz SDK — plugin-forms plugin",
8
9
  "type": "module",
9
10
  "sideEffects": false,
@@ -30,9 +31,9 @@
30
31
  "@dnd-kit/modifiers": "^9.0.0",
31
32
  "@dnd-kit/sortable": "^10.0.0",
32
33
  "@dnd-kit/utilities": "^3.2.2",
33
- "@fayz-ai/core": "^0.15.0",
34
- "@fayz-ai/admin": "^0.15.0",
35
- "@fayz-ai/ui": "^0.15.0"
34
+ "@fayz-ai/core": "^0.16.0",
35
+ "@fayz-ai/admin": "^0.16.0",
36
+ "@fayz-ai/ui": "^0.16.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/react": "^18.3.0",