@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.
Files changed (187) hide show
  1. package/canonical-schema.json +106 -23
  2. package/dist/index.cjs +4 -4
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/schema/spine.d.ts +42 -37
  9. package/dist/schema/spine.d.ts.map +1 -1
  10. package/migrations/000_baseline.sql +29667 -0
  11. package/migrations/001_the_tenant_has_apps.sql +314 -0
  12. package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
  13. package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
  14. package/migrations/004_the_projections_get_a_successor.sql +312 -0
  15. package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
  16. package/migrations/006_the_helpers_read_the_source.sql +153 -0
  17. package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
  18. package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
  19. package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
  20. package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
  21. package/migrations/011_leadcontrol_joins_the_family.sql +116 -0
  22. package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
  23. package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
  24. package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
  25. package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
  26. package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
  27. package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
  28. package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
  29. package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
  30. package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
  31. package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
  32. package/package.json +1 -1
  33. package/migrations/0000_legacy_quarantine.sql +0 -86
  34. package/migrations/0000b_straggler_sweep.sql +0 -37
  35. package/migrations/000_core_v1_convert.sql +0 -343
  36. package/migrations/001_core.sql +0 -372
  37. package/migrations/002_rls_user_tenant_ids.sql +0 -64
  38. package/migrations/003_invitations.sql +0 -17
  39. package/migrations/004_archetypes.sql +0 -343
  40. package/migrations/005_locations_archetype.sql +0 -18
  41. package/migrations/006_archetype_rls.sql +0 -74
  42. package/migrations/007_archetype_grants.sql +0 -4
  43. package/migrations/008_grants.sql +0 -5
  44. package/migrations/009_anon_hardening.sql +0 -19
  45. package/migrations/010_migration_ledger.sql +0 -46
  46. package/migrations/011_anon_write_revoke.sql +0 -12
  47. package/migrations/012_rename_migration_ledger.sql +0 -61
  48. package/migrations/013_sequences_rls.sql +0 -22
  49. package/migrations/014_plan_entitlements.sql +0 -28
  50. package/migrations/015_agent_guard.sql +0 -108
  51. package/migrations/016_agent_guard_actor.sql +0 -102
  52. package/migrations/017_core_addresses.sql +0 -115
  53. package/migrations/018_global_search.sql +0 -482
  54. package/migrations/019_tenant_members_person_id.sql +0 -20
  55. package/migrations/020_fayz_projects.sql +0 -63
  56. package/migrations/021_tenant_roles.sql +0 -74
  57. package/migrations/022_invite_acceptance.sql +0 -91
  58. package/migrations/023_team_visibility.sql +0 -75
  59. package/migrations/024_analytics_engine.sql +0 -317
  60. package/migrations/025_created_by.sql +0 -75
  61. package/migrations/026_audit_trail.sql +0 -73
  62. package/migrations/027_domain_events.sql +0 -266
  63. package/migrations/028_tenant_scoped_token.sql +0 -127
  64. package/migrations/029_connections.sql +0 -186
  65. package/migrations/030_effect_idempotency.sql +0 -159
  66. package/migrations/031_sync_run_message.sql +0 -39
  67. package/migrations/032_connection_secrets.sql +0 -227
  68. package/migrations/033_sync_schedule.sql +0 -651
  69. package/migrations/034_custom_fields.sql +0 -55
  70. package/migrations/035_field_registry.sql +0 -148
  71. package/migrations/036_analytics_run_batch.sql +0 -84
  72. package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
  73. package/migrations/038_onboarding_responses.sql +0 -103
  74. package/migrations/039_unit_tree.sql +0 -270
  75. package/migrations/040_resource_grants.sql +0 -474
  76. package/migrations/041_scoped_columns.sql +0 -192
  77. package/migrations/042_unit_scope_policies.sql +0 -145
  78. package/migrations/043_view_invoker.sql +0 -90
  79. package/migrations/044_unit_member_facts.sql +0 -47
  80. package/migrations/045_unit_entry.sql +0 -236
  81. package/migrations/046_membership_visible_to_members.sql +0 -85
  82. package/migrations/047_tasks.sql +0 -266
  83. package/migrations/048_every_login_is_a_person.sql +0 -190
  84. package/migrations/049_bookable_people.sql +0 -126
  85. package/migrations/050_authz_core.sql +0 -230
  86. package/migrations/051_authz_helpers.sql +0 -330
  87. package/migrations/052_authz_fanout.sql +0 -253
  88. package/migrations/053_authz_catalog_templates.sql +0 -402
  89. package/migrations/060_tenancy_seam.sql +0 -132
  90. package/migrations/061_create_tenant.sql +0 -125
  91. package/migrations/062_invitations.sql +0 -329
  92. package/migrations/063_legacy_tenancy_retired.sql +0 -568
  93. package/migrations/064_agent_guard_authz.sql +0 -118
  94. package/migrations/064b_supersede_unit_scope.sql +0 -205
  95. package/migrations/065_scaffold_sweep.sql +0 -139
  96. package/migrations/066_shadow_report.sql +0 -122
  97. package/migrations/070_permission_catalog.sql +0 -205
  98. package/migrations/071_v1_permission_map.sql +0 -266
  99. package/migrations/072_role_rpcs.sql +0 -126
  100. package/migrations/073_legacy_rbac_retire.sql +0 -471
  101. package/migrations/075_company_legal_entities.sql +0 -169
  102. package/migrations/076_tenant_settings.sql +0 -251
  103. package/migrations/080_people_roles.sql +0 -269
  104. package/migrations/081_people_identity.sql +0 -271
  105. package/migrations/082_people_membership_link.sql +0 -142
  106. package/migrations/083_people_data_migration.sql +0 -75
  107. package/migrations/084_people_registrations_tokens.sql +0 -282
  108. package/migrations/085_catalog_products.sql +0 -333
  109. package/migrations/086_catalog_pricing.sql +0 -194
  110. package/migrations/087_catalog_packages.sql +0 -134
  111. package/migrations/090_orders_venda.sql +0 -166
  112. package/migrations/091_orders_scaffold.sql +0 -92
  113. package/migrations/095_config_field_rules.sql +0 -261
  114. package/migrations/096_audit_append_only.sql +0 -256
  115. package/migrations/097_sequences_authorized.sql +0 -111
  116. package/migrations/098_holidays.sql +0 -80
  117. package/migrations/099_access_grants.sql +0 -293
  118. package/migrations/100_migration_schema.sql +0 -217
  119. package/migrations/101_migration_allowlist.sql +0 -202
  120. package/migrations/102_migration_rpcs.sql +0 -933
  121. package/migrations/103_migration_reconcile_ponr.sql +0 -132
  122. package/migrations/105_policies_to_authenticated.sql +0 -71
  123. package/migrations/106_function_search_path.sql +0 -55
  124. package/migrations/107_core_table_prefix.sql +0 -361
  125. package/migrations/108_authz_write_guards.sql +0 -149
  126. package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
  127. package/migrations/110_people_kind_vocabulary.sql +0 -120
  128. package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
  129. package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
  130. package/migrations/112_money_precision_standard.sql +0 -132
  131. package/migrations/113_addresses_units_companies.sql +0 -114
  132. package/migrations/114_migrations_ledger_immutable.sql +0 -65
  133. package/migrations/115_registration_honours_field_rules.sql +0 -172
  134. package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
  135. package/migrations/117_authz_reconciliation_schedule.sql +0 -82
  136. package/migrations/118_migration_catalog_writers.sql +0 -450
  137. package/migrations/119_migration_config_writers.sql +0 -193
  138. package/migrations/120_migration_facet_writer.sql +0 -139
  139. package/migrations/121_migration_financial_writers.sql +0 -294
  140. package/migrations/122_migration_inventory_writers.sql +0 -245
  141. package/migrations/123_migration_order_item_writer.sql +0 -144
  142. package/migrations/124_migration_ledger_writers.sql +0 -170
  143. package/migrations/125_migration_authz_people_writers.sql +0 -205
  144. package/migrations/126_owner_scoping_is_configuration.sql +0 -247
  145. package/migrations/127_scaffold_audit_hook.sql +0 -98
  146. package/migrations/128_shadow_tables_enforced.sql +0 -93
  147. package/migrations/129_order_kind_terminal_states.sql +0 -57
  148. package/migrations/130_every_login_is_a_person.sql +0 -196
  149. package/migrations/131_migration_invoice_split.sql +0 -127
  150. package/migrations/132_audit_prune_scheduled.sql +0 -28
  151. package/migrations/133_unit_is_a_session_property.sql +0 -155
  152. package/migrations/134_field_rules_on_read.sql +0 -341
  153. package/migrations/135_distribution_polarity.sql +0 -158
  154. package/migrations/136_unit_id_means_locality.sql +0 -358
  155. package/migrations/137_vendor_named_spine_objects.sql +0 -126
  156. package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
  157. package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
  158. package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
  159. package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
  160. package/migrations/142_a_person_has_addresses.sql +0 -212
  161. package/migrations/143_a_professional_has_a_rate.sql +0 -75
  162. package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
  163. package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
  164. package/migrations/146_products_stock_has_one_writer.sql +0 -67
  165. package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
  166. package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
  167. package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
  168. package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
  169. package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
  170. package/migrations/152_a_file_is_an_archetype.sql +0 -222
  171. package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
  172. package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
  173. package/migrations/155_a_tenant_can_leave.sql +0 -271
  174. package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
  175. package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
  176. package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
  177. package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
  178. package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
  179. package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
  180. package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
  181. package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
  182. package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
  183. package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
  184. package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
  185. package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
  186. package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
  187. package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
