@fayz-ai/plugin-reports 0.12.3 → 0.13.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.
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
- import { expandBagColumns, reportColumnsToTanstack, formatValuePlain } from './chunk-4QIHTUXD.js';
1
+ import { exportToExcel } from './chunk-VQJKOO2B.js';
2
+ export { exportToExcel } from './chunk-VQJKOO2B.js';
3
+ import { exportToPdf } from './chunk-BPTKNPXQ.js';
4
+ export { exportToPdf } from './chunk-BPTKNPXQ.js';
5
+ import { expandBagColumns, formatValuePlain, reportColumnsToTanstack } from './chunk-4QIHTUXD.js';
2
6
  import * as React10 from 'react';
3
7
  import React10__default, { useState, useEffect } from 'react';
4
- import { usePluginRuntime, getAnalyticsCards, useTranslation, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, hashRouterAdapter, exportCSV } from '@fayz-ai/core';
8
+ import { usePluginRuntime, getAnalyticsCards, exportCSV, useTranslation, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, hashRouterAdapter } from '@fayz-ai/core';
5
9
  import { useOrganizationStore, useShellRailAvailable, useContextualNav, askAI, useAgentSurface, QuickActionsButton, AssistantInvite, RightRailPage } from '@fayz-ai/admin';
6
10
  import { AnalyticsContextProvider, useModuleLayout, ModulePage, collectFilters, AnalyticsControls, AnalyticsGrid, Card, FilterChip, DataTable, Button, SubpageHeader, useAnalyticsCard, useAnalyticsCardRows, ToolbarAction, PageHeaderActions, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, useAnalyticsContext, AnalyticsCard, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, Badge, getLucideIcon, Input, Checkbox } from '@fayz-ai/ui';
