@fayz-ai/db 0.13.0 → 0.14.0
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/canonical-schema.json +106 -23
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +42 -37
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/000_baseline.sql +29667 -0
- package/migrations/001_the_tenant_has_apps.sql +314 -0
- package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
- package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
- package/migrations/004_the_projections_get_a_successor.sql +312 -0
- package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
- package/migrations/006_the_helpers_read_the_source.sql +153 -0
- package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
- package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
- package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
- package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
- package/migrations/011_leadcontrol_joins_the_family.sql +116 -0
- package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
- package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
- package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
- package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
- package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
- package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
- package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
- package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
- package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
- package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
- package/package.json +1 -1
- package/migrations/0000_legacy_quarantine.sql +0 -86
- package/migrations/0000b_straggler_sweep.sql +0 -37
- package/migrations/000_core_v1_convert.sql +0 -343
- package/migrations/001_core.sql +0 -372
- package/migrations/002_rls_user_tenant_ids.sql +0 -64
- package/migrations/003_invitations.sql +0 -17
- package/migrations/004_archetypes.sql +0 -343
- package/migrations/005_locations_archetype.sql +0 -18
- package/migrations/006_archetype_rls.sql +0 -74
- package/migrations/007_archetype_grants.sql +0 -4
- package/migrations/008_grants.sql +0 -5
- package/migrations/009_anon_hardening.sql +0 -19
- package/migrations/010_migration_ledger.sql +0 -46
- package/migrations/011_anon_write_revoke.sql +0 -12
- package/migrations/012_rename_migration_ledger.sql +0 -61
- package/migrations/013_sequences_rls.sql +0 -22
- package/migrations/014_plan_entitlements.sql +0 -28
- package/migrations/015_agent_guard.sql +0 -108
- package/migrations/016_agent_guard_actor.sql +0 -102
- package/migrations/017_core_addresses.sql +0 -115
- package/migrations/018_global_search.sql +0 -482
- package/migrations/019_tenant_members_person_id.sql +0 -20
- package/migrations/020_fayz_projects.sql +0 -63
- package/migrations/021_tenant_roles.sql +0 -74
- package/migrations/022_invite_acceptance.sql +0 -91
- package/migrations/023_team_visibility.sql +0 -75
- package/migrations/024_analytics_engine.sql +0 -317
- package/migrations/025_created_by.sql +0 -75
- package/migrations/026_audit_trail.sql +0 -73
- package/migrations/027_domain_events.sql +0 -266
- package/migrations/028_tenant_scoped_token.sql +0 -127
- package/migrations/029_connections.sql +0 -186
- package/migrations/030_effect_idempotency.sql +0 -159
- package/migrations/031_sync_run_message.sql +0 -39
- package/migrations/032_connection_secrets.sql +0 -227
- package/migrations/033_sync_schedule.sql +0 -651
- package/migrations/034_custom_fields.sql +0 -55
- package/migrations/035_field_registry.sql +0 -148
- package/migrations/036_analytics_run_batch.sql +0 -84
- package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
- package/migrations/038_onboarding_responses.sql +0 -103
- package/migrations/039_unit_tree.sql +0 -270
- package/migrations/040_resource_grants.sql +0 -474
- package/migrations/041_scoped_columns.sql +0 -192
- package/migrations/042_unit_scope_policies.sql +0 -145
- package/migrations/043_view_invoker.sql +0 -90
- package/migrations/044_unit_member_facts.sql +0 -47
- package/migrations/045_unit_entry.sql +0 -236
- package/migrations/046_membership_visible_to_members.sql +0 -85
- package/migrations/047_tasks.sql +0 -266
- package/migrations/048_every_login_is_a_person.sql +0 -190
- package/migrations/049_bookable_people.sql +0 -126
- package/migrations/050_authz_core.sql +0 -230
- package/migrations/051_authz_helpers.sql +0 -330
- package/migrations/052_authz_fanout.sql +0 -253
- package/migrations/053_authz_catalog_templates.sql +0 -402
- package/migrations/060_tenancy_seam.sql +0 -132
- package/migrations/061_create_tenant.sql +0 -125
- package/migrations/062_invitations.sql +0 -329
- package/migrations/063_legacy_tenancy_retired.sql +0 -568
- package/migrations/064_agent_guard_authz.sql +0 -118
- package/migrations/064b_supersede_unit_scope.sql +0 -205
- package/migrations/065_scaffold_sweep.sql +0 -139
- package/migrations/066_shadow_report.sql +0 -122
- package/migrations/070_permission_catalog.sql +0 -205
- package/migrations/071_v1_permission_map.sql +0 -266
- package/migrations/072_role_rpcs.sql +0 -126
- package/migrations/073_legacy_rbac_retire.sql +0 -471
- package/migrations/075_company_legal_entities.sql +0 -169
- package/migrations/076_tenant_settings.sql +0 -251
- package/migrations/080_people_roles.sql +0 -269
- package/migrations/081_people_identity.sql +0 -271
- package/migrations/082_people_membership_link.sql +0 -142
- package/migrations/083_people_data_migration.sql +0 -75
- package/migrations/084_people_registrations_tokens.sql +0 -282
- package/migrations/085_catalog_products.sql +0 -333
- package/migrations/086_catalog_pricing.sql +0 -194
- package/migrations/087_catalog_packages.sql +0 -134
- package/migrations/090_orders_venda.sql +0 -166
- package/migrations/091_orders_scaffold.sql +0 -92
- package/migrations/095_config_field_rules.sql +0 -261
- package/migrations/096_audit_append_only.sql +0 -256
- package/migrations/097_sequences_authorized.sql +0 -111
- package/migrations/098_holidays.sql +0 -80
- package/migrations/099_access_grants.sql +0 -293
- package/migrations/100_migration_schema.sql +0 -217
- package/migrations/101_migration_allowlist.sql +0 -202
- package/migrations/102_migration_rpcs.sql +0 -933
- package/migrations/103_migration_reconcile_ponr.sql +0 -132
- package/migrations/105_policies_to_authenticated.sql +0 -71
- package/migrations/106_function_search_path.sql +0 -55
- package/migrations/107_core_table_prefix.sql +0 -361
- package/migrations/108_authz_write_guards.sql +0 -149
- package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
- package/migrations/110_people_kind_vocabulary.sql +0 -120
- package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
- package/migrations/112_money_precision_standard.sql +0 -132
- package/migrations/113_addresses_units_companies.sql +0 -114
- package/migrations/114_migrations_ledger_immutable.sql +0 -65
- package/migrations/115_registration_honours_field_rules.sql +0 -172
- package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
- package/migrations/117_authz_reconciliation_schedule.sql +0 -82
- package/migrations/118_migration_catalog_writers.sql +0 -450
- package/migrations/119_migration_config_writers.sql +0 -193
- package/migrations/120_migration_facet_writer.sql +0 -139
- package/migrations/121_migration_financial_writers.sql +0 -294
- package/migrations/122_migration_inventory_writers.sql +0 -245
- package/migrations/123_migration_order_item_writer.sql +0 -144
- package/migrations/124_migration_ledger_writers.sql +0 -170
- package/migrations/125_migration_authz_people_writers.sql +0 -205
- package/migrations/126_owner_scoping_is_configuration.sql +0 -247
- package/migrations/127_scaffold_audit_hook.sql +0 -98
- package/migrations/128_shadow_tables_enforced.sql +0 -93
- package/migrations/129_order_kind_terminal_states.sql +0 -57
- package/migrations/130_every_login_is_a_person.sql +0 -196
- package/migrations/131_migration_invoice_split.sql +0 -127
- package/migrations/132_audit_prune_scheduled.sql +0 -28
- package/migrations/133_unit_is_a_session_property.sql +0 -155
- package/migrations/134_field_rules_on_read.sql +0 -341
- package/migrations/135_distribution_polarity.sql +0 -158
- package/migrations/136_unit_id_means_locality.sql +0 -358
- package/migrations/137_vendor_named_spine_objects.sql +0 -126
- package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
- package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
- package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
- package/migrations/142_a_person_has_addresses.sql +0 -212
- package/migrations/143_a_professional_has_a_rate.sql +0 -75
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
- package/migrations/146_products_stock_has_one_writer.sql +0 -67
- package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
- package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
- package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
- package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
- package/migrations/152_a_file_is_an_archetype.sql +0 -222
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
- package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
- package/migrations/155_a_tenant_can_leave.sql +0 -271
- package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
- package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
- package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
- package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
- package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
- package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
- package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
- package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 096_audit_append_only.sql — the audit log is append-only and its actor and
|
|
3
|
-
-- tenant are derived on the server (PRD 07 R2 / #144, Contexto §9.2 P1
|
|
4
|
-
-- "audit log pode preservar ator informado pelo cliente").
|
|
5
|
-
--
|
|
6
|
-
-- public.audit_logs exists since 001; 026 made the actor default to auth.uid()
|
|
7
|
-
-- and closed the INSERT policy. What was still open, and closes here:
|
|
8
|
-
--
|
|
9
|
-
-- actor a signed-in caller who SENDS a user_id different from auth.uid()
|
|
10
|
-
-- is refused (42501) — before, COALESCE kept the client's value.
|
|
11
|
-
-- Without a JWT (service_role, migration, definer job) an explicit
|
|
12
|
-
-- author is still allowed: a backfill must be able to say who did it.
|
|
13
|
-
-- tenant filled from app.current_tenant_id() when absent; a tenant the
|
|
14
|
-
-- caller is not a member of is refused (42501).
|
|
15
|
-
-- reason a destructive action (delete/cancel/remove/void, bare or as the
|
|
16
|
-
-- last segment of a dotted action) requires a reason (23514) —
|
|
17
|
-
-- jornadas invariant 4: "ação destrutiva deixa rastro: autor,
|
|
18
|
-
-- motivo e hora".
|
|
19
|
-
-- shape unit_id, before/after (minimal snapshots) and reason columns.
|
|
20
|
-
-- immutable no UPDATE/DELETE grant for the API roles AND a trigger that
|
|
21
|
-
-- refuses both for everyone — the retention job is the one path
|
|
22
|
-
-- (audit_logs_prune, service-only, per-tenant keep_days).
|
|
23
|
-
-- read audit.read within the tenant (043 catalog key), replacing 001's
|
|
24
|
-
-- "every member reads".
|
|
25
|
-
--
|
|
26
|
-
-- Idempotent and replay-safe.
|
|
27
|
-
-- ============================================================================
|
|
28
|
-
|
|
29
|
-
ALTER TABLE public.audit_logs ADD COLUMN IF NOT EXISTS unit_id uuid;
|
|
30
|
-
ALTER TABLE public.audit_logs ADD COLUMN IF NOT EXISTS reason text;
|
|
31
|
-
ALTER TABLE public.audit_logs ADD COLUMN IF NOT EXISTS before jsonb;
|
|
32
|
-
ALTER TABLE public.audit_logs ADD COLUMN IF NOT EXISTS after jsonb;
|
|
33
|
-
-- unit_id is a historical reference on purpose (no FK): a unit that is deleted
|
|
34
|
-
-- later must not rewrite the trail.
|
|
35
|
-
CREATE INDEX IF NOT EXISTS idx_audit_logs_unit ON public.audit_logs (tenant_id, unit_id, created_at DESC) WHERE unit_id IS NOT NULL;
|
|
36
|
-
|
|
37
|
-
-- ── which actions are destructive ───────────────────────────────────────
|
|
38
|
-
CREATE OR REPLACE FUNCTION public.audit_action_is_destructive(p_action text)
|
|
39
|
-
RETURNS boolean LANGUAGE sql IMMUTABLE AS $$
|
|
40
|
-
SELECT p_action IS NOT NULL AND (
|
|
41
|
-
lower(p_action) IN ('delete', 'cancel', 'remove', 'void', 'deleted', 'cancelled', 'removed', 'voided')
|
|
42
|
-
OR lower(p_action) ~ '\.(delete|cancel|remove|void|deleted|cancelled|removed|voided)$'
|
|
43
|
-
OR lower(p_action) ~ '^(delete|cancel|remove|void)\.'
|
|
44
|
-
);
|
|
45
|
-
$$;
|
|
46
|
-
GRANT EXECUTE ON FUNCTION public.audit_action_is_destructive(text) TO authenticated, service_role;
|
|
47
|
-
|
|
48
|
-
-- ── the actor/tenant/reason gate — BEFORE INSERT ────────────────────────
|
|
49
|
-
-- Same trigger name as 026 (audit_logs_actor) so a replay converges on one.
|
|
50
|
-
CREATE OR REPLACE FUNCTION public.handle_audit_actor()
|
|
51
|
-
RETURNS trigger
|
|
52
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
53
|
-
AS $fn$
|
|
54
|
-
DECLARE
|
|
55
|
-
v_uid uuid := auth.uid();
|
|
56
|
-
BEGIN
|
|
57
|
-
-- actor: derived from the session; a signed-in caller cannot sign as someone else
|
|
58
|
-
IF v_uid IS NOT NULL THEN
|
|
59
|
-
IF NEW.user_id IS NOT NULL AND NEW.user_id <> v_uid THEN
|
|
60
|
-
RAISE EXCEPTION 'audit_logs: the actor is derived server-side (user_id must be omitted or equal to auth.uid())'
|
|
61
|
-
USING ERRCODE = '42501';
|
|
62
|
-
END IF;
|
|
63
|
-
NEW.user_id := v_uid;
|
|
64
|
-
END IF;
|
|
65
|
-
|
|
66
|
-
-- tenant: derived when absent; must be one the actor belongs to
|
|
67
|
-
IF NEW.tenant_id IS NULL THEN
|
|
68
|
-
NEW.tenant_id := app.current_tenant_id();
|
|
69
|
-
END IF;
|
|
70
|
-
IF v_uid IS NOT NULL THEN
|
|
71
|
-
IF NEW.tenant_id IS NULL THEN
|
|
72
|
-
RAISE EXCEPTION 'audit_logs: no tenant in scope for this session' USING ERRCODE = '42501';
|
|
73
|
-
END IF;
|
|
74
|
-
IF NOT app.is_member(NEW.tenant_id) THEN
|
|
75
|
-
RAISE EXCEPTION 'audit_logs: tenant % is not one the actor is a member of', NEW.tenant_id
|
|
76
|
-
USING ERRCODE = '42501';
|
|
77
|
-
END IF;
|
|
78
|
-
END IF;
|
|
79
|
-
|
|
80
|
-
-- reason: mandatory for destructive actions
|
|
81
|
-
IF public.audit_action_is_destructive(NEW.action) AND nullif(btrim(coalesce(NEW.reason, '')), '') IS NULL THEN
|
|
82
|
-
RAISE EXCEPTION 'audit_logs: a destructive action (%) requires a reason', NEW.action
|
|
83
|
-
USING ERRCODE = '23514', HINT = 'Informe o motivo da ação.';
|
|
84
|
-
END IF;
|
|
85
|
-
|
|
86
|
-
RETURN NEW;
|
|
87
|
-
END;
|
|
88
|
-
$fn$;
|
|
89
|
-
REVOKE ALL ON FUNCTION public.handle_audit_actor() FROM public, anon, authenticated;
|
|
90
|
-
|
|
91
|
-
DROP TRIGGER IF EXISTS audit_logs_actor ON public.audit_logs;
|
|
92
|
-
CREATE TRIGGER audit_logs_actor BEFORE INSERT ON public.audit_logs
|
|
93
|
-
FOR EACH ROW EXECUTE FUNCTION public.handle_audit_actor();
|
|
94
|
-
|
|
95
|
-
-- ── append-only: nobody updates, only the retention job deletes ─────────
|
|
96
|
-
CREATE OR REPLACE FUNCTION public.audit_logs_append_only()
|
|
97
|
-
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
98
|
-
AS $fn$
|
|
99
|
-
BEGIN
|
|
100
|
-
IF TG_OP = 'DELETE' AND coalesce(current_setting('fayz.audit_logs.prune', true), 'off') = 'on' THEN
|
|
101
|
-
RETURN OLD;
|
|
102
|
-
END IF;
|
|
103
|
-
RAISE EXCEPTION 'audit_logs is append-only (% refused)', TG_OP USING ERRCODE = '42501';
|
|
104
|
-
END;
|
|
105
|
-
$fn$;
|
|
106
|
-
REVOKE ALL ON FUNCTION public.audit_logs_append_only() FROM public, anon, authenticated;
|
|
107
|
-
DROP TRIGGER IF EXISTS audit_logs_append_only ON public.audit_logs;
|
|
108
|
-
CREATE TRIGGER audit_logs_append_only BEFORE UPDATE OR DELETE ON public.audit_logs
|
|
109
|
-
FOR EACH ROW EXECUTE FUNCTION public.audit_logs_append_only();
|
|
110
|
-
|
|
111
|
-
REVOKE UPDATE, DELETE, TRUNCATE ON public.audit_logs FROM anon, authenticated;
|
|
112
|
-
GRANT SELECT, INSERT ON public.audit_logs TO authenticated;
|
|
113
|
-
GRANT ALL ON public.audit_logs TO service_role;
|
|
114
|
-
|
|
115
|
-
-- ── policies: audit.read within the tenant; insert for members ──────────
|
|
116
|
-
ALTER TABLE public.audit_logs ENABLE ROW LEVEL SECURITY;
|
|
117
|
-
DROP POLICY IF EXISTS "audit_select" ON public.audit_logs;
|
|
118
|
-
CREATE POLICY "audit_select" ON public.audit_logs FOR SELECT TO authenticated
|
|
119
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('audit.read')));
|
|
120
|
-
-- The trigger has already derived/validated tenant_id when this runs
|
|
121
|
-
-- (WITH CHECK sees the row as it will be stored).
|
|
122
|
-
DROP POLICY IF EXISTS "audit_insert" ON public.audit_logs;
|
|
123
|
-
CREATE POLICY "audit_insert" ON public.audit_logs FOR INSERT TO authenticated
|
|
124
|
-
WITH CHECK ((select app.is_member(tenant_id)));
|
|
125
|
-
|
|
126
|
-
-- ── the helper every RPC calls ──────────────────────────────────────────
|
|
127
|
-
-- Actor = auth.uid(); tenant = the session's (p_tenant is honoured only when
|
|
128
|
-
-- there is no JWT — a service job must name the tenant it acts for). Returns
|
|
129
|
-
-- the audit row id.
|
|
130
|
-
CREATE OR REPLACE FUNCTION public.audit_log(
|
|
131
|
-
p_action text,
|
|
132
|
-
p_entity_type text,
|
|
133
|
-
p_entity_id uuid,
|
|
134
|
-
p_reason text DEFAULT NULL,
|
|
135
|
-
p_before jsonb DEFAULT NULL,
|
|
136
|
-
p_after jsonb DEFAULT NULL,
|
|
137
|
-
p_unit uuid DEFAULT NULL,
|
|
138
|
-
p_metadata jsonb DEFAULT NULL,
|
|
139
|
-
p_tenant uuid DEFAULT NULL
|
|
140
|
-
)
|
|
141
|
-
RETURNS uuid
|
|
142
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
143
|
-
AS $$
|
|
144
|
-
DECLARE
|
|
145
|
-
v_uid uuid := auth.uid();
|
|
146
|
-
v_tenant uuid;
|
|
147
|
-
v_id uuid;
|
|
148
|
-
BEGIN
|
|
149
|
-
IF nullif(btrim(coalesce(p_action, '')), '') IS NULL THEN
|
|
150
|
-
RAISE EXCEPTION 'audit_log: action is required' USING ERRCODE = '22023';
|
|
151
|
-
END IF;
|
|
152
|
-
IF v_uid IS NOT NULL THEN
|
|
153
|
-
v_tenant := app.current_tenant_id();
|
|
154
|
-
IF p_tenant IS NOT NULL AND p_tenant <> v_tenant THEN
|
|
155
|
-
RAISE EXCEPTION 'audit_log: tenant is derived server-side' USING ERRCODE = '42501';
|
|
156
|
-
END IF;
|
|
157
|
-
ELSE
|
|
158
|
-
v_tenant := coalesce(p_tenant, app.current_tenant_id());
|
|
159
|
-
END IF;
|
|
160
|
-
IF v_tenant IS NULL THEN
|
|
161
|
-
RAISE EXCEPTION 'audit_log: no tenant in scope' USING ERRCODE = '42501';
|
|
162
|
-
END IF;
|
|
163
|
-
INSERT INTO public.audit_logs (tenant_id, unit_id, user_id, action, entity_type, entity_id, reason, before, after, metadata)
|
|
164
|
-
VALUES (v_tenant, p_unit, v_uid, p_action, p_entity_type, p_entity_id::text, p_reason, p_before, p_after, coalesce(p_metadata, '{}'::jsonb))
|
|
165
|
-
RETURNING id INTO v_id;
|
|
166
|
-
RETURN v_id;
|
|
167
|
-
END $$;
|
|
168
|
-
REVOKE ALL ON FUNCTION public.audit_log(text, text, uuid, text, jsonb, jsonb, uuid, jsonb, uuid) FROM public, anon;
|
|
169
|
-
GRANT EXECUTE ON FUNCTION public.audit_log(text, text, uuid, text, jsonb, jsonb, uuid, jsonb, uuid) TO authenticated, service_role;
|
|
170
|
-
|
|
171
|
-
-- ── retention: an approved policy per tenant, a service-only prune ──────
|
|
172
|
-
CREATE TABLE IF NOT EXISTS public.audit_retention_policies (
|
|
173
|
-
tenant_id uuid PRIMARY KEY REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
174
|
-
keep_days integer NOT NULL CHECK (keep_days >= 30),
|
|
175
|
-
approved_by uuid,
|
|
176
|
-
approved_at timestamptz NOT NULL DEFAULT now(),
|
|
177
|
-
note text,
|
|
178
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
179
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
180
|
-
);
|
|
181
|
-
DROP TRIGGER IF EXISTS audit_retention_policies_updated_at ON public.audit_retention_policies;
|
|
182
|
-
CREATE TRIGGER audit_retention_policies_updated_at BEFORE UPDATE ON public.audit_retention_policies
|
|
183
|
-
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
184
|
-
REVOKE ALL ON public.audit_retention_policies FROM anon, authenticated;
|
|
185
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.audit_retention_policies TO authenticated;
|
|
186
|
-
GRANT ALL ON public.audit_retention_policies TO service_role;
|
|
187
|
-
ALTER TABLE public.audit_retention_policies ENABLE ROW LEVEL SECURITY;
|
|
188
|
-
ALTER TABLE public.audit_retention_policies FORCE ROW LEVEL SECURITY;
|
|
189
|
-
-- A replay of 002 stamps generic tenant_isolation_* policies on any public
|
|
190
|
-
-- table with tenant_id; this table has curated ones — drop the generic set.
|
|
191
|
-
DROP POLICY IF EXISTS tenant_isolation_select ON public.audit_retention_policies;
|
|
192
|
-
DROP POLICY IF EXISTS tenant_isolation_insert ON public.audit_retention_policies;
|
|
193
|
-
DROP POLICY IF EXISTS tenant_isolation_update ON public.audit_retention_policies;
|
|
194
|
-
DROP POLICY IF EXISTS tenant_isolation_delete ON public.audit_retention_policies;
|
|
195
|
-
DROP POLICY IF EXISTS audit_retention_policies_select ON public.audit_retention_policies;
|
|
196
|
-
CREATE POLICY audit_retention_policies_select ON public.audit_retention_policies FOR SELECT TO authenticated
|
|
197
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('audit.read')));
|
|
198
|
-
DROP POLICY IF EXISTS audit_retention_policies_write ON public.audit_retention_policies;
|
|
199
|
-
CREATE POLICY audit_retention_policies_write ON public.audit_retention_policies FOR ALL TO authenticated
|
|
200
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('settings.manage')))
|
|
201
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('settings.manage')));
|
|
202
|
-
|
|
203
|
-
CREATE OR REPLACE FUNCTION public.audit_retention_stamp_actor()
|
|
204
|
-
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
205
|
-
BEGIN
|
|
206
|
-
NEW.approved_by := coalesce(auth.uid(), NEW.approved_by);
|
|
207
|
-
RETURN NEW;
|
|
208
|
-
END $$;
|
|
209
|
-
REVOKE ALL ON FUNCTION public.audit_retention_stamp_actor() FROM public, anon, authenticated;
|
|
210
|
-
DROP TRIGGER IF EXISTS audit_retention_policies_actor ON public.audit_retention_policies;
|
|
211
|
-
CREATE TRIGGER audit_retention_policies_actor BEFORE INSERT OR UPDATE ON public.audit_retention_policies
|
|
212
|
-
FOR EACH ROW EXECUTE FUNCTION public.audit_retention_stamp_actor();
|
|
213
|
-
|
|
214
|
-
-- Prune: rows older than p_before (optionally one tenant), or — with no
|
|
215
|
-
-- p_before — each tenant's approved keep_days. Service-only: a JWT session is
|
|
216
|
-
-- refused even if it somehow holds EXECUTE. The trigger lets the delete through
|
|
217
|
-
-- only under the transaction-local flag this function sets and clears.
|
|
218
|
-
CREATE OR REPLACE FUNCTION public.audit_logs_prune(p_before timestamptz DEFAULT NULL, p_tenant uuid DEFAULT NULL)
|
|
219
|
-
RETURNS integer
|
|
220
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
221
|
-
AS $$
|
|
222
|
-
DECLARE v_n integer := 0; v_k integer;
|
|
223
|
-
BEGIN
|
|
224
|
-
IF auth.uid() IS NOT NULL THEN
|
|
225
|
-
RAISE EXCEPTION 'audit_logs_prune: service-only' USING ERRCODE = '42501';
|
|
226
|
-
END IF;
|
|
227
|
-
PERFORM set_config('fayz.audit_logs.prune', 'on', true);
|
|
228
|
-
BEGIN
|
|
229
|
-
IF p_before IS NOT NULL THEN
|
|
230
|
-
DELETE FROM public.audit_logs a
|
|
231
|
-
WHERE a.created_at < p_before AND (p_tenant IS NULL OR a.tenant_id = p_tenant);
|
|
232
|
-
GET DIAGNOSTICS v_n = ROW_COUNT;
|
|
233
|
-
ELSE
|
|
234
|
-
DELETE FROM public.audit_logs a
|
|
235
|
-
USING public.audit_retention_policies p
|
|
236
|
-
WHERE p.tenant_id = a.tenant_id
|
|
237
|
-
AND (p_tenant IS NULL OR a.tenant_id = p_tenant)
|
|
238
|
-
AND a.created_at < now() - make_interval(days => p.keep_days);
|
|
239
|
-
GET DIAGNOSTICS v_k = ROW_COUNT;
|
|
240
|
-
v_n := v_k;
|
|
241
|
-
END IF;
|
|
242
|
-
EXCEPTION WHEN OTHERS THEN
|
|
243
|
-
PERFORM set_config('fayz.audit_logs.prune', 'off', true);
|
|
244
|
-
RAISE;
|
|
245
|
-
END;
|
|
246
|
-
PERFORM set_config('fayz.audit_logs.prune', 'off', true);
|
|
247
|
-
RETURN v_n;
|
|
248
|
-
END $$;
|
|
249
|
-
REVOKE ALL ON FUNCTION public.audit_logs_prune(timestamptz, uuid) FROM public, anon, authenticated;
|
|
250
|
-
GRANT EXECUTE ON FUNCTION public.audit_logs_prune(timestamptz, uuid) TO service_role;
|
|
251
|
-
|
|
252
|
-
COMMENT ON TABLE public.audit_logs IS
|
|
253
|
-
'Append-only audit trail. Actor (user_id) and tenant are derived server-side by the audit_logs_actor trigger; '
|
|
254
|
-
'destructive actions require a reason; UPDATE/DELETE are refused; retention via audit_logs_prune() (service-only). Read: audit.read.';
|
|
255
|
-
COMMENT ON TABLE public.audit_retention_policies IS
|
|
256
|
-
'Approved audit retention per tenant (keep_days). audit_logs_prune() with no p_before applies it.';
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 097_sequences_authorized.sql — sequences / next_sequence() authorized by
|
|
3
|
-
-- tenant (PRD 07 R3 / #144; Contexto §9.2 P1 "sequência financeira recebe
|
|
4
|
-
-- tenant sem autorização suficiente").
|
|
5
|
-
--
|
|
6
|
-
-- public.sequences and next_sequence(p_tenant_id, p_kind) were born in the
|
|
7
|
-
-- financial plugin (plugins/plugin-financial 004_order_to_cash.sql) as a
|
|
8
|
-
-- SECURITY DEFINER counter with no check on p_tenant_id: any signed-in user
|
|
9
|
-
-- could advance any tenant's REC-/PAG- numbering. The spine now owns the table
|
|
10
|
-
-- and the function (same shape, so the plugin's CREATE TABLE IF NOT EXISTS is a
|
|
11
|
-
-- no-op) and the function is guarded:
|
|
12
|
-
--
|
|
13
|
-
-- * with a JWT, p_tenant_id must be one the caller is an active member of,
|
|
14
|
-
-- and — when the session is narrowed to a tenant (claim or single
|
|
15
|
-
-- membership) — exactly that one;
|
|
16
|
-
-- * without a JWT (service_role, a migration, a definer job) the caller names
|
|
17
|
-
-- the tenant it acts for.
|
|
18
|
-
--
|
|
19
|
-
-- next_sequence_authorized(kind[, permission]) is the tenant-derived form for
|
|
20
|
-
-- new callers: the tenant is the session's, optionally gated by a permission.
|
|
21
|
-
--
|
|
22
|
-
-- The plugin's 004 file re-emits its old body on a fresh chain (plugins run
|
|
23
|
-
-- after the spine); plugin-financial 023_sequences_authorized.sql re-asserts
|
|
24
|
-
-- this exact guarded body last, and the pgTAP suite asserts the guard on the
|
|
25
|
-
-- final state. Idempotent and replay-safe.
|
|
26
|
-
-- ============================================================================
|
|
27
|
-
|
|
28
|
-
CREATE TABLE IF NOT EXISTS public.sequences (
|
|
29
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id),
|
|
30
|
-
kind text NOT NULL,
|
|
31
|
-
current_value bigint NOT NULL DEFAULT 0,
|
|
32
|
-
PRIMARY KEY (tenant_id, kind)
|
|
33
|
-
);
|
|
34
|
-
-- Deny-all for the API roles: written only through the definer functions.
|
|
35
|
-
ALTER TABLE public.sequences ENABLE ROW LEVEL SECURITY;
|
|
36
|
-
REVOKE ALL ON public.sequences FROM anon, authenticated;
|
|
37
|
-
GRANT ALL ON public.sequences TO service_role;
|
|
38
|
-
DROP POLICY IF EXISTS tenant_isolation_select ON public.sequences;
|
|
39
|
-
DROP POLICY IF EXISTS tenant_isolation_insert ON public.sequences;
|
|
40
|
-
DROP POLICY IF EXISTS tenant_isolation_update ON public.sequences;
|
|
41
|
-
DROP POLICY IF EXISTS tenant_isolation_delete ON public.sequences;
|
|
42
|
-
|
|
43
|
-
-- The one counter (internal, no API grant).
|
|
44
|
-
CREATE OR REPLACE FUNCTION app.sequence_next(p_tenant_id uuid, p_kind text)
|
|
45
|
-
RETURNS bigint LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
46
|
-
AS $$
|
|
47
|
-
DECLARE v_next bigint;
|
|
48
|
-
BEGIN
|
|
49
|
-
IF p_tenant_id IS NULL OR nullif(btrim(coalesce(p_kind, '')), '') IS NULL THEN
|
|
50
|
-
RAISE EXCEPTION 'sequence_next: tenant and kind are required' USING ERRCODE = '22023';
|
|
51
|
-
END IF;
|
|
52
|
-
INSERT INTO public.sequences (tenant_id, kind, current_value)
|
|
53
|
-
VALUES (p_tenant_id, p_kind, 1)
|
|
54
|
-
ON CONFLICT (tenant_id, kind)
|
|
55
|
-
DO UPDATE SET current_value = public.sequences.current_value + 1
|
|
56
|
-
RETURNING current_value INTO v_next;
|
|
57
|
-
RETURN v_next;
|
|
58
|
-
END $$;
|
|
59
|
-
REVOKE ALL ON FUNCTION app.sequence_next(uuid, text) FROM public, anon, authenticated;
|
|
60
|
-
GRANT EXECUTE ON FUNCTION app.sequence_next(uuid, text) TO service_role;
|
|
61
|
-
|
|
62
|
-
-- Guarded, same signature the financial plugin and its clients already call.
|
|
63
|
-
-- Body kept self-contained (no dependency on app.sequence_next) so the plugin's
|
|
64
|
-
-- 023 file can carry the identical text.
|
|
65
|
-
CREATE OR REPLACE FUNCTION public.next_sequence(p_tenant_id uuid, p_kind text)
|
|
66
|
-
RETURNS bigint
|
|
67
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
68
|
-
AS $$
|
|
69
|
-
DECLARE
|
|
70
|
-
v_next bigint;
|
|
71
|
-
v_scope uuid;
|
|
72
|
-
BEGIN
|
|
73
|
-
IF p_tenant_id IS NULL OR nullif(btrim(coalesce(p_kind, '')), '') IS NULL THEN
|
|
74
|
-
RAISE EXCEPTION 'next_sequence: tenant and kind are required' USING ERRCODE = '22023';
|
|
75
|
-
END IF;
|
|
76
|
-
IF auth.uid() IS NOT NULL THEN
|
|
77
|
-
v_scope := app.current_tenant_id();
|
|
78
|
-
IF (v_scope IS NOT NULL AND p_tenant_id <> v_scope) OR NOT app.is_member(p_tenant_id) THEN
|
|
79
|
-
RAISE EXCEPTION 'next_sequence: not authorized for tenant %', p_tenant_id USING ERRCODE = '42501';
|
|
80
|
-
END IF;
|
|
81
|
-
END IF;
|
|
82
|
-
INSERT INTO public.sequences (tenant_id, kind, current_value)
|
|
83
|
-
VALUES (p_tenant_id, p_kind, 1)
|
|
84
|
-
ON CONFLICT (tenant_id, kind)
|
|
85
|
-
DO UPDATE SET current_value = public.sequences.current_value + 1
|
|
86
|
-
RETURNING current_value INTO v_next;
|
|
87
|
-
RETURN v_next;
|
|
88
|
-
END $$;
|
|
89
|
-
REVOKE ALL ON FUNCTION public.next_sequence(uuid, text) FROM public, anon;
|
|
90
|
-
GRANT EXECUTE ON FUNCTION public.next_sequence(uuid, text) TO authenticated, service_role;
|
|
91
|
-
|
|
92
|
-
-- Tenant derived from the session; optional permission gate.
|
|
93
|
-
CREATE OR REPLACE FUNCTION public.next_sequence_authorized(p_kind text, p_permission text DEFAULT NULL)
|
|
94
|
-
RETURNS bigint
|
|
95
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
96
|
-
AS $$
|
|
97
|
-
DECLARE v_tenant uuid := app.current_tenant_id();
|
|
98
|
-
BEGIN
|
|
99
|
-
IF v_tenant IS NULL THEN
|
|
100
|
-
RAISE EXCEPTION 'next_sequence_authorized: no tenant in scope' USING ERRCODE = '42501';
|
|
101
|
-
END IF;
|
|
102
|
-
IF p_permission IS NOT NULL AND NOT app.has_permission(p_permission) THEN
|
|
103
|
-
RAISE EXCEPTION 'next_sequence_authorized: permission % denied', p_permission USING ERRCODE = '42501';
|
|
104
|
-
END IF;
|
|
105
|
-
RETURN app.sequence_next(v_tenant, p_kind);
|
|
106
|
-
END $$;
|
|
107
|
-
REVOKE ALL ON FUNCTION public.next_sequence_authorized(text, text) FROM public, anon;
|
|
108
|
-
GRANT EXECUTE ON FUNCTION public.next_sequence_authorized(text, text) TO authenticated, service_role;
|
|
109
|
-
|
|
110
|
-
COMMENT ON TABLE public.sequences IS
|
|
111
|
-
'Per-tenant numbering counters (REC-/PAG-…). Written only through next_sequence()/next_sequence_authorized(), which authorize the tenant.';
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 098_holidays.sql — the tenant/unit holiday calendar (PRD 07 R5 / #144).
|
|
3
|
-
--
|
|
4
|
-
-- V1 had two tables: holidays (per company) and central_holidays (the network's,
|
|
5
|
-
-- pushed to every unit). V2 is one: a row with unit_id NULL is the tenant's
|
|
6
|
-
-- (network-wide, what central_holidays was); a row with a unit_id is that
|
|
7
|
-
-- unit's own. holidays_for_unit(unit, from, to) is the calendar the agenda
|
|
8
|
-
-- reads: tenant-wide rows + the unit's rows (nothing for a unit that is not
|
|
9
|
-
-- the tenant's), recurring ones projected onto every year of the range.
|
|
10
|
-
--
|
|
11
|
-
-- The agenda plugin already declares a registry on `holidays` (name, date,
|
|
12
|
-
-- recurring, description, isActive) with no SQL of its own — this table is that
|
|
13
|
-
-- table, so those columns are here with the same names; the registry needs no
|
|
14
|
-
-- change. Kind (national|state|city|company) and metadata are additions.
|
|
15
|
-
--
|
|
16
|
-
-- service_locations (V1) is a spot inside a unit (a chair, a room), not a unit —
|
|
17
|
-
-- it stays with the owning plugin (agenda/salon) in Phase 2; nothing here.
|
|
18
|
-
--
|
|
19
|
-
-- Idempotent and replay-safe.
|
|
20
|
-
-- ============================================================================
|
|
21
|
-
|
|
22
|
-
CREATE TABLE IF NOT EXISTS public.holidays (
|
|
23
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
24
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
25
|
-
unit_id uuid, -- NULL = tenant-wide (V1 central_holidays)
|
|
26
|
-
date date NOT NULL,
|
|
27
|
-
name text NOT NULL CHECK (btrim(name) <> ''),
|
|
28
|
-
kind text NOT NULL DEFAULT 'company' CHECK (kind IN ('national', 'state', 'city', 'company')),
|
|
29
|
-
recurring boolean NOT NULL DEFAULT false, -- same month/day every year
|
|
30
|
-
description text,
|
|
31
|
-
is_active boolean NOT NULL DEFAULT true,
|
|
32
|
-
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
33
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
34
|
-
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
35
|
-
CONSTRAINT holidays_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE
|
|
36
|
-
);
|
|
37
|
-
CREATE UNIQUE INDEX IF NOT EXISTS holidays_one_per_scope
|
|
38
|
-
ON public.holidays (tenant_id, coalesce(unit_id, '00000000-0000-0000-0000-000000000000'::uuid), date, name);
|
|
39
|
-
CREATE INDEX IF NOT EXISTS holidays_tenant_date ON public.holidays (tenant_id, date) WHERE is_active;
|
|
40
|
-
|
|
41
|
-
SELECT app.scaffold_table('public.holidays', 'config.holiday', 'config', false, 'enforce');
|
|
42
|
-
REVOKE ALL ON public.holidays FROM anon;
|
|
43
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.holidays TO authenticated;
|
|
44
|
-
GRANT ALL ON public.holidays TO service_role;
|
|
45
|
-
|
|
46
|
-
-- The calendar of one unit over a range: tenant-wide + the unit's own rows,
|
|
47
|
-
-- recurring rows projected onto each year of the range (Feb 29 lands on Feb 28
|
|
48
|
-
-- in a common year, as date + interval does). SECURITY INVOKER on purpose: the
|
|
49
|
-
-- caller's RLS decides what she sees (config.read + unit scope).
|
|
50
|
-
CREATE OR REPLACE FUNCTION public.holidays_for_unit(p_unit uuid, p_from date, p_to date)
|
|
51
|
-
RETURNS TABLE (id uuid, occurs_on date, name text, kind text, unit_id uuid, recurring boolean, description text)
|
|
52
|
-
LANGUAGE sql STABLE SET search_path = ''
|
|
53
|
-
AS $$
|
|
54
|
-
WITH years AS (
|
|
55
|
-
SELECT generate_series(extract(year FROM p_from)::int, extract(year FROM p_to)::int) AS y
|
|
56
|
-
),
|
|
57
|
-
scoped AS (
|
|
58
|
-
SELECT h.* FROM public.holidays h
|
|
59
|
-
WHERE h.tenant_id = app.current_tenant_id()
|
|
60
|
-
AND h.is_active
|
|
61
|
-
AND (h.unit_id IS NULL OR h.unit_id = p_unit)
|
|
62
|
-
-- a unit that is not the tenant's yields nothing (NULL = tenant-wide only)
|
|
63
|
-
AND (p_unit IS NULL OR EXISTS (SELECT 1 FROM app.units u WHERE u.tenant_id = h.tenant_id AND u.id = p_unit))
|
|
64
|
-
)
|
|
65
|
-
SELECT s.id, s.date AS occurs_on, s.name, s.kind, s.unit_id, s.recurring, s.description
|
|
66
|
-
FROM scoped s
|
|
67
|
-
WHERE NOT s.recurring AND s.date BETWEEN p_from AND p_to
|
|
68
|
-
UNION ALL
|
|
69
|
-
SELECT s.id, (s.date + make_interval(years => (y.y - extract(year FROM s.date)::int)))::date, s.name, s.kind, s.unit_id, s.recurring, s.description
|
|
70
|
-
FROM scoped s CROSS JOIN years y
|
|
71
|
-
WHERE s.recurring
|
|
72
|
-
AND (s.date + make_interval(years => (y.y - extract(year FROM s.date)::int)))::date BETWEEN p_from AND p_to
|
|
73
|
-
ORDER BY 2, 3;
|
|
74
|
-
$$;
|
|
75
|
-
REVOKE ALL ON FUNCTION public.holidays_for_unit(uuid, date, date) FROM public, anon;
|
|
76
|
-
GRANT EXECUTE ON FUNCTION public.holidays_for_unit(uuid, date, date) TO authenticated, service_role;
|
|
77
|
-
|
|
78
|
-
COMMENT ON TABLE public.holidays IS
|
|
79
|
-
'Tenant/unit holiday calendar (PRD 07 R5). unit_id NULL = tenant-wide (V1 central_holidays); a unit row is that unit''s own (V1 holidays). '
|
|
80
|
-
'holidays_for_unit(unit, from, to) is the calendar the agenda reads.';
|