@@ -1,65 +0,0 @@
1
- -- 114_migrations_ledger_immutable.sql
2
- --
3
- -- The migration ledger stops being append-only by politeness. Fix for #145.
4
- --
5
- -- `public._migrations` is what `fayz db apply` reads to know what has run and to
6
- -- detect checksum drift. It had RLS enabled and **zero policies**, which sounds
7
- -- locked — but `service_role` holds BYPASSRLS, so as far as the API was concerned
8
- -- the ledger was an ordinary writable table: `UPDATE 1`, `DELETE 1`, both
9
- -- succeeded. A client could quietly tell the applier that a migration it never
10
- -- ran has already run, or that one it did run has not.
11
- --
12
- -- WHAT "APPEND-ONLY" ACTUALLY MEANS HERE, because the criterion overstates it and
13
- -- the chain itself proves the overstatement: `107_core_table_prefix.sql:359`
14
- -- deliberately DELETEs a ledger row so the next apply re-runs a file that had no
15
- -- fix-forward form. Forgetting a row is a legitimate migration act. A rule that
16
- -- forbade it would be a rule the chain breaks on line one.
17
- --
18
- -- So the enforceable property is not "nothing is ever deleted". It is:
19
- --
20
- -- 1. no API role touches the ledger at all — not anon, not authenticated, and
21
- -- not service_role, which is the one that could;
22
- -- 2. a row's IDENTITY never changes. `checksum`, `applied_at`, `applied_by` and
23
- -- `plugin_version` move when a file is legitimately re-applied after an
24
- -- edit — that is how drift detection works — but the (plugin_id, file_name)
25
- -- a row stands for is fixed for its life. Rewriting that is not a
26
- -- correction, it is a forgery: it makes one file's history describe another.
27
- --
28
- -- The applier connects through the Supabase Management API, which runs as
29
- -- `postgres`, so it is unaffected. This is a boundary between the applier and the
30
- -- API, not a freeze.
31
- --
32
- -- Replay-safe: CREATE OR REPLACE + DROP/CREATE TRIGGER, no data touched.
33
-
34
- CREATE OR REPLACE FUNCTION app.migrations_ledger_guard()
35
- RETURNS trigger LANGUAGE plpgsql SECURITY INVOKER SET search_path = '' AS $$
36
- BEGIN
37
- -- 1. the ledger belongs to the applier, not to the API surface
38
- IF current_user IN ('anon', 'authenticated', 'service_role') THEN
39
- RAISE EXCEPTION
40
- 'public._migrations is the migration applier''s ledger — % may not write it', current_user
41
- USING ERRCODE = '42501',
42
- HINT = 'Apply migrations with `fayz db apply`, which owns this table and detects checksum drift.';
43
- END IF;
44
-
45
- -- 2. a row's identity is fixed for its life; its provenance is not
46
- IF TG_OP = 'UPDATE' AND (NEW.id <> OLD.id
47
- OR NEW.plugin_id <> OLD.plugin_id
48
- OR NEW.file_name <> OLD.file_name) THEN
49
- RAISE EXCEPTION
50
- 'public._migrations: the (plugin_id, file_name) of ledger row % cannot change — that would make one file''s history describe another', OLD.id
51
- USING ERRCODE = '42501',
52
- HINT = 'Re-applying an edited file updates its checksum; it does not move the row to a different file.';
53
- END IF;
54
-
55
- RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
56
- END $$;
57
- REVOKE ALL ON FUNCTION app.migrations_ledger_guard() FROM public, anon, authenticated;
58
-
59
- DROP TRIGGER IF EXISTS migrations_ledger_guard ON public._migrations;
60
- CREATE TRIGGER migrations_ledger_guard
61
- BEFORE INSERT OR UPDATE OR DELETE ON public._migrations
62
- FOR EACH ROW EXECUTE FUNCTION app.migrations_ledger_guard();
63
-
64
- COMMENT ON TABLE public._migrations IS
65
- 'The migration applier''s ledger (#145). Client-immutable: no API role may write it (104). A row''s (plugin_id, file_name) is fixed for its life; checksum/applied_at/applied_by move when a file is legitimately re-applied after an edit, which is how drift detection works. A migration may forget a row (097 does, deliberately) — that is an applier act, not a client one.';
@@ -1,172 +0,0 @@
1
- -- 115_registration_honours_field_rules.sql
2
- --
3
- -- The validation API gets its first caller. Fix for #144.
4
- --
5
- -- #144 shipped FieldRules with three states per tenant, per unit and per context,
6
- -- a resolver, and `field_rules_check`, which raises in pt-BR with a HINT. All of
7
- -- it works. Nothing called it. `095_config_field_rules.sql:16` even names the
8
- -- caller it expected -- "the check-in RPC calls field_rules_check('check_in',
9
- -- 'person', ...)" -- and that RPC does not exist; plugin-agenda ships fifteen
10
- -- migrations and no check-in.
11
- --
12
- -- So the acceptance sentence ("a required FieldRule blocks the RPC in pt-BR") had
13
- -- no RPC to block, and a mechanism nobody invokes is indistinguishable from one
14
- -- that does not work.
15
- --
16
- -- `person_registrations_process` is the natural host and was the clearest
17
- -- illustration of the gap: it read `payload ->> 'name'` and, when that was empty,
18
- -- wrote the Person anyway under the name 'Sem nome'. A tenant that had declared
19
- -- name REQUIRED for pre-registration got a row called "Sem nome".
20
- --
21
- -- The check goes in before the INSERT, on the `pre_registration` context, scoped
22
- -- to the registration's own unit so a branch's stricter rule applies. A tenant
23
- -- with no rule for that context is unaffected -- the check returns {} and the
24
- -- call is a no-op -- so this changes behaviour only where somebody asked for it.
25
- --
26
- -- The other half of #144's acceptance (a hidden field never reaching the payload)
27
- -- is a read-side concern and is not addressed here; it stays on the issue.
28
- --
29
- -- Replay-safe: CREATE OR REPLACE of one function, no DDL, no data touched.
30
-
31
- CREATE OR REPLACE FUNCTION public.person_registrations_process(p_id uuid, p_action text DEFAULT 'process', p_person_id uuid DEFAULT NULL)
32
- RETURNS jsonb
33
- LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
34
- AS $$
35
- DECLARE
36
- v_tenant uuid := app.current_tenant_id();
37
- v_actor uuid := auth.uid();
38
- r public.person_registrations%ROWTYPE;
39
- v_email text;
40
- v_phone text;
41
- v_by_doc uuid[];
42
- v_by_sig uuid[];
43
- v_person uuid;
44
- v_q uuid;
45
- v_status text;
46
- BEGIN
47
- IF v_tenant IS NULL THEN RAISE EXCEPTION 'no active tenant for this session' USING ERRCODE = '42501'; END IF;
48
- IF p_action NOT IN ('process', 'create', 'merge', 'reject') THEN
49
- RAISE EXCEPTION 'person_registrations_process: unknown action %', p_action USING ERRCODE = '22023';
50
- END IF;
51
- SELECT * INTO r FROM public.person_registrations WHERE tenant_id = v_tenant AND id = p_id FOR UPDATE;
52
- IF NOT FOUND THEN RAISE EXCEPTION 'person_registrations_process: registration not found in this tenant' USING ERRCODE = 'P0002'; END IF;
53
- PERFORM public.people_assert_scope(CASE WHEN p_action = 'merge' THEN 'people.manage' ELSE 'people.create' END, r.unit_id);
54
-
55
- IF p_action = 'process' AND r.status <> 'pending' THEN
56
- RAISE EXCEPTION 'person_registrations_process: only a pending registration can be processed (status %)', r.status USING ERRCODE = 'P0001';
57
- END IF;
58
- IF p_action IN ('create', 'merge') AND r.status <> 'quarantined' THEN
59
- RAISE EXCEPTION 'person_registrations_process: % resolves a quarantined registration (status %)', p_action, r.status USING ERRCODE = 'P0001';
60
- END IF;
61
- IF p_action = 'reject' AND r.status NOT IN ('pending', 'quarantined') THEN
62
- RAISE EXCEPTION 'person_registrations_process: registration already %', r.status USING ERRCODE = 'P0001';
63
- END IF;
64
-
65
- IF p_action = 'reject' THEN
66
- UPDATE public.person_registrations SET status = 'rejected', processed_at = now(), processed_by = v_actor WHERE id = r.id;
67
- RETURN jsonb_build_object('registration_id', r.id, 'status', 'rejected', 'person_id', NULL);
68
- END IF;
69
-
70
- IF p_action = 'merge' THEN
71
- IF p_person_id IS NULL THEN RAISE EXCEPTION 'person_registrations_process: merge needs p_person_id' USING ERRCODE = '22023'; END IF;
72
- SELECT p.id INTO v_person FROM public.people p WHERE p.tenant_id = v_tenant AND p.id = p_person_id AND p.merged_into_id IS NULL;
73
- IF v_person IS NULL THEN RAISE EXCEPTION 'person_registrations_process: person not found in this tenant' USING ERRCODE = 'P0002'; END IF;
74
- v_status := 'merged';
75
- ELSIF p_action = 'create' THEN
76
- v_person := NULL;
77
- v_status := 'created';
78
- ELSE
79
- -- 'process': the dedup rules
80
- v_email := nullif(lower(btrim(r.payload ->> 'email')), '');
81
- v_phone := nullif(regexp_replace(coalesce(r.payload ->> 'phone', ''), '\D', '', 'g'), '');
82
- IF r.document_number IS NOT NULL THEN
83
- SELECT coalesce(array_agg(p.id ORDER BY p.created_at), '{}') INTO v_by_doc
84
- FROM public.people p
85
- WHERE p.tenant_id = v_tenant AND p.merged_into_id IS NULL AND p.document_number = r.document_number;
86
- ELSE
87
- v_by_doc := '{}';
88
- END IF;
89
- IF cardinality(v_by_doc) = 1 THEN
90
- v_person := v_by_doc[1];
91
- v_status := 'merged';
92
- ELSIF cardinality(v_by_doc) > 1 THEN
93
- v_status := 'quarantined';
94
- v_by_sig := v_by_doc;
95
- ELSE
96
- -- no document match: a same e-mail / same phone person is a possible
97
- -- duplicate the CPF cannot confirm — a human decides
98
- SELECT coalesce(array_agg(p.id ORDER BY p.created_at), '{}') INTO v_by_sig
99
- FROM public.people p
100
- WHERE p.tenant_id = v_tenant AND p.merged_into_id IS NULL
101
- AND ((v_email IS NOT NULL AND lower(p.email) = v_email)
102
- OR (v_phone IS NOT NULL AND regexp_replace(coalesce(p.phone, ''), '\D', '', 'g') = v_phone));
103
- IF cardinality(v_by_sig) > 0 THEN
104
- v_status := 'quarantined';
105
- ELSE
106
- v_status := 'created';
107
- END IF;
108
- END IF;
109
-
110
- IF v_status = 'quarantined' THEN
111
- INSERT INTO public.person_identity_quarantine (tenant_id, document_number, person_ids, reason, context)
112
- VALUES (v_tenant, r.document_number, v_by_sig,
113
- CASE WHEN cardinality(v_by_doc) > 1 THEN 'registration: more than one person carries this document'
114
- ELSE 'registration: e-mail/phone match without a document match' END,
115
- jsonb_build_object('source', 'person_registrations_process', 'registration_id', r.id))
116
- ON CONFLICT (tenant_id, document_number) WHERE resolved_at IS NULL AND document_number IS NOT NULL DO UPDATE
117
- SET person_ids = EXCLUDED.person_ids, context = public.person_identity_quarantine.context || EXCLUDED.context
118
- RETURNING id INTO v_q;
119
- UPDATE public.person_registrations SET status = 'quarantined', quarantine_id = v_q, processed_at = now(), processed_by = v_actor WHERE id = r.id;
120
- RETURN jsonb_build_object('registration_id', r.id, 'status', 'quarantined', 'person_id', NULL, 'quarantine_id', v_q, 'candidates', to_jsonb(v_by_sig));
121
- END IF;
122
- END IF;
123
-
124
- IF v_status = 'created' THEN
125
- -- The tenant's FieldRules for this context decide whether this payload may
126
- -- become a Person at all. Until now field_rules_check had ZERO callers: the
127
- -- rules resolved, raised in pt-BR, and nothing ever asked them — while this
128
- -- very statement defaulted a missing name to 'Sem nome' and wrote the row
129
- -- (#144). A tenant with no pre_registration rule is unaffected: the check
130
- -- returns {} and this is a no-op.
131
- PERFORM public.field_rules_check('pre_registration', 'person', r.payload, r.unit_id);
132
-
133
- -- kind = 'customer' goes through the compat trigger: the customer role is
134
- -- created as primary. Vertical fields stay in metadata.registration.
135
- INSERT INTO public.people (tenant_id, unit_id, kind, name, email, phone, document_number, date_of_birth,
136
- address, city, state, country, postal_code, notes, metadata, created_by)
137
- VALUES (v_tenant, r.unit_id, 'customer',
138
- coalesce(nullif(btrim(r.payload ->> 'name'), ''), 'Sem nome'),
139
- nullif(btrim(r.payload ->> 'email'), ''), nullif(btrim(r.payload ->> 'phone'), ''), r.document_number,
140
- CASE WHEN (r.payload ->> 'date_of_birth') ~ '^\d{4}-\d{2}-\d{2}$' THEN (r.payload ->> 'date_of_birth')::date END,
141
- nullif(r.payload ->> 'address', ''), nullif(r.payload ->> 'city', ''), nullif(r.payload ->> 'state', ''),
142
- coalesce(nullif(r.payload ->> 'country', ''), 'BR'), nullif(r.payload ->> 'postal_code', ''), nullif(r.payload ->> 'notes', ''),
143
- jsonb_build_object('registration', jsonb_build_object('id', r.id, 'unit_id', r.unit_id, 'payload', r.payload)),
144
- v_actor)
145
- RETURNING id INTO v_person;
146
- ELSE
147
- -- merged: the person exists; she is (also) a customer now. Nothing else is
148
- -- overwritten — the registration keeps the payload for a human to compare.
149
- IF NOT EXISTS (SELECT 1 FROM public.person_roles x WHERE x.tenant_id = v_tenant AND x.person_id = v_person AND x.role = 'customer'
150
- AND public.person_role_active(x.valid_from, x.valid_to)) THEN
151
- INSERT INTO public.person_roles (tenant_id, person_id, role, unit_id, is_primary, metadata)
152
- VALUES (v_tenant, v_person, 'customer', r.unit_id,
153
- NOT EXISTS (SELECT 1 FROM public.person_roles x WHERE x.tenant_id = v_tenant AND x.person_id = v_person AND x.is_primary),
154
- jsonb_build_object('source', 'person_registrations_process', 'registration_id', r.id))
155
- ON CONFLICT DO NOTHING;
156
- UPDATE public.person_roles x SET valid_to = NULL, valid_from = least(x.valid_from, current_date)
157
- WHERE x.tenant_id = v_tenant AND x.person_id = v_person AND x.role = 'customer' AND NOT public.person_role_active(x.valid_from, x.valid_to)
158
- AND NOT EXISTS (SELECT 1 FROM public.person_roles y WHERE y.tenant_id = v_tenant AND y.person_id = v_person AND y.role = 'customer' AND public.person_role_active(y.valid_from, y.valid_to));
159
- END IF;
160
- END IF;
161
-
162
- UPDATE public.person_registrations
163
- SET status = v_status, matched_person_id = v_person, processed_at = now(), processed_by = v_actor
164
- WHERE id = r.id;
165
- IF r.quarantine_id IS NOT NULL THEN
166
- UPDATE public.person_identity_quarantine SET resolved_at = now(), resolved_by = v_actor, resolution = v_status || ':' || v_person::text
167
- WHERE id = r.quarantine_id AND resolved_at IS NULL;
168
- END IF;
169
- INSERT INTO public.audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)
170
- VALUES (v_tenant, v_actor, 'people.registration.' || v_status, 'people', v_person::text, jsonb_build_object('registration_id', r.id, 'action', p_action));
171
- RETURN jsonb_build_object('registration_id', r.id, 'status', v_status, 'person_id', v_person);
172
- END $$;
@@ -1,72 +0,0 @@
1
- -- 116_field_rules_check_needs_a_tenant.sql
2
- --
3
- -- A validator that cannot see the rules must not approve. Fix for #147.
4
- --
5
- -- `field_rules_check` resolves the tenant from `app.current_tenant_id()`, which
6
- -- reads the JWT. With no claim there is no tenant, so `field_rules_validate`
7
- -- finds no rules, so nothing is ever "missing", so the function returns the
8
- -- payload untouched -- a clean pass on a payload containing none of the required
9
- -- fields.
10
- --
11
- -- That is not hypothetical. `11-verification-suite.md` told the QA Agent to run
12
- -- its invariants as `service_role`, and US-PLT-ADM-07 claims this function raises
13
- -- 23514 for a missing required field. Run exactly as documented it returned `{}`,
14
- -- so **the agent got a silent pass on the very violation it was checking for**.
15
- --
16
- -- The doc is corrected in the same commit to say "as the persona". This is the
17
- -- half that does not depend on anyone reading it: with no tenant, the function
18
- -- refuses instead of approving. Failing closed is the only safe direction for a
19
- -- validator -- a false "valid" is worse than an error, because it is invisible.
20
- --
21
- -- ONE TRAP WORTH RECORDING, because it cost a bench run and will catch the next
22
- -- person. 097 renamed the eight core tables AND rewrote every function body that
23
- -- referenced them, with pg_get_functiondef + regexp_replace. So the live body of
24
- -- this function says `entity_fields`, while 085 -- the file it is defined in --
25
- -- still says `plg_entity_fields`, because applied files are never edited. Lifting
26
- -- a function from its original file after 097 therefore RESURRECTS the old table
27
- -- name and the function breaks at its first call:
28
- --
29
- -- ERROR: relation "public.plg_entity_fields" does not exist
30
- --
31
- -- The reference below is the post-097 name on purpose. scripts/check-post-097-names.mjs
32
- -- now fails CI on any migration after 097 that mentions one of the eight old names.
33
- --
34
- -- Replay-safe: CREATE OR REPLACE of one function, no DDL, no data touched.
35
-
36
- CREATE OR REPLACE FUNCTION public.field_rules_check(p_context text, p_reference text, p_payload jsonb, p_unit uuid DEFAULT NULL)
37
- RETURNS jsonb
38
- LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = ''
39
- AS $$
40
- DECLARE
41
- v_res jsonb := public.field_rules_validate(p_context, p_reference, p_payload, p_unit);
42
- v_tenant uuid := app.current_tenant_id();
43
- v_labels text;
44
- BEGIN
45
- -- Without a tenant there are no rules, so nothing is ever "missing" and this
46
- -- returns the payload untouched — a caller checking for a required field gets
47
- -- a clean pass on a payload that has none of them. That is what happened to the
48
- -- QA Agent hooks (#147): the doc told them to run as `service_role`, which
49
- -- carries no JWT, so ADM-07 silently returned {} where it claimed to raise.
50
- -- A validator that cannot see the rules must say so, not approve.
51
- IF v_tenant IS NULL THEN
52
- RAISE EXCEPTION 'field_rules_check: no active tenant — the rules cannot be resolved, so nothing can be validated'
53
- USING ERRCODE = '42501',
54
- HINT = 'Call this as the persona whose form is being validated. service_role carries no tenant claim.';
55
- END IF;
56
- IF (v_res ->> 'ok')::boolean THEN
57
- RETURN v_res -> 'payload';
58
- END IF;
59
- SELECT string_agg(coalesce(
60
- (SELECT f.label FROM public.entity_fields f
61
- WHERE f.reference = p_reference AND f.key = k.key
62
- AND (f.tenant_id IS NULL OR f.tenant_id = v_tenant)
63
- AND f.archived_at IS NULL AND nullif(btrim(f.label), '') IS NOT NULL
64
- ORDER BY f.tenant_id NULLS LAST LIMIT 1),
65
- k.key), ', ' ORDER BY k.ord)
66
- INTO v_labels
67
- FROM jsonb_array_elements_text(v_res -> 'missing') WITH ORDINALITY AS k(key, ord);
68
- RAISE EXCEPTION 'Campo obrigatório: %', v_labels
69
- USING ERRCODE = '23514',
70
- DETAIL = format('field_rules_check: required fields missing for %s/%s: %s', p_context, p_reference, (v_res -> 'missing')::text),
71
- HINT = 'Preencha os campos obrigatórios antes de continuar.';
72
- END $$;
@@ -1,82 +0,0 @@
1
- -- 117_authz_reconciliation_schedule.sql
2
- --
3
- -- The reconciliation alert stops being a view nobody queries. Fix for #133.
4
- --
5
- -- #133's criterion is "`recompute_user_unit_access` + diff-log; runs after authz
6
- -- migrations and on demand; **alert when diff ≠ 0 outside migrations**". What
7
- -- shipped for the last clause was `public.v_authz_reconciliation_alerts` — a
8
- -- correct view over the diff log, and nothing that ever reads it. Nothing was
9
- -- scheduled, no channel existed, and `source = 'scheduled'` appeared nowhere in
10
- -- the repo, so on a built database the log held only rows the migrations and the
11
- -- tests wrote. An alert nobody runs is a query, and the difference matters most
12
- -- in the case the alert exists for: `user_unit_access` is a PRECOMPUTED
13
- -- projection, so a drift there is not a slow page — it is a person seeing units
14
- -- they should not, or losing units they should have, silently, until someone
15
- -- happens to look.
16
- --
17
- -- Three parts, and the third is the one that makes it an alert:
18
- --
19
- -- 1. `app.authz_reconcile_all()` — recompute every tenant with source
20
- -- 'scheduled', returning what it changed. It fixes as it goes: the
21
- -- projection is derived, so the recompute IS the repair, and leaving a
22
- -- known-wrong access row in place to preserve evidence would be choosing
23
- -- forensics over the person who cannot see her own unit. The diff log keeps
24
- -- the evidence.
25
- -- 2. a pg_cron job at 03:41, following the idiom of 033 (guarded on pg_cron
26
- -- being present, idempotent by job name, off-peak and off-the-hour so it
27
- -- does not stampede with every other platform's 03:00).
28
- -- 3. `app.authz_alert_reconciliation()` — RAISES WARNING per drifting tenant,
29
- -- so a non-zero diff reaches the Postgres log and anything tailing it,
30
- -- instead of sitting in a table. This is the honest ceiling for what a
31
- -- migration can wire on its own: routing to Slack or a pager needs an
32
- -- endpoint the platform owns, and inventing one here would be a channel
33
- -- nobody is on.
34
- --
35
- -- Replay-safe: CREATE OR REPLACE, cron.schedule upserts by job name.
36
-
37
- CREATE OR REPLACE FUNCTION app.authz_reconcile_all()
38
- RETURNS TABLE (tenant_id uuid, added integer, removed integer)
39
- LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
40
- DECLARE r record; v record;
41
- BEGIN
42
- FOR r IN SELECT t.id FROM public.tenants t ORDER BY t.id LOOP
43
- SELECT * INTO v FROM app.recompute_user_unit_access(r.id, 'scheduled');
44
- IF v.added <> 0 OR v.removed <> 0 THEN
45
- tenant_id := r.id; added := v.added; removed := v.removed;
46
- RETURN NEXT;
47
- END IF;
48
- END LOOP;
49
- END $$;
50
- REVOKE ALL ON FUNCTION app.authz_reconcile_all() FROM public, anon, authenticated;
51
-
52
- -- The alert itself. A WARNING is what a migration can reach on its own; the
53
- -- platform decides where the log goes.
54
- CREATE OR REPLACE FUNCTION app.authz_alert_reconciliation()
55
- RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
56
- DECLARE r record; v_n integer := 0;
57
- BEGIN
58
- FOR r IN SELECT * FROM app.authz_reconcile_all() LOOP
59
- v_n := v_n + 1;
60
- RAISE WARNING 'authz drift: tenant % had user_unit_access repaired (+% / -%). A precomputed access row was wrong until now — see public.v_authz_reconciliation_alerts',
61
- r.tenant_id, r.added, r.removed;
62
- END LOOP;
63
- IF v_n = 0 THEN
64
- RAISE NOTICE 'authz reconciliation: no drift';
65
- END IF;
66
- RETURN v_n;
67
- END $$;
68
- REVOKE ALL ON FUNCTION app.authz_alert_reconciliation() FROM public, anon, authenticated;
69
-
70
- DO $do$
71
- BEGIN
72
- IF to_regprocedure('cron.schedule(text, text, text)') IS NULL THEN
73
- RAISE WARNING 'pg_cron is not available: authz reconciliation is NOT scheduled on this pool — a drift in user_unit_access would go unnoticed';
74
- RETURN;
75
- END IF;
76
- -- 03:41: off-peak, and off the hour so it does not land with every other
77
- -- platform's 03:00 job. Daily, because the fan-out triggers keep the projection
78
- -- right in the same transaction — this is the net that catches what a trigger
79
- -- could not see (a restore, a manual fix, a bug), not the primary mechanism.
80
- PERFORM cron.schedule('fayz_authz_reconcile', '41 3 * * *',
81
- $cmd$SELECT app.authz_alert_reconciliation()$cmd$);
82
- END $do$;