7
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -170,10 +174,10 @@ function searchColumns(card) {
170
174
  function camelToSnake(s) {
171
175
  return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
172
176
  }
173
- async function selectFromView(card, state, db, tenantId, reserved) {
177
+ async function selectFromView(card, state, db2, tenantId, reserved) {
174
178
  const { source } = card.query;
175
179
  const size = state.pageSize || 50;
176
- let q = db.from(source.name).select("*", { count: "exact" });
180
+ let q = db2.from(source.name).select("*", { count: "exact" });
177
181
  if (tenantId) q = q.eq(source.tenantColumn ?? "tenant_id", tenantId);
178
182
  const snapshot = card.query.dateColumn === null;
179
183
  const dateCol = card.query.dateColumn ?? "created_at";
@@ -669,6 +673,695 @@ var reportsLocales = {
669
673
  "pt-BR": ptBR
670
674
  };
671
675
 
676
+ // src/migrations/index.ts
677
+ var MIGRATION_000_BASELINE = `-- ============================================================================
678
+ -- plugins/plugin-reports/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.
679
+ --
680
+ -- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:
681
+ -- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs
682
+ -- prove the result still matches.
683
+ --
684
+ -- This replaced 3 migration files. They are kept, unapplied, in
685
+ -- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no
686
+ -- longer worth replaying.
687
+ --
688
+ -- Object order is fixed rather than dependency-sorted: schemas, types,
689
+ -- sequences, tables, defaults, functions, constraints, views, indexes, foreign
690
+ -- keys, triggers, row security, policies, grants. Dependencies BETWEEN units
691
+ -- are carried by the order packages/db/chain.json declares.
692
+ -- ============================================================================
693
+
694
+ -- A LANGUAGE sql function binds its body at CREATE time, and 570 functions
695
+ -- sorted by name are not sorted by who calls whom. This is what lets them load
696
+ -- in any order; every body is still compiled on first call, so a genuinely
697
+ -- broken reference surfaces then rather than never. Session-scoped.
698
+ SET check_function_bodies = false;
699
+
700
+
701
+
702
+ -- \u2500\u2500 default privileges: reset \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
703
+ -- Cleared BEFORE anything is created, so every object below gets the ACL the
704
+ -- dump describes rather than the image's defaults on top of it. Restored at
705
+ -- the end of this file.
706
+
707
+ DO $dp$ BEGIN
708
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
709
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
710
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
711
+ END $dp$;
712
+
713
+ DO $dp$ BEGIN
714
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
715
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
716
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
717
+ END $dp$;
718
+
719
+ DO $dp$ BEGIN
720
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
721
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
722
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
723
+ END $dp$;
724
+
725
+ DO $dp$ BEGIN
726
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
727
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
728
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
729
+ END $dp$;
730
+
731
+ DO $dp$ BEGIN
732
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
733
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
734
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
735
+ END $dp$;
736
+
737
+ DO $dp$ BEGIN
738
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
739
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
740
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
741
+ END $dp$;
742
+
743
+
744
+ -- \u2500\u2500 table \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\u2500
745
+
746
+ CREATE TABLE public.plg_reports_definitions (
747
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
748
+ tenant_id uuid,
749
+ unit_id uuid,
750
+ key text NOT NULL,
751
+ version integer DEFAULT 1 NOT NULL,
752
+ title text NOT NULL,
753
+ description text,
754
+ module text NOT NULL,
755
+ source_view text NOT NULL,
756
+ params jsonb DEFAULT '[]'::jsonb NOT NULL,
757
+ columns jsonb DEFAULT '[]'::jsonb NOT NULL,
758
+ default_order_by text,
759
+ required_permission text NOT NULL,
760
+ is_active boolean DEFAULT true NOT NULL,
761
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
762
+ created_by uuid,
763
+ owner_id uuid,
764
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
765
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
766
+ CONSTRAINT plg_reports_definitions_required_permission_check CHECK ((btrim(required_permission) <> ''::text)),
767
+ CONSTRAINT plg_reports_definitions_version_check CHECK ((version >= 1))
768
+ );
769
+
770
+ ALTER TABLE ONLY public.plg_reports_definitions FORCE ROW LEVEL SECURITY;
771
+
772
+ CREATE TABLE public.plg_reports_runs (
773
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
774
+ tenant_id uuid NOT NULL,
775
+ unit_id uuid,
776
+ definition_key text NOT NULL,
777
+ definition_version integer DEFAULT 1 NOT NULL,
778
+ params jsonb DEFAULT '{}'::jsonb NOT NULL,
779
+ requested_by uuid,
780
+ row_count bigint,
781
+ duration_ms integer,
782
+ status text DEFAULT 'ok'::text NOT NULL,
783
+ error text,
784
+ format text,
785
+ owner_id uuid,
786
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
787
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
788
+ CONSTRAINT plg_reports_runs_status_check CHECK ((status = ANY (ARRAY['ok'::text, 'refused'::text, 'error'::text])))
789
+ );
790
+
791
+ ALTER TABLE ONLY public.plg_reports_runs FORCE ROW LEVEL SECURITY;
792
+
793
+ CREATE TABLE public.plg_reports_schedules (
794
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
795
+ tenant_id uuid NOT NULL,
796
+ unit_id uuid,
797
+ definition_key text NOT NULL,
798
+ cron text NOT NULL,
799
+ params jsonb DEFAULT '{}'::jsonb NOT NULL,
800
+ recipients jsonb DEFAULT '[]'::jsonb NOT NULL,
801
+ format text DEFAULT 'csv'::text NOT NULL,
802
+ template_key text DEFAULT 'report.delivered'::text NOT NULL,
803
+ is_active boolean DEFAULT true NOT NULL,
804
+ last_run_at timestamp with time zone,
805
+ next_run_at timestamp with time zone,
806
+ owner_id uuid,
807
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
808
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
809
+ CONSTRAINT plg_reports_schedules_format_check CHECK ((format = ANY (ARRAY['csv'::text, 'xlsx'::text, 'pdf'::text])))
810
+ );
811
+
812
+ ALTER TABLE ONLY public.plg_reports_schedules FORCE ROW LEVEL SECURITY;
813
+
814
+
815
+ -- \u2500\u2500 function \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
816
+
817
+ CREATE FUNCTION public.reports_definition(p_key text, p_tenant uuid DEFAULT NULL::uuid) RETURNS public.plg_reports_definitions
818
+ LANGUAGE sql STABLE SECURITY DEFINER
819
+ SET search_path TO ''
820
+ AS $$
821
+ SELECT d.* FROM public.plg_reports_definitions d
822
+ WHERE d.key = p_key AND d.is_active
823
+ AND (d.tenant_id = coalesce(p_tenant, app.current_tenant_id()) OR d.tenant_id IS NULL)
824
+ -- A tenant's own definition beats the platform one, and the newest version
825
+ -- beats an older one.
826
+ ORDER BY (d.tenant_id IS NOT NULL) DESC, d.version DESC
827
+ LIMIT 1
828
+ $$;
829
+
830
+ CREATE FUNCTION public.reports_deliver_due(p_limit integer DEFAULT 50) RETURNS jsonb
831
+ LANGUAGE plpgsql SECURITY DEFINER
832
+ SET search_path TO ''
833
+ AS $$
834
+ DECLARE
835
+ s public.plg_reports_schedules%ROWTYPE;
836
+ v_recipient jsonb;
837
+ v_res jsonb;
838
+ v_sent integer := 0;
839
+ v_runs integer := 0;
840
+ BEGIN
841
+ IF to_regprocedure('public.notifications_enqueue(uuid, text, uuid, jsonb, text, timestamptz, text, text, uuid, uuid)') IS NULL THEN
842
+ RETURN jsonb_build_object('skipped', 'plugin-notifications is not installed');
843
+ END IF;
844
+
845
+ FOR s IN
846
+ SELECT * FROM public.plg_reports_schedules
847
+ WHERE is_active AND (next_run_at IS NULL OR next_run_at <= now())
848
+ ORDER BY next_run_at NULLS FIRST
849
+ LIMIT greatest(p_limit, 0)
850
+ FOR UPDATE SKIP LOCKED
851
+ LOOP
852
+ v_runs := v_runs + 1;
853
+ FOR v_recipient IN SELECT * FROM jsonb_array_elements(coalesce(s.recipients, '[]'::jsonb)) LOOP
854
+ -- The campaign rule applies to a report too: this ENQUEUES, it does not
855
+ -- send. One engine, one queue, one place a delivery can be retried.
856
+ v_res := to_jsonb(public.notifications_enqueue(
857
+ p_tenant_id => s.tenant_id,
858
+ p_template_key => s.template_key,
859
+ p_person_id => nullif(v_recipient ->> 'person_id', '')::uuid,
860
+ p_payload => jsonb_build_object('report_key', s.definition_key, 'format', s.format),
861
+ p_dedupe_key => 'report:' || s.id::text || ':' || to_char(now(), 'YYYY-MM-DD') || ':' || coalesce(v_recipient ->> 'person_id', ''),
862
+ p_unit_id => s.unit_id));
863
+ v_sent := v_sent + 1;
864
+ END LOOP;
865
+
866
+ UPDATE public.plg_reports_schedules
867
+ SET last_run_at = now(),
868
+ next_run_at = now() + interval '1 day'
869
+ WHERE id = s.id;
870
+ END LOOP;
871
+
872
+ RETURN jsonb_build_object('schedules', v_runs, 'queued', v_sent);
873
+ END $$;
874
+
875
+ CREATE FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text DEFAULT 'email'::text) RETURNS uuid
876
+ LANGUAGE plpgsql SECURITY DEFINER
877
+ SET search_path TO ''
878
+ AS $$
879
+ DECLARE v_id uuid;
880
+ BEGIN
881
+ IF to_regclass('public.plg_notifications_templates') IS NULL THEN
882
+ RAISE EXCEPTION 'reports_ensure_delivery_template: plugin-notifications is not installed' USING ERRCODE = '0A000';
883
+ END IF;
884
+ INSERT INTO public.plg_notifications_templates (tenant_id, key, channel, locale, subject, body, is_default, purpose)
885
+ VALUES (p_tenant, 'report.delivered', p_channel, 'pt-BR',
886
+ 'Seu relat\xF3rio est\xE1 pronto',
887
+ 'O relat\xF3rio {{report_key}} foi gerado em {{format}} e est\xE1 dispon\xEDvel.', true, 'transactional')
888
+ ON CONFLICT (tenant_id, key, channel, locale) DO UPDATE SET is_active = true
889
+ RETURNING id INTO v_id;
890
+ RETURN v_id;
891
+ END $$;
892
+
893
+ CREATE FUNCTION public.reports_run(p_key text, p_params jsonb DEFAULT '{}'::jsonb, p_limit integer DEFAULT 1000, p_offset integer DEFAULT 0, p_format text DEFAULT NULL::text) RETURNS jsonb
894
+ LANGUAGE plpgsql SECURITY DEFINER
895
+ SET search_path TO ''
896
+ AS $$
897
+ DECLARE
898
+ v_tenant uuid := app.current_tenant_id();
899
+ v_def public.plg_reports_definitions%ROWTYPE;
900
+ v_sql text;
901
+ v_where text := '';
902
+ v_param jsonb;
903
+ v_name text;
904
+ v_value text;
905
+ v_rows jsonb;
906
+ v_count bigint := 0;
907
+ v_start timestamptz := clock_timestamp();
908
+ v_has_unit boolean;
909
+ BEGIN
910
+ IF v_tenant IS NULL THEN
911
+ RAISE EXCEPTION 'reports_run: no active tenant' USING ERRCODE = '42501';
912
+ END IF;
913
+
914
+ v_def := public.reports_definition(p_key, v_tenant);
915
+ IF v_def.id IS NULL THEN
916
+ RAISE EXCEPTION 'reports_run: no active report called %', p_key USING ERRCODE = 'P0002';
917
+ END IF;
918
+
919
+ -- \u2500\u2500 refuse BEFORE the query, not after the rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
920
+ --
921
+ -- The refusal is RETURNED rather than raised, and the reason is mechanical:
922
+ -- raising rolls the statement back, taking the run record with it, and a
923
+ -- refusal nobody can see afterwards is the thing this history exists for.
924
+ -- No rows are read either way \u2014 which is what "refuse before the query" means
925
+ -- \u2014 and a caller that ignores \`status\` still gets nothing.
926
+ IF NOT (select app.has_permission(v_def.required_permission, NULL)) THEN
927
+ INSERT INTO public.plg_reports_runs (tenant_id, definition_key, definition_version, params, requested_by, status, error)
928
+ VALUES (v_tenant, p_key, v_def.version, coalesce(p_params, '{}'::jsonb), auth.uid(), 'refused',
929
+ format('%s is required', v_def.required_permission));
930
+ RETURN jsonb_build_object(
931
+ 'key', p_key, 'version', v_def.version, 'title', v_def.title,
932
+ 'status', 'refused', 'required_permission', v_def.required_permission,
933
+ 'columns', '[]'::jsonb, 'rows', '[]'::jsonb, 'row_count', 0);
934
+ END IF;
935
+
936
+ -- \u2500\u2500 scope the engine injects, and no definition can switch off \u2500\u2500\u2500\u2500\u2500\u2500\u2500
937
+ v_where := format('%I = %L', 'tenant_id', v_tenant);
938
+
939
+ SELECT EXISTS (
940
+ SELECT 1 FROM information_schema.columns
941
+ WHERE table_schema = 'public' AND table_name = v_def.source_view AND column_name = 'unit_id')
942
+ INTO v_has_unit;
943
+ IF v_has_unit THEN
944
+ v_where := v_where || ' AND (unit_id IS NULL OR (select app.has_unit(unit_id)))';
945
+ END IF;
946
+
947
+ -- \u2500\u2500 the declared parameters, and only those \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
948
+ FOR v_param IN SELECT * FROM jsonb_array_elements(coalesce(v_def.params, '[]'::jsonb)) LOOP
949
+ v_name := v_param ->> 'name';
950
+ v_value := coalesce(p_params ->> v_name, v_param ->> 'default');
951
+ IF v_value IS NULL THEN
952
+ IF coalesce((v_param ->> 'required')::boolean, false) THEN
953
+ RAISE EXCEPTION 'reports_run: % requires the parameter %', p_key, v_name USING ERRCODE = '22023';
954
+ END IF;
955
+ CONTINUE;
956
+ END IF;
957
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
958
+ WHERE table_schema = 'public' AND table_name = v_def.source_view
959
+ AND column_name = coalesce(v_param ->> 'column', v_name)) THEN
960
+ RAISE EXCEPTION 'reports_run: parameter % has no column on %', v_name, v_def.source_view USING ERRCODE = '42703';
961
+ END IF;
962
+ v_where := v_where || format(' AND %I %s %L',
963
+ coalesce(v_param ->> 'column', v_name),
964
+ CASE v_param ->> 'operator' WHEN '>=' THEN '>=' WHEN '<=' THEN '<=' WHEN '>' THEN '>' WHEN '<' THEN '<' WHEN 'ilike' THEN 'ILIKE' ELSE '=' END,
965
+ v_value);
966
+ END LOOP;
967
+
968
+ -- A parameter the caller sent that the definition never declared is refused
969
+ -- rather than ignored: silently dropping a filter shows the wrong number.
970
+ IF p_params IS NOT NULL AND jsonb_typeof(p_params) = 'object' THEN
971
+ FOR v_name IN SELECT key FROM jsonb_each_text(p_params) LOOP
972
+ IF NOT EXISTS (SELECT 1 FROM jsonb_array_elements(coalesce(v_def.params, '[]'::jsonb)) d WHERE d ->> 'name' = v_name) THEN
973
+ RAISE EXCEPTION 'reports_run: % does not declare a parameter called %', p_key, v_name USING ERRCODE = '22023';
974
+ END IF;
975
+ END LOOP;
976
+ END IF;
977
+
978
+ v_sql := format('SELECT coalesce(jsonb_agg(t), ''[]''::jsonb), count(*) FROM (SELECT * FROM public.%I WHERE %s %s LIMIT %s OFFSET %s) t',
979
+ v_def.source_view, v_where,
980
+ CASE WHEN v_def.default_order_by IS NULL THEN '' ELSE 'ORDER BY ' || quote_ident(v_def.default_order_by) END,
981
+ greatest(coalesce(p_limit, 1000), 0), greatest(coalesce(p_offset, 0), 0));
982
+
983
+ EXECUTE v_sql INTO v_rows, v_count;
984
+
985
+ INSERT INTO public.plg_reports_runs (tenant_id, definition_key, definition_version, params, requested_by,
986
+ row_count, duration_ms, status, format)
987
+ VALUES (v_tenant, p_key, v_def.version, coalesce(p_params, '{}'::jsonb), auth.uid(),
988
+ v_count, (extract(epoch FROM clock_timestamp() - v_start) * 1000)::integer, 'ok', p_format);
989
+
990
+ RETURN jsonb_build_object(
991
+ 'key', p_key, 'version', v_def.version, 'title', v_def.title,
992
+ 'status', 'ok', 'columns', v_def.columns, 'rows', v_rows, 'row_count', v_count);
993
+ END $$;
994
+
995
+ CREATE FUNCTION public.trg_plg_reports_definitions_source() RETURNS trigger
996
+ LANGUAGE plpgsql SECURITY DEFINER
997
+ SET search_path TO ''
998
+ AS $_$
999
+ DECLARE v_kind char;
1000
+ BEGIN
1001
+ IF NEW.source_view !~ '^[a-z_][a-z0-9_]*$' THEN
1002
+ RAISE EXCEPTION 'plg_reports_definitions: source_view % is not a bare relation name in public', NEW.source_view
1003
+ USING ERRCODE = '22023';
1004
+ END IF;
1005
+
1006
+ SELECT c.relkind INTO v_kind
1007
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
1008
+ WHERE n.nspname = 'public' AND c.relname = NEW.source_view;
1009
+
1010
+ IF v_kind IS NULL THEN
1011
+ RAISE EXCEPTION 'plg_reports_definitions: public.% does not exist', NEW.source_view USING ERRCODE = '42P01';
1012
+ END IF;
1013
+
1014
+ -- A view or a materialized view is a read model by construction. A TABLE is
1015
+ -- only allowed when its name says so \u2014 that is the \`plg_<x>_rep_*\` convention
1016
+ -- Phase 1 established, and it is checked rather than trusted.
1017
+ IF v_kind NOT IN ('v', 'm') AND NEW.source_view !~ '^plg_[a-z0-9_]+_rep_' THEN
1018
+ RAISE EXCEPTION 'plg_reports_definitions: public.% is a business table, not a read model', NEW.source_view
1019
+ USING ERRCODE = '23514',
1020
+ HINT = 'Point the definition at a view or at a plg_<plugin>_rep_* read model. Reporting off a business table is how a report starts holding a schema still.';
1021
+ END IF;
1022
+ RETURN NEW;
1023
+ END $_$;
1024
+
1025
+
1026
+ -- \u2500\u2500 constraint \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
1027
+
1028
+ ALTER TABLE ONLY public.plg_reports_definitions
1029
+ ADD CONSTRAINT plg_reports_definitions_pkey PRIMARY KEY (id);
1030
+
1031
+ ALTER TABLE ONLY public.plg_reports_definitions
1032
+ ADD CONSTRAINT plg_reports_definitions_tenant_id_id_key UNIQUE (tenant_id, id);
1033
+
1034
+ ALTER TABLE ONLY public.plg_reports_runs
1035
+ ADD CONSTRAINT plg_reports_runs_pkey PRIMARY KEY (id);
1036
+
1037
+ ALTER TABLE ONLY public.plg_reports_runs
1038
+ ADD CONSTRAINT plg_reports_runs_tenant_id_id_key UNIQUE (tenant_id, id);
1039
+
1040
+ ALTER TABLE ONLY public.plg_reports_schedules
1041
+ ADD CONSTRAINT plg_reports_schedules_pkey PRIMARY KEY (id);
1042
+
1043
+ ALTER TABLE ONLY public.plg_reports_schedules
1044
+ ADD CONSTRAINT plg_reports_schedules_tenant_id_id_key UNIQUE (tenant_id, id);
1045
+
1046
+
1047
+ -- \u2500\u2500 index \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\u2500
1048
+
1049
+ CREATE UNIQUE INDEX plg_reports_definitions_key_uq ON public.plg_reports_definitions USING btree (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid), key, version);
1050
+
1051
+ CREATE INDEX plg_reports_definitions_module_idx ON public.plg_reports_definitions USING btree (module) WHERE is_active;
1052
+
1053
+ CREATE INDEX plg_reports_definitions_tenant_owner_idx ON public.plg_reports_definitions USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
1054
+
1055
+ CREATE INDEX plg_reports_definitions_tenant_unit_idx ON public.plg_reports_definitions USING btree (tenant_id, unit_id);
1056
+
1057
+ CREATE INDEX plg_reports_runs_key_idx ON public.plg_reports_runs USING btree (tenant_id, definition_key, created_at DESC);
1058
+
1059
+ CREATE INDEX plg_reports_runs_tenant_owner_idx ON public.plg_reports_runs USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
1060
+
1061
+ CREATE INDEX plg_reports_runs_tenant_unit_idx ON public.plg_reports_runs USING btree (tenant_id, unit_id);
1062
+
1063
+ CREATE INDEX plg_reports_schedules_due_idx ON public.plg_reports_schedules USING btree (next_run_at) WHERE is_active;
1064
+
1065
+ CREATE INDEX plg_reports_schedules_tenant_owner_idx ON public.plg_reports_schedules USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
1066
+
1067
+ CREATE INDEX plg_reports_schedules_tenant_unit_idx ON public.plg_reports_schedules USING btree (tenant_id, unit_id);
1068
+
1069
+
1070
+ -- \u2500\u2500 fk constraint \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
1071
+
1072
+ ALTER TABLE ONLY public.plg_reports_definitions
1073
+ ADD CONSTRAINT plg_reports_definitions_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
1074
+
1075
+ ALTER TABLE ONLY public.plg_reports_definitions
1076
+ ADD CONSTRAINT plg_reports_definitions_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
1077
+
1078
+ ALTER TABLE ONLY public.plg_reports_definitions
1079
+ ADD CONSTRAINT plg_reports_definitions_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
1080
+
1081
+ ALTER TABLE ONLY public.plg_reports_runs
1082
+ ADD CONSTRAINT plg_reports_runs_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
1083
+
1084
+ ALTER TABLE ONLY public.plg_reports_runs
1085
+ ADD CONSTRAINT plg_reports_runs_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
1086
+
1087
+ ALTER TABLE ONLY public.plg_reports_runs
1088
+ ADD CONSTRAINT plg_reports_runs_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
1089
+
1090
+ ALTER TABLE ONLY public.plg_reports_schedules
1091
+ ADD CONSTRAINT plg_reports_schedules_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
1092
+
1093
+ ALTER TABLE ONLY public.plg_reports_schedules
1094
+ ADD CONSTRAINT plg_reports_schedules_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
1095
+
1096
+ ALTER TABLE ONLY public.plg_reports_schedules
1097
+ ADD CONSTRAINT plg_reports_schedules_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
1098
+
1099
+
1100
+ -- \u2500\u2500 table data \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
1101
+
1102
+ COPY public.plg_reports_definitions (id, tenant_id, unit_id, key, version, title, description, module, source_view, params, columns, default_order_by, required_permission, is_active, metadata, created_by, owner_id, created_at, updated_at) FROM stdin;
1103
+ b9bdb96e-2219-4bea-a83d-be54e28c4bda \\N \\N orders.sales 1 Vendas Every sale, whichever door it came through \u2014 the query the order vocabulary made possible. orders v_sales [{"name": "channel", "column": "channel"}, {"name": "status", "column": "status"}, {"name": "from", "column": "created_at", "operator": ">="}, {"name": "to", "column": "created_at", "operator": "<="}] [] created_at orders.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1104
+ 706d0d78-193c-4abe-8828-5e00d719d155 \\N \\N orders.fulfillment 1 Entregas pendentes Ordered against delivered, per line. A positive pending is what a boolean could never describe. orders v_order_fulfillment [{"name": "state", "column": "state"}] [] \\N orders.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1105
+ 6e27d897-55d8-4f39-b963-b5cb3121069d \\N \\N financial.receivables 1 Contas a receber Instalments and what is still owed on them, straight from the allocations. financial v_financial_installment_balances [{"name": "status", "column": "status"}, {"name": "due_from", "column": "due_date", "operator": ">="}, {"name": "due_to", "column": "due_date", "operator": "<="}] [] due_date financial.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1106
+ 66d7f336-fff1-40ea-b754-6b158f1b0dea \\N \\N financial.uninvoiced 1 Vendas sem t\xEDtulo Sales that produced no invoice. Empty is the bar this phase closes on. financial v_orders_sem_invoice [{"name": "kind", "column": "kind"}] [] created_at financial.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1107
+ a5a3ffab-e6e1-46ad-942f-f36582b4e126 \\N \\N inventory.balances 1 Saldo de estoque The balance, from the one ledger that owns it. inventory v_inventory_balances [] [] \\N inventory.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1108
+ a2d3191e-5a8d-4e03-892f-0c9573675ea5 \\N \\N notifications.queue 1 Fila de notifica\xE7\xF5es What is waiting, what is overdue, what was abandoned and what was refused. notifications v_notification_queue [{"name": "status", "column": "status"}, {"name": "channel", "column": "channel"}] [] \\N notifications.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1109
+ c616ae83-b861-41fb-959a-bbb85431ed77 \\N \\N agenda.appointments 1 Agendamentos The calendar as a table, with the Venda and the payment state each booking carries. agenda v_appointments [{"name": "status", "column": "status"}, {"name": "from", "column": "starts_at", "operator": ">="}, {"name": "to", "column": "starts_at", "operator": "<="}] [] starts_at agenda.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1110
+ 5af4a8c8-903b-4ea6-8829-98877b017883 \\N \\N crm.funnel 1 Funil The funnel over plg_crm_deals, with won_at and lost_at \u2014 which is what makes a cycle length measurable. crm v_deals [{"name": "status", "column": "status"}, {"name": "stage", "column": "stage_name"}] [] created_at crm.pipeline.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1111
+ 9143ce67-ecaa-46a3-8028-7c354c34f301 \\N \\N marketing.campaigns 1 Campanhas Reach, delivery and conversion per campaign \u2014 the metric comes from here, never from a counter on the campaign. marketing v_marketing_campaign_metrics [{"name": "status", "column": "status"}] [] \\N marketing.read t {} \\N \\N 2026-09-04 15:03:27.267053+00 2026-09-04 15:03:27.267053+00
1112
+ \\.
1113
+
1114
+
1115
+ -- \u2500\u2500 trigger \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
1116
+
1117
+ CREATE TRIGGER plg_reports_definitions_source BEFORE INSERT OR UPDATE OF source_view ON public.plg_reports_definitions FOR EACH ROW EXECUTE FUNCTION public.trg_plg_reports_definitions_source();
1118
+
1119
+ CREATE TRIGGER plg_reports_definitions_updated_at BEFORE UPDATE ON public.plg_reports_definitions FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
1120
+
1121
+ CREATE TRIGGER plg_reports_runs_updated_at BEFORE UPDATE ON public.plg_reports_runs FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
1122
+
1123
+ CREATE TRIGGER plg_reports_schedules_updated_at BEFORE UPDATE ON public.plg_reports_schedules FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
1124
+
1125
+
1126
+ -- \u2500\u2500 row security \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
1127
+
1128
+ ALTER TABLE public.plg_reports_definitions ENABLE ROW LEVEL SECURITY;
1129
+
1130
+ ALTER TABLE public.plg_reports_runs ENABLE ROW LEVEL SECURITY;
1131
+
1132
+ ALTER TABLE public.plg_reports_schedules ENABLE ROW LEVEL SECURITY;
1133
+
1134
+
1135
+ -- \u2500\u2500 policy \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
1136
+
1137
+ CREATE POLICY plg_reports_definitions_authz_delete ON public.plg_reports_definitions FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.definition'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.definition'::text, plg_reports_definitions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_definitions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.delete'::text, plg_reports_definitions.unit_id) AS has_permission)));
1138
+
1139
+ CREATE POLICY plg_reports_definitions_authz_insert ON public.plg_reports_definitions FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.create'::text, plg_reports_definitions.unit_id) AS has_permission)));
1140
+
1141
+ CREATE POLICY plg_reports_definitions_authz_select ON public.plg_reports_definitions FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.definition'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.definition'::text, plg_reports_definitions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_definitions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.read'::text, plg_reports_definitions.unit_id) AS has_permission)));
1142
+
1143
+ CREATE POLICY plg_reports_definitions_authz_update ON public.plg_reports_definitions FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.definition'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.definition'::text, plg_reports_definitions.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_definitions.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_definitions.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_definitions.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_definitions.unit_id) AS has_permission)));
1144
+
1145
+ CREATE POLICY plg_reports_definitions_platform_read ON public.plg_reports_definitions FOR SELECT TO authenticated USING (((tenant_id IS NULL) AND is_active));
1146
+
1147
+ CREATE POLICY plg_reports_runs_authz_delete ON public.plg_reports_runs FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.run'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.run'::text, plg_reports_runs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_runs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.delete'::text, plg_reports_runs.unit_id) AS has_permission)));
1148
+
1149
+ CREATE POLICY plg_reports_runs_authz_insert ON public.plg_reports_runs FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.create'::text, plg_reports_runs.unit_id) AS has_permission)));
1150
+
1151
+ CREATE POLICY plg_reports_runs_authz_select ON public.plg_reports_runs FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.run'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.run'::text, plg_reports_runs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_runs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.read'::text, plg_reports_runs.unit_id) AS has_permission)));
1152
+
1153
+ CREATE POLICY plg_reports_runs_authz_update ON public.plg_reports_runs FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.run'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.run'::text, plg_reports_runs.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_runs.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_runs.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_runs.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_runs.unit_id) AS has_permission)));
1154
+
1155
+ CREATE POLICY plg_reports_schedules_authz_delete ON public.plg_reports_schedules FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.schedule'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.schedule'::text, plg_reports_schedules.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_schedules.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.delete'::text, plg_reports_schedules.unit_id) AS has_permission)));
1156
+
1157
+ CREATE POLICY plg_reports_schedules_authz_insert ON public.plg_reports_schedules FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.create'::text, plg_reports_schedules.unit_id) AS has_permission)));
1158
+
1159
+ CREATE POLICY plg_reports_schedules_authz_select ON public.plg_reports_schedules FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.schedule'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.schedule'::text, plg_reports_schedules.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_schedules.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.read'::text, plg_reports_schedules.unit_id) AS has_permission)));
1160
+
1161
+ CREATE POLICY plg_reports_schedules_authz_update ON public.plg_reports_schedules FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('reports.schedule'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('reports.schedule'::text, plg_reports_schedules.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('reports.read_all'::text, plg_reports_schedules.unit_id) AS has_permission)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_schedules.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_reports_schedules.unit_id) AS has_unit)) AND ( SELECT app.has_permission('reports.edit'::text, plg_reports_schedules.unit_id) AS has_permission)));
1162
+
1163
+
1164
+ -- \u2500\u2500 acl \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\u2500\u2500\u2500
1165
+
1166
+ GRANT MAINTAIN ON TABLE public.plg_reports_definitions TO anon;
1167
+ GRANT ALL ON TABLE public.plg_reports_definitions TO authenticated;
1168
+ GRANT ALL ON TABLE public.plg_reports_definitions TO service_role;
1169
+
1170
+ REVOKE ALL ON FUNCTION public.reports_definition(p_key text, p_tenant uuid) FROM PUBLIC;
1171
+ GRANT ALL ON FUNCTION public.reports_definition(p_key text, p_tenant uuid) TO authenticated;
1172
+ GRANT ALL ON FUNCTION public.reports_definition(p_key text, p_tenant uuid) TO service_role;
1173
+
1174
+ REVOKE ALL ON FUNCTION public.reports_deliver_due(p_limit integer) FROM PUBLIC;
1175
+ GRANT ALL ON FUNCTION public.reports_deliver_due(p_limit integer) TO service_role;
1176
+
1177
+ REVOKE ALL ON FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text) FROM PUBLIC;
1178
+ GRANT ALL ON FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text) TO authenticated;
1179
+ GRANT ALL ON FUNCTION public.reports_ensure_delivery_template(p_tenant uuid, p_channel text) TO service_role;
1180
+
1181
+ REVOKE ALL ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) FROM PUBLIC;
1182
+ GRANT ALL ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) TO authenticated;
1183
+ GRANT ALL ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) TO service_role;
1184
+
1185
+ REVOKE ALL ON FUNCTION public.trg_plg_reports_definitions_source() FROM PUBLIC;
1186
+ GRANT ALL ON FUNCTION public.trg_plg_reports_definitions_source() TO service_role;
1187
+
1188
+ GRANT MAINTAIN ON TABLE public.plg_reports_runs TO anon;
1189
+ GRANT ALL ON TABLE public.plg_reports_runs TO authenticated;
1190
+ GRANT ALL ON TABLE public.plg_reports_runs TO service_role;
1191
+
1192
+ GRANT MAINTAIN ON TABLE public.plg_reports_schedules TO anon;
1193
+ GRANT ALL ON TABLE public.plg_reports_schedules TO authenticated;
1194
+ GRANT ALL ON TABLE public.plg_reports_schedules TO service_role;
1195
+
1196
+
1197
+ -- \u2500\u2500 comment \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
1198
+
1199
+ COMMENT ON TABLE public.plg_reports_definitions IS 'The report catalogue (PRD 08). Versioned, so changing a report does not rewrite the history of everyone who ran the previous one; and \`required_permission\` is declared HERE rather than on a screen, so the engine can refuse before it queries.';
1200
+
1201
+ COMMENT ON FUNCTION public.reports_deliver_due(p_limit integer) IS 'Queue the scheduled reports that are due (PRD 08). It enqueues through the notification engine rather than sending \u2014 the same mechanism as dunning and as a campaign, which is what made building that engine first worth doing.';
1202
+
1203
+ COMMENT ON FUNCTION public.reports_run(p_key text, p_params jsonb, p_limit integer, p_offset integer, p_format text) IS 'Run a report from the catalogue (PRD 08). The permission is checked BEFORE the query is built \u2014 a refusal reads no rows and comes back as status=refused, recorded in plg_reports_runs \u2014 the tenant and unit predicates are injected by the engine and cannot be switched off by a definition, and an undeclared parameter is refused rather than ignored.';
1204
+
1205
+ COMMENT ON TABLE public.plg_reports_runs IS 'Who ran what, with which parameters, against which VERSION of the definition (PRD 08). A refusal is recorded too \u2014 "the report is empty" and "you were not allowed to see it" are different answers and the history has to keep them apart.';
1206
+
1207
+ COMMENT ON TABLE public.plg_reports_schedules IS 'A report on a schedule (PRD 08). Delivery is the notification engine''s job (PRD 03) \u2014 the same queue, retry and opt-out that carry a receipt carry a report, which is the whole reason that engine was built first.';
1208
+
1209
+
1210
+ -- \u2500\u2500 default privileges: restore \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
1211
+ -- Put back, so the NEXT thing installed inherits what the chain had.
1212
+
1213
+ DO $dp$ BEGIN
1214
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1215
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1216
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1217
+ END $dp$;
1218
+
1219
+ DO $dp$ BEGIN
1220
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1221
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1222
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1223
+ END $dp$;
1224
+
1225
+ DO $dp$ BEGIN
1226
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1227
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1228
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1229
+ END $dp$;
1230
+
1231
+ DO $dp$ BEGIN
1232
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1233
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1234
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1235
+ END $dp$;
1236
+
1237
+ DO $dp$ BEGIN
1238
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1239
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1240
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
1241
+ END $dp$;
1242
+
1243
+ DO $dp$ BEGIN
1244
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1245
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1246
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
1247
+ END $dp$;
1248
+
1249
+ DO $dp$ BEGIN
1250
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1251
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1252
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
1253
+ END $dp$;
1254
+
1255
+ DO $dp$ BEGIN
1256
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1257
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1258
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
1259
+ END $dp$;
1260
+
1261
+ DO $dp$ BEGIN
1262
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1263
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1264
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1265
+ END $dp$;
1266
+
1267
+ DO $dp$ BEGIN
1268
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1269
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1270
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1271
+ END $dp$;
1272
+
1273
+ DO $dp$ BEGIN
1274
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1275
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1276
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1277
+ END $dp$;
1278
+
1279
+ DO $dp$ BEGIN
1280
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1281
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1282
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1283
+ END $dp$;
1284
+
1285
+ DO $dp$ BEGIN
1286
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
1287
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1288
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
1289
+ END $dp$;
1290
+
1291
+ DO $dp$ BEGIN
1292
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1293
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1294
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1295
+ END $dp$;
1296
+
1297
+ DO $dp$ BEGIN
1298
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1299
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1300
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1301
+ END $dp$;
1302
+
1303
+ DO $dp$ BEGIN
1304
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1305
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1306
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1307
+ END $dp$;
1308
+
1309
+ DO $dp$ BEGIN
1310
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
1311
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1312
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
1313
+ END $dp$;
1314
+
1315
+ DO $dp$ BEGIN
1316
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1317
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1318
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1319
+ END $dp$;
1320
+
1321
+ DO $dp$ BEGIN
1322
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1323
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1324
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1325
+ END $dp$;
1326
+
1327
+ DO $dp$ BEGIN
1328
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1329
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1330
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1331
+ END $dp$;
1332
+
1333
+ DO $dp$ BEGIN
1334
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
1335
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1336
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
1337
+ END $dp$;
1338
+
1339
+ DO $dp$ BEGIN
1340
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1341
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1342
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1343
+ END $dp$;
1344
+
1345
+ DO $dp$ BEGIN
1346
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1347
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1348
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1349
+ END $dp$;
1350
+
1351
+ DO $dp$ BEGIN
1352
+ EXECUTE $stmt$--
1353
+ -- PostgreSQL database dump complete
1354
+ --$stmt$;
1355
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1356
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--
1357
+ -- PostgreSQL database dump complete
1358
+ --$stmt$;
1359
+ END $dp$;
1360
+ `;
1361
+ var MIGRATIONS = [
1362
+ { id: "000_baseline", sql: MIGRATION_000_BASELINE }
1363
+ ];
1364
+
672
1365
  // src/lib/legacy.ts
673
1366
  function reportDefToCard(report, createdBy) {
674
1367
  return {
@@ -1336,9 +2029,9 @@ function ExportDropdown({ columns, data, filename, currency, title, fetchAll })
1336
2029
  const [pdfAvailable, setPdfAvailable] = useState(false);
1337
2030
  const [busy, setBusy] = useState(false);
1338
2031
  useEffect(() => {
1339
- import('./excel-DSORXL4O.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
2032
+ import('./excel-NKWJ2AMC.js').then((m) => m.isExcelAvailable()).then(setExcelAvailable).catch(() => {
1340
2033
  });
1341
- import('./pdf-DH4CJMV5.js').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {
2034
+ import('./pdf-H73MJGB6.js').then((m) => m.isPdfAvailable()).then(setPdfAvailable).catch(() => {
1342
2035
  });
1343
2036
  }, []);
1344
2037
  const withRows = async (write3) => {
@@ -1353,12 +2046,12 @@ function ExportDropdown({ columns, data, filename, currency, title, fetchAll })
1353
2046
  };
1354
2047
  const handleCsv = () => withRows((rows) => exportToCsv(columns, rows, filename, currency));
1355
2048
  const handleExcel = () => withRows(async (rows) => {
1356
- const { exportToExcel } = await import('./excel-DSORXL4O.js');
1357
- await exportToExcel(columns, rows, filename, currency);
2049
+ const { exportToExcel: exportToExcel2 } = await import('./excel-NKWJ2AMC.js');
2050
+ await exportToExcel2(columns, rows, filename, currency);
1358
2051
  });
1359
2052
  const handlePdf = () => withRows(async (rows) => {
1360
- const { exportToPdf } = await import('./pdf-DH4CJMV5.js');
1361
- await exportToPdf(columns, rows, filename, { title, currency });
2053
+ const { exportToPdf: exportToPdf2 } = await import('./pdf-H73MJGB6.js');
2054
+ await exportToPdf2(columns, rows, filename, { title, currency });
1362
2055
  });
1363
2056
  return /* @__PURE__ */ jsxs(Dropdown, { children: [
1364
2057
  /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsx(ToolbarAction, { icon: Download, caret: true, disabled: busy, children: t("reports.export") }) }),
@@ -2110,6 +2803,42 @@ function AnalyticsSettingsPage() {
2110
2803
  creating ? /* @__PURE__ */ jsx(NewReportPanel, { sources, categories, onClose: () => setCreating(false) }) : null
2111
2804
  ] });
2112
2805
  }
2806
+ function db() {
2807
+ const client = getSupabaseClientOptional();
2808
+ if (!client) throw new Error("Supabase client is not configured");
2809
+ return client;
2810
+ }
2811
+ async function runReport(key, params = {}, options) {
2812
+ const { data, error } = await db().rpc("reports_run", {
2813
+ p_key: key,
2814
+ p_params: params,
2815
+ p_limit: options?.limit ?? 1e3,
2816
+ p_offset: options?.offset ?? 0,
2817
+ p_format: options?.format ?? null
2818
+ });
2819
+ if (error) throw new Error(error.message);
2820
+ const r = data ?? {};
2821
+ return {
2822
+ key: r.key ?? key,
2823
+ version: Number(r.version) || 1,
2824
+ title: r.title ?? key,
2825
+ status: r.status === "refused" ? "refused" : "ok",
2826
+ requiredPermission: r.required_permission ?? void 0,
2827
+ columns: r.columns ?? [],
2828
+ rows: r.rows ?? [],
2829
+ rowCount: Number(r.row_count) || 0
2830
+ };
2831
+ }
2832
+ async function runAndExportReport(key, format, params = {}, options) {
2833
+ const result = await runReport(key, params, { limit: options?.limit ?? 5e3, format });
2834
+ if (result.status === "refused") return result;
2835
+ const filename = options?.filename ?? `${result.key}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
2836
+ const columns = result.columns.length > 0 ? result.columns : Object.keys(result.rows[0] ?? {}).map((key2) => ({ key: key2, label: key2 }));
2837
+ if (format === "csv") exportToCsv(columns, result.rows, filename, options?.currency);
2838
+ else if (format === "xlsx") await exportToExcel(columns, result.rows, filename, options?.currency);
2839
+ else await exportToPdf(columns, result.rows, filename, { title: result.title, currency: options?.currency });
2840
+ return result;
2841
+ }
2113
2842
  function camelToSnake2(str) {
2114
2843
  return str.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
2115
2844
  }
@@ -2347,6 +3076,18 @@ function createAnalyticsPlugin(options = {}) {
2347
3076
  // Measurement, not acquisition — what the Fayz plugin page lists for
2348
3077
  // Analytics.
2349
3078
  integrations: ["google-analytics", "hotjar"],
3079
+ // The engine, at last (Phase 2, PRD 08). This package shipped ZERO
3080
+ // migrations until now: a package and a screen, never a catalogue of
3081
+ // versioned definitions with a declared permission, a run history and an
3082
+ // export. Without them "report" meant "a query somebody wrote into a
3083
+ // screen", which is how a report ships with no authorization rule by simple
3084
+ // forgetfulness.
3085
+ migrations: MIGRATIONS.map((m) => ({
3086
+ id: `reports-${m.id}`,
3087
+ version: "1.0.0",
3088
+ sql: m.sql,
3089
+ description: m.id
3090
+ })),
2350
3091
  locales: reportsLocales
2351
3092
  };
2352
3093
  }
@@ -2367,6 +3108,6 @@ function createReportsPlugin(options) {
2367
3108
  });
2368
3109
  }
2369
3110
 
2370
- export { ANALYTICS_BASE, AnalyticsModule, AnalyticsOverview, AnalyticsReportViewer, AnalyticsSettingsPage, AnalyticsSurface, REPORTS_BASE, ReportsListPage, SETTINGS_ROUTE, aggregateRows, cardColumns, clearCardPref, createAnalyticsPlugin, createMockAnalyticsProvider, createMockReportProvider, createReportsPlugin, createSupabaseAnalyticsProvider, createSupabaseReportProvider, deleteCustomReport, fieldPalette, getReportsTenantId, markViewed, reportDefToCard, reportDefsToCards, resetAnalyticsPrefs, resolveCards, resolveCustomReport, saveCustomReport, setCardPref, setOverviewOrder, setReportsTenantId, sourceKey, useAnalyticsCards, useAnalyticsCatalogue, useAnalyticsConfig, useAnalyticsPrefs, useLastViewed, useOverviewCards };
3111
+ export { ANALYTICS_BASE, AnalyticsModule, AnalyticsOverview, AnalyticsReportViewer, AnalyticsSettingsPage, AnalyticsSurface, REPORTS_BASE, ReportsListPage, SETTINGS_ROUTE, aggregateRows, cardColumns, clearCardPref, createAnalyticsPlugin, createMockAnalyticsProvider, createMockReportProvider, createReportsPlugin, createSupabaseAnalyticsProvider, createSupabaseReportProvider, deleteCustomReport, exportToCsv, fieldPalette, getReportsTenantId, markViewed, reportDefToCard, reportDefsToCards, resetAnalyticsPrefs, resolveCards, resolveCustomReport, runAndExportReport, runReport, saveCustomReport, setCardPref, setOverviewOrder, setReportsTenantId, sourceKey, useAnalyticsCards, useAnalyticsCatalogue, useAnalyticsConfig, useAnalyticsPrefs, useLastViewed, useOverviewCards };
2371
3112
  //# sourceMappingURL=index.js.map
2372
3113
  //# sourceMappingURL=index.js.map