@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,568 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 063_legacy_tenancy_retired.sql — tenant_members / locations / location_members
|
|
3
|
-
-- stop being sources of truth (#134). They become COMPAT PROJECTIONS of
|
|
4
|
-
-- app.memberships / app.units / app.membership_units.
|
|
5
|
-
--
|
|
6
|
-
-- WHY PROJECTION TABLES AND NOT VIEWS. The retirement was designed as views;
|
|
7
|
-
-- the bench proved that shape impossible in this repo: frozen migrations
|
|
8
|
-
-- (001, 019, 023 …) re-run on every force replay and execute
|
|
9
|
-
-- `ALTER TABLE public.tenant_members ENABLE ROW LEVEL SECURITY`,
|
|
10
|
-
-- `ADD COLUMN IF NOT EXISTS`, `CREATE POLICY … ON public.tenant_members` —
|
|
11
|
-
-- every one of which ERRORS on a view ("This operation is not supported for
|
|
12
|
-
-- views"), and plugin-blog 002 declares a FOREIGN KEY to tenant_members(id)
|
|
13
|
-
-- and 004 declares FKs to locations(id), which a view cannot satisfy. Applied
|
|
14
|
-
-- migrations are never edited, so the names must stay physical tables. What
|
|
15
|
-
-- changes is who writes them:
|
|
16
|
-
--
|
|
17
|
-
-- * app.* is the ONLY source of truth. Triggers on app.memberships,
|
|
18
|
-
-- app.membership_units, app.units and app.roles rebuild the projection rows
|
|
19
|
-
-- (same ids: tenant_members.id = memberships.id, locations.id = units.id).
|
|
20
|
-
-- * A write to a projection table by an API role is FORWARDED to app.* by a
|
|
21
|
-
-- BEFORE trigger (as the invoker, so the app.* RLS policies decide), and
|
|
22
|
-
-- the projection row is materialised by the same statement only when the
|
|
23
|
-
-- forwarded write happened — the admin adapter's UPDATE role / DELETE
|
|
24
|
-
-- member / INSERT location keep working, and RETURNING keeps returning.
|
|
25
|
-
-- tenant_members INSERT → app.memberships INSERT (members.manage; never self)
|
|
26
|
-
-- tenant_members UPDATE → memberships.role_id / person_id (never your own role)
|
|
27
|
-
-- tenant_members DELETE → memberships.active = false (history is kept)
|
|
28
|
-
-- locations INSERT/UPDATE/DELETE → app.units (units.manage); legacy scalar
|
|
29
|
-
-- columns travel in units.metadata
|
|
30
|
-
-- location_members INSERT/DELETE → app.membership_units (members.manage)
|
|
31
|
-
-- * The projection tables' policies are re-authored onto app.* helpers; the
|
|
32
|
-
-- legacy self-insert policy (members_insert_own — the old model's P0) is
|
|
33
|
-
-- dropped. On a force replay 001 re-creates it and this file drops it again.
|
|
34
|
-
--
|
|
35
|
-
-- DATA MIGRATION (idempotent, replay-safe): every legacy row that has no app.*
|
|
36
|
-
-- counterpart is migrated once — tenant_members → memberships (role key mapped
|
|
37
|
-
-- through the 'generic' template: owner/admin/manager/staff/viewer keep their
|
|
38
|
-
-- key, anything else → staff, falling back to viewer, then to the least
|
|
39
|
-
-- privileged non-owner system role; the tenant's roles are seeded from
|
|
40
|
-
-- 'generic' first when it has none), locations → units (the first
|
|
41
|
-
-- is_headquarters row → hq, the rest → branch; a tenant left without an hq
|
|
42
|
-
-- gets its earliest unit promoted or, having none, an hq named after the
|
|
43
|
-
-- tenant), location_members → membership_units (only for users who hold a
|
|
44
|
-
-- membership; owners get all_units). Then app.compat_rebuild() re-derives the
|
|
45
|
-
-- three projections completely.
|
|
46
|
-
--
|
|
47
|
-
-- Custom role grants of the legacy tenant_role_overrides / tenant_roles are
|
|
48
|
-
-- NOT translated here — that is #136's mapping. public.permissions,
|
|
49
|
-
-- role_permissions, tenant_role_overrides and tenant_roles are left in place.
|
|
50
|
-
-- ============================================================================
|
|
51
|
-
|
|
52
|
-
-- ─────────────────────────── helpers ────────────────────────────────────
|
|
53
|
-
-- Two transaction-local switches:
|
|
54
|
-
-- fayz.compat_sync = 'on' while the projection is being (re)written from
|
|
55
|
-
-- app.* — the forwarding triggers let the row through.
|
|
56
|
-
-- fayz.compat_forward = 'on' while a forwarding trigger writes app.* — the
|
|
57
|
-
-- sync triggers stay quiet; the outer statement
|
|
58
|
-
-- materialises the projection row itself.
|
|
59
|
-
CREATE OR REPLACE FUNCTION app.compat_flag(p_name text)
|
|
60
|
-
RETURNS boolean LANGUAGE sql STABLE SET search_path = '' AS $$
|
|
61
|
-
SELECT coalesce(current_setting('fayz.compat_' || p_name, true), '') = 'on';
|
|
62
|
-
$$;
|
|
63
|
-
REVOKE ALL ON FUNCTION app.compat_flag(text) FROM public, anon;
|
|
64
|
-
GRANT EXECUTE ON FUNCTION app.compat_flag(text) TO authenticated, service_role;
|
|
65
|
-
|
|
66
|
-
CREATE OR REPLACE FUNCTION app.compat_role_id(p_tenant uuid, p_key text)
|
|
67
|
-
RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
68
|
-
SELECT r.id FROM app.roles r WHERE r.tenant_id = p_tenant AND r.key = p_key;
|
|
69
|
-
$$;
|
|
70
|
-
REVOKE ALL ON FUNCTION app.compat_role_id(uuid, text) FROM public, anon;
|
|
71
|
-
GRANT EXECUTE ON FUNCTION app.compat_role_id(uuid, text) TO authenticated, service_role;
|
|
72
|
-
|
|
73
|
-
CREATE OR REPLACE FUNCTION app.compat_uuid(p_text text)
|
|
74
|
-
RETURNS uuid LANGUAGE sql IMMUTABLE SET search_path = '' AS $$
|
|
75
|
-
SELECT CASE WHEN p_text ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN p_text::uuid END;
|
|
76
|
-
$$;
|
|
77
|
-
REVOKE ALL ON FUNCTION app.compat_uuid(text) FROM public, anon;
|
|
78
|
-
GRANT EXECUTE ON FUNCTION app.compat_uuid(text) TO authenticated, service_role;
|
|
79
|
-
|
|
80
|
-
-- The legacy role-key mapping used by the data migration only.
|
|
81
|
-
CREATE OR REPLACE FUNCTION app.compat_legacy_role_id(p_tenant uuid, p_key text)
|
|
82
|
-
RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
83
|
-
SELECT coalesce(
|
|
84
|
-
app.compat_role_id(p_tenant, p_key),
|
|
85
|
-
app.compat_role_id(p_tenant, CASE p_key WHEN 'owner' THEN 'owner' WHEN 'admin' THEN 'admin' WHEN 'manager' THEN 'manager'
|
|
86
|
-
WHEN 'staff' THEN 'staff' WHEN 'viewer' THEN 'viewer' ELSE 'staff' END),
|
|
87
|
-
app.compat_role_id(p_tenant, 'viewer'),
|
|
88
|
-
(SELECT r.id FROM app.roles r
|
|
89
|
-
LEFT JOIN app.role_permissions rp ON rp.tenant_id = r.tenant_id AND rp.role_id = r.id
|
|
90
|
-
WHERE r.tenant_id = p_tenant AND r.is_system AND r.key <> 'owner'
|
|
91
|
-
GROUP BY r.id, r.key ORDER BY count(rp.permission), r.key LIMIT 1)
|
|
92
|
-
);
|
|
93
|
-
$$;
|
|
94
|
-
REVOKE ALL ON FUNCTION app.compat_legacy_role_id(uuid, text) FROM public, anon, authenticated;
|
|
95
|
-
|
|
96
|
-
-- ── projection writers (SECURITY DEFINER: they write the legacy tables as the
|
|
97
|
-
-- migration role; the forwarding triggers see fayz.compat_sync = on) ─────
|
|
98
|
-
CREATE OR REPLACE FUNCTION app.compat_sync_membership(p_tenant uuid, p_user uuid)
|
|
99
|
-
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
100
|
-
DECLARE m record;
|
|
101
|
-
BEGIN
|
|
102
|
-
PERFORM set_config('fayz.compat_sync', 'on', true);
|
|
103
|
-
SELECT ms.id, ms.tenant_id, ms.user_id, ms.role_id, ms.person_id, ms.created_at, ms.active,
|
|
104
|
-
r.key AS role_key
|
|
105
|
-
INTO m
|
|
106
|
-
FROM app.memberships ms
|
|
107
|
-
JOIN app.roles r ON r.tenant_id = ms.tenant_id AND r.id = ms.role_id
|
|
108
|
-
WHERE ms.tenant_id = p_tenant AND ms.user_id = p_user;
|
|
109
|
-
|
|
110
|
-
IF m.id IS NULL OR NOT m.active THEN
|
|
111
|
-
DELETE FROM public.location_members lm
|
|
112
|
-
WHERE lm.user_id = p_user
|
|
113
|
-
AND lm.location_id IN (SELECT u.id FROM app.units u WHERE u.tenant_id = p_tenant);
|
|
114
|
-
DELETE FROM public.tenant_members tm WHERE tm.tenant_id = p_tenant AND tm.user_id = p_user;
|
|
115
|
-
ELSE
|
|
116
|
-
-- a stale row for the same (tenant, user) under another id would collide
|
|
117
|
-
DELETE FROM public.tenant_members tm
|
|
118
|
-
WHERE tm.tenant_id = p_tenant AND tm.user_id = p_user AND tm.id <> m.id;
|
|
119
|
-
INSERT INTO public.tenant_members (id, tenant_id, user_id, role, person_id, created_at)
|
|
120
|
-
VALUES (m.id, m.tenant_id, m.user_id, m.role_key,
|
|
121
|
-
-- the legacy unique (tenant_id, person_id) must hold
|
|
122
|
-
CASE WHEN EXISTS (SELECT 1 FROM public.tenant_members x
|
|
123
|
-
WHERE x.tenant_id = m.tenant_id AND x.person_id = m.person_id AND x.id <> m.id)
|
|
124
|
-
THEN NULL ELSE m.person_id END,
|
|
125
|
-
m.created_at)
|
|
126
|
-
ON CONFLICT (id) DO UPDATE
|
|
127
|
-
SET tenant_id = EXCLUDED.tenant_id, user_id = EXCLUDED.user_id, role = EXCLUDED.role,
|
|
128
|
-
person_id = EXCLUDED.person_id, created_at = EXCLUDED.created_at;
|
|
129
|
-
|
|
130
|
-
DELETE FROM public.location_members lm
|
|
131
|
-
WHERE lm.user_id = p_user
|
|
132
|
-
AND lm.location_id IN (SELECT u.id FROM app.units u WHERE u.tenant_id = p_tenant)
|
|
133
|
-
AND NOT EXISTS (SELECT 1 FROM app.membership_units mu
|
|
134
|
-
WHERE mu.tenant_id = p_tenant AND mu.user_id = p_user AND mu.unit_id = lm.location_id);
|
|
135
|
-
INSERT INTO public.location_members (location_id, user_id, role)
|
|
136
|
-
SELECT mu.unit_id, mu.user_id, coalesce(ro.key, m.role_key)
|
|
137
|
-
FROM app.membership_units mu
|
|
138
|
-
LEFT JOIN app.roles ro ON ro.tenant_id = mu.tenant_id AND ro.id = mu.role_override_id
|
|
139
|
-
WHERE mu.tenant_id = p_tenant AND mu.user_id = p_user
|
|
140
|
-
AND EXISTS (SELECT 1 FROM public.locations l WHERE l.id = mu.unit_id)
|
|
141
|
-
ON CONFLICT (location_id, user_id) DO UPDATE SET role = EXCLUDED.role;
|
|
142
|
-
END IF;
|
|
143
|
-
PERFORM set_config('fayz.compat_sync', 'off', true);
|
|
144
|
-
END $$;
|
|
145
|
-
REVOKE ALL ON FUNCTION app.compat_sync_membership(uuid, uuid) FROM public, anon, authenticated;
|
|
146
|
-
|
|
147
|
-
CREATE OR REPLACE FUNCTION app.compat_sync_unit(p_unit uuid)
|
|
148
|
-
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
149
|
-
DECLARE u record;
|
|
150
|
-
BEGIN
|
|
151
|
-
PERFORM set_config('fayz.compat_sync', 'on', true);
|
|
152
|
-
SELECT * INTO u FROM app.units x WHERE x.id = p_unit;
|
|
153
|
-
IF u.id IS NULL THEN
|
|
154
|
-
DELETE FROM public.locations l WHERE l.id = p_unit;
|
|
155
|
-
ELSE
|
|
156
|
-
INSERT INTO public.locations
|
|
157
|
-
(id, tenant_id, kind, name, email, phone, address, city, state, country, postal_code,
|
|
158
|
-
is_headquarters, is_active, tags, notes, metadata, custom_fields, created_by, created_at, updated_at,
|
|
159
|
-
parent_id, code)
|
|
160
|
-
VALUES (u.id, u.tenant_id, coalesce(u.metadata ->> 'legacy_kind', u.kind), u.name,
|
|
161
|
-
u.metadata ->> 'email', u.metadata ->> 'phone', u.metadata ->> 'address',
|
|
162
|
-
u.metadata ->> 'city', u.metadata ->> 'state', coalesce(u.metadata ->> 'country', 'BR'),
|
|
163
|
-
u.metadata ->> 'postal_code',
|
|
164
|
-
u.kind = 'hq', u.active,
|
|
165
|
-
coalesce((SELECT array_agg(t) FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(u.metadata -> 'tags') = 'array' THEN u.metadata -> 'tags' ELSE '[]'::jsonb END) t), '{}'::text[]),
|
|
166
|
-
u.metadata ->> 'notes',
|
|
167
|
-
u.metadata,
|
|
168
|
-
CASE WHEN jsonb_typeof(u.metadata -> 'custom_fields') = 'object' THEN u.metadata -> 'custom_fields' ELSE '{}'::jsonb END,
|
|
169
|
-
app.compat_uuid(u.metadata ->> 'created_by'),
|
|
170
|
-
u.created_at, u.updated_at,
|
|
171
|
-
-- main's 039 gave locations a tree; the projection carries it, or the
|
|
172
|
-
-- units UI reads a flat list and 064b nulls the real tree on replay
|
|
173
|
-
u.parent_unit_id, u.metadata ->> 'code')
|
|
174
|
-
ON CONFLICT (id) DO UPDATE
|
|
175
|
-
SET tenant_id = EXCLUDED.tenant_id, kind = EXCLUDED.kind, name = EXCLUDED.name, email = EXCLUDED.email,
|
|
176
|
-
phone = EXCLUDED.phone, address = EXCLUDED.address, city = EXCLUDED.city, state = EXCLUDED.state,
|
|
177
|
-
country = EXCLUDED.country, postal_code = EXCLUDED.postal_code, is_headquarters = EXCLUDED.is_headquarters,
|
|
178
|
-
is_active = EXCLUDED.is_active, tags = EXCLUDED.tags, notes = EXCLUDED.notes, metadata = EXCLUDED.metadata,
|
|
179
|
-
custom_fields = EXCLUDED.custom_fields, created_by = EXCLUDED.created_by, updated_at = EXCLUDED.updated_at,
|
|
180
|
-
parent_id = EXCLUDED.parent_id, code = EXCLUDED.code;
|
|
181
|
-
-- created_by is stamped by the legacy trigger from auth.uid() when null; keep it null-safe.
|
|
182
|
-
END IF;
|
|
183
|
-
PERFORM set_config('fayz.compat_sync', 'off', true);
|
|
184
|
-
END $$;
|
|
185
|
-
REVOKE ALL ON FUNCTION app.compat_sync_unit(uuid) FROM public, anon, authenticated;
|
|
186
|
-
|
|
187
|
-
-- Full re-derivation of the three projections (used by this migration and
|
|
188
|
-
-- available to reconciliation). Deletes what app.* no longer has, upserts the rest.
|
|
189
|
-
CREATE OR REPLACE FUNCTION app.compat_rebuild()
|
|
190
|
-
RETURNS TABLE (n_tenant_members integer, n_locations integer, n_location_members integer)
|
|
191
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
192
|
-
DECLARE r record;
|
|
193
|
-
BEGIN
|
|
194
|
-
PERFORM set_config('fayz.compat_sync', 'on', true);
|
|
195
|
-
DELETE FROM public.location_members lm
|
|
196
|
-
WHERE NOT EXISTS (SELECT 1 FROM app.membership_units mu
|
|
197
|
-
JOIN app.memberships m ON m.tenant_id = mu.tenant_id AND m.user_id = mu.user_id AND m.active
|
|
198
|
-
WHERE mu.unit_id = lm.location_id AND mu.user_id = lm.user_id);
|
|
199
|
-
DELETE FROM public.tenant_members tm
|
|
200
|
-
WHERE NOT EXISTS (SELECT 1 FROM app.memberships m WHERE m.id = tm.id AND m.active);
|
|
201
|
-
DELETE FROM public.locations l
|
|
202
|
-
WHERE NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = l.id);
|
|
203
|
-
PERFORM set_config('fayz.compat_sync', 'off', true);
|
|
204
|
-
FOR r IN SELECT u.id FROM app.units u LOOP
|
|
205
|
-
PERFORM app.compat_sync_unit(r.id);
|
|
206
|
-
END LOOP;
|
|
207
|
-
FOR r IN SELECT m.tenant_id, m.user_id FROM app.memberships m LOOP
|
|
208
|
-
PERFORM app.compat_sync_membership(r.tenant_id, r.user_id);
|
|
209
|
-
END LOOP;
|
|
210
|
-
SELECT count(*)::integer INTO n_tenant_members FROM public.tenant_members;
|
|
211
|
-
SELECT count(*)::integer INTO n_locations FROM public.locations;
|
|
212
|
-
SELECT count(*)::integer INTO n_location_members FROM public.location_members;
|
|
213
|
-
RETURN NEXT;
|
|
214
|
-
END $$;
|
|
215
|
-
REVOKE ALL ON FUNCTION app.compat_rebuild() FROM public, anon, authenticated;
|
|
216
|
-
GRANT EXECUTE ON FUNCTION app.compat_rebuild() TO service_role;
|
|
217
|
-
|
|
218
|
-
-- ── sync triggers on app.* ──────────────────────────────────────────────
|
|
219
|
-
CREATE OR REPLACE FUNCTION app.trg_memberships_compat()
|
|
220
|
-
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
221
|
-
BEGIN
|
|
222
|
-
IF app.compat_flag('forward') THEN RETURN NULL; END IF;
|
|
223
|
-
IF TG_OP = 'DELETE' THEN
|
|
224
|
-
PERFORM app.compat_sync_membership(OLD.tenant_id, OLD.user_id);
|
|
225
|
-
ELSE
|
|
226
|
-
IF TG_OP = 'UPDATE' AND (OLD.tenant_id <> NEW.tenant_id OR OLD.user_id <> NEW.user_id) THEN
|
|
227
|
-
PERFORM app.compat_sync_membership(OLD.tenant_id, OLD.user_id);
|
|
228
|
-
END IF;
|
|
229
|
-
PERFORM app.compat_sync_membership(NEW.tenant_id, NEW.user_id);
|
|
230
|
-
END IF;
|
|
231
|
-
RETURN NULL;
|
|
232
|
-
END $$;
|
|
233
|
-
DROP TRIGGER IF EXISTS memberships_compat ON app.memberships;
|
|
234
|
-
CREATE TRIGGER memberships_compat
|
|
235
|
-
AFTER INSERT OR UPDATE OR DELETE ON app.memberships
|
|
236
|
-
FOR EACH ROW EXECUTE FUNCTION app.trg_memberships_compat();
|
|
237
|
-
|
|
238
|
-
CREATE OR REPLACE FUNCTION app.trg_membership_units_compat()
|
|
239
|
-
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
240
|
-
BEGIN
|
|
241
|
-
IF app.compat_flag('forward') THEN RETURN NULL; END IF;
|
|
242
|
-
IF TG_OP IN ('DELETE', 'UPDATE') THEN
|
|
243
|
-
PERFORM app.compat_sync_membership(OLD.tenant_id, OLD.user_id);
|
|
244
|
-
END IF;
|
|
245
|
-
IF TG_OP IN ('INSERT', 'UPDATE') AND (TG_OP = 'INSERT' OR OLD.tenant_id <> NEW.tenant_id OR OLD.user_id <> NEW.user_id) THEN
|
|
246
|
-
PERFORM app.compat_sync_membership(NEW.tenant_id, NEW.user_id);
|
|
247
|
-
END IF;
|
|
248
|
-
RETURN NULL;
|
|
249
|
-
END $$;
|
|
250
|
-
DROP TRIGGER IF EXISTS membership_units_compat ON app.membership_units;
|
|
251
|
-
CREATE TRIGGER membership_units_compat
|
|
252
|
-
AFTER INSERT OR UPDATE OR DELETE ON app.membership_units
|
|
253
|
-
FOR EACH ROW EXECUTE FUNCTION app.trg_membership_units_compat();
|
|
254
|
-
|
|
255
|
-
CREATE OR REPLACE FUNCTION app.trg_units_compat()
|
|
256
|
-
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
257
|
-
BEGIN
|
|
258
|
-
IF app.compat_flag('forward') THEN RETURN NULL; END IF;
|
|
259
|
-
IF TG_OP = 'DELETE' THEN
|
|
260
|
-
PERFORM app.compat_sync_unit(OLD.id);
|
|
261
|
-
ELSE
|
|
262
|
-
PERFORM app.compat_sync_unit(NEW.id);
|
|
263
|
-
END IF;
|
|
264
|
-
RETURN NULL;
|
|
265
|
-
END $$;
|
|
266
|
-
DROP TRIGGER IF EXISTS units_compat ON app.units;
|
|
267
|
-
CREATE TRIGGER units_compat
|
|
268
|
-
AFTER INSERT OR UPDATE OR DELETE ON app.units
|
|
269
|
-
FOR EACH ROW EXECUTE FUNCTION app.trg_units_compat();
|
|
270
|
-
|
|
271
|
-
-- a role key rename must show in tenant_members.role / location_members.role
|
|
272
|
-
CREATE OR REPLACE FUNCTION app.trg_roles_compat()
|
|
273
|
-
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
274
|
-
DECLARE r record;
|
|
275
|
-
BEGIN
|
|
276
|
-
IF OLD.key IS DISTINCT FROM NEW.key THEN
|
|
277
|
-
FOR r IN SELECT m.tenant_id, m.user_id FROM app.memberships m WHERE m.tenant_id = NEW.tenant_id AND m.role_id = NEW.id
|
|
278
|
-
UNION SELECT mu.tenant_id, mu.user_id FROM app.membership_units mu WHERE mu.tenant_id = NEW.tenant_id AND mu.role_override_id = NEW.id
|
|
279
|
-
LOOP
|
|
280
|
-
PERFORM app.compat_sync_membership(r.tenant_id, r.user_id);
|
|
281
|
-
END LOOP;
|
|
282
|
-
END IF;
|
|
283
|
-
RETURN NULL;
|
|
284
|
-
END $$;
|
|
285
|
-
DROP TRIGGER IF EXISTS roles_compat ON app.roles;
|
|
286
|
-
CREATE TRIGGER roles_compat
|
|
287
|
-
AFTER UPDATE ON app.roles
|
|
288
|
-
FOR EACH ROW EXECUTE FUNCTION app.trg_roles_compat();
|
|
289
|
-
|
|
290
|
-
-- ── forwarding triggers on the projection tables (SECURITY INVOKER: app.* RLS
|
|
291
|
-
-- decides; a write that app.* filters to zero rows changes nothing) ──────
|
|
292
|
-
CREATE OR REPLACE FUNCTION public.trg_tenant_members_forward()
|
|
293
|
-
RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $$
|
|
294
|
-
DECLARE v_role uuid; v_n integer;
|
|
295
|
-
BEGIN
|
|
296
|
-
IF app.compat_flag('sync') THEN
|
|
297
|
-
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
298
|
-
END IF;
|
|
299
|
-
PERFORM set_config('fayz.compat_forward', 'on', true);
|
|
300
|
-
IF TG_OP = 'INSERT' THEN
|
|
301
|
-
v_role := app.compat_role_id(NEW.tenant_id, NEW.role);
|
|
302
|
-
IF v_role IS NULL THEN
|
|
303
|
-
RAISE EXCEPTION 'tenant_members: unknown role key % in this tenant', NEW.role USING ERRCODE = '22023';
|
|
304
|
-
END IF;
|
|
305
|
-
NEW.id := coalesce(NEW.id, gen_random_uuid());
|
|
306
|
-
INSERT INTO app.memberships (tenant_id, user_id, id, role_id, all_units, active, person_id, created_at)
|
|
307
|
-
VALUES (NEW.tenant_id, NEW.user_id, NEW.id, v_role, false, true, NEW.person_id, coalesce(NEW.created_at, now()));
|
|
308
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
309
|
-
RETURN NEW;
|
|
310
|
-
ELSIF TG_OP = 'UPDATE' THEN
|
|
311
|
-
IF NEW.role IS DISTINCT FROM OLD.role AND OLD.user_id = auth.uid() THEN
|
|
312
|
-
RAISE EXCEPTION 'tenant_members: you cannot change your own role' USING ERRCODE = '42501';
|
|
313
|
-
END IF;
|
|
314
|
-
IF NEW.tenant_id <> OLD.tenant_id OR NEW.user_id <> OLD.user_id OR NEW.id <> OLD.id THEN
|
|
315
|
-
RAISE EXCEPTION 'tenant_members: tenant, user and id are immutable' USING ERRCODE = '22023';
|
|
316
|
-
END IF;
|
|
317
|
-
v_role := app.compat_role_id(NEW.tenant_id, NEW.role);
|
|
318
|
-
IF v_role IS NULL THEN
|
|
319
|
-
RAISE EXCEPTION 'tenant_members: unknown role key % in this tenant', NEW.role USING ERRCODE = '22023';
|
|
320
|
-
END IF;
|
|
321
|
-
UPDATE app.memberships m SET role_id = v_role, person_id = NEW.person_id WHERE m.id = OLD.id;
|
|
322
|
-
GET DIAGNOSTICS v_n = ROW_COUNT;
|
|
323
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
324
|
-
IF v_n = 0 THEN RETURN NULL; END IF;
|
|
325
|
-
RETURN NEW;
|
|
326
|
-
ELSE
|
|
327
|
-
-- removing a member = deactivating the membership (history and FKs survive)
|
|
328
|
-
UPDATE app.memberships m SET active = false WHERE m.id = OLD.id AND m.active;
|
|
329
|
-
GET DIAGNOSTICS v_n = ROW_COUNT;
|
|
330
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
331
|
-
IF v_n = 0 THEN RETURN NULL; END IF;
|
|
332
|
-
RETURN OLD;
|
|
333
|
-
END IF;
|
|
334
|
-
END $$;
|
|
335
|
-
REVOKE ALL ON FUNCTION public.trg_tenant_members_forward() FROM public, anon;
|
|
336
|
-
DROP TRIGGER IF EXISTS tenant_members_forward ON public.tenant_members;
|
|
337
|
-
CREATE TRIGGER tenant_members_forward BEFORE INSERT OR UPDATE OR DELETE ON public.tenant_members
|
|
338
|
-
FOR EACH ROW EXECUTE FUNCTION public.trg_tenant_members_forward();
|
|
339
|
-
|
|
340
|
-
CREATE OR REPLACE FUNCTION public.trg_locations_forward()
|
|
341
|
-
RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $$
|
|
342
|
-
DECLARE v_kind text; v_meta jsonb; v_n integer;
|
|
343
|
-
BEGIN
|
|
344
|
-
IF app.compat_flag('sync') THEN
|
|
345
|
-
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
346
|
-
END IF;
|
|
347
|
-
PERFORM set_config('fayz.compat_forward', 'on', true);
|
|
348
|
-
IF TG_OP = 'DELETE' THEN
|
|
349
|
-
DELETE FROM app.units u WHERE u.id = OLD.id;
|
|
350
|
-
GET DIAGNOSTICS v_n = ROW_COUNT;
|
|
351
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
352
|
-
IF v_n = 0 THEN RETURN NULL; END IF;
|
|
353
|
-
RETURN OLD;
|
|
354
|
-
END IF;
|
|
355
|
-
|
|
356
|
-
-- the legacy scalar columns live in units.metadata
|
|
357
|
-
v_meta := (coalesce(NEW.metadata, '{}'::jsonb)
|
|
358
|
-
- ARRAY['email', 'phone', 'address', 'city', 'state', 'country', 'postal_code', 'tags', 'notes', 'custom_fields', 'legacy_kind'])
|
|
359
|
-
|| jsonb_strip_nulls(jsonb_build_object(
|
|
360
|
-
'email', NEW.email, 'phone', NEW.phone, 'address', NEW.address, 'city', NEW.city,
|
|
361
|
-
'state', NEW.state, 'country', NEW.country, 'postal_code', NEW.postal_code,
|
|
362
|
-
'tags', CASE WHEN NEW.tags IS NULL OR cardinality(NEW.tags) = 0 THEN NULL ELSE to_jsonb(NEW.tags) END,
|
|
363
|
-
'notes', NEW.notes,
|
|
364
|
-
'custom_fields', CASE WHEN NEW.custom_fields IS NULL OR NEW.custom_fields = '{}'::jsonb THEN NULL ELSE NEW.custom_fields END,
|
|
365
|
-
'created_by', coalesce(NEW.created_by, auth.uid(), app.compat_uuid(NEW.metadata ->> 'created_by')),
|
|
366
|
-
'legacy_kind', CASE WHEN NEW.kind IN ('hq', 'branch') THEN NULL ELSE NEW.kind END));
|
|
367
|
-
IF TG_OP = 'INSERT' THEN
|
|
368
|
-
NEW.id := coalesce(NEW.id, gen_random_uuid());
|
|
369
|
-
v_kind := CASE WHEN coalesce(NEW.is_headquarters, false) OR NEW.kind = 'hq' THEN 'hq' ELSE 'branch' END;
|
|
370
|
-
IF v_kind = 'hq' AND EXISTS (SELECT 1 FROM app.units u WHERE u.tenant_id = NEW.tenant_id AND u.kind = 'hq') THEN
|
|
371
|
-
RAISE EXCEPTION 'locations: the tenant already has a headquarters unit' USING ERRCODE = '23505';
|
|
372
|
-
END IF;
|
|
373
|
-
INSERT INTO app.units (tenant_id, id, kind, name, active, metadata, created_at, updated_at)
|
|
374
|
-
VALUES (NEW.tenant_id, NEW.id, v_kind, NEW.name, coalesce(NEW.is_active, true), v_meta,
|
|
375
|
-
coalesce(NEW.created_at, now()), coalesce(NEW.updated_at, now()));
|
|
376
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
377
|
-
NEW.kind := v_kind; NEW.is_headquarters := (v_kind = 'hq'); NEW.is_active := coalesce(NEW.is_active, true);
|
|
378
|
-
NEW.metadata := v_meta; NEW.country := coalesce(NEW.country, 'BR');
|
|
379
|
-
RETURN NEW;
|
|
380
|
-
END IF;
|
|
381
|
-
|
|
382
|
-
IF NEW.tenant_id <> OLD.tenant_id OR NEW.id <> OLD.id THEN
|
|
383
|
-
RAISE EXCEPTION 'locations: tenant and id are immutable' USING ERRCODE = '22023';
|
|
384
|
-
END IF;
|
|
385
|
-
v_kind := CASE WHEN coalesce(NEW.is_headquarters, false) OR NEW.kind = 'hq' THEN 'hq' ELSE 'branch' END;
|
|
386
|
-
UPDATE app.units u
|
|
387
|
-
SET name = NEW.name, kind = v_kind, active = coalesce(NEW.is_active, true), metadata = v_meta
|
|
388
|
-
WHERE u.id = OLD.id;
|
|
389
|
-
GET DIAGNOSTICS v_n = ROW_COUNT;
|
|
390
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
391
|
-
IF v_n = 0 THEN RETURN NULL; END IF;
|
|
392
|
-
NEW.kind := v_kind; NEW.is_headquarters := (v_kind = 'hq'); NEW.metadata := v_meta;
|
|
393
|
-
RETURN NEW;
|
|
394
|
-
END $$;
|
|
395
|
-
REVOKE ALL ON FUNCTION public.trg_locations_forward() FROM public, anon;
|
|
396
|
-
DROP TRIGGER IF EXISTS locations_forward ON public.locations;
|
|
397
|
-
CREATE TRIGGER locations_forward BEFORE INSERT OR UPDATE OR DELETE ON public.locations
|
|
398
|
-
FOR EACH ROW EXECUTE FUNCTION public.trg_locations_forward();
|
|
399
|
-
|
|
400
|
-
CREATE OR REPLACE FUNCTION public.trg_location_members_forward()
|
|
401
|
-
RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $$
|
|
402
|
-
DECLARE v_tenant uuid; v_n integer;
|
|
403
|
-
BEGIN
|
|
404
|
-
IF app.compat_flag('sync') THEN
|
|
405
|
-
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
|
406
|
-
END IF;
|
|
407
|
-
PERFORM set_config('fayz.compat_forward', 'on', true);
|
|
408
|
-
IF TG_OP = 'INSERT' THEN
|
|
409
|
-
SELECT u.tenant_id INTO v_tenant FROM app.units u WHERE u.id = NEW.location_id;
|
|
410
|
-
IF v_tenant IS NULL THEN
|
|
411
|
-
RAISE EXCEPTION 'location_members: unknown location %', NEW.location_id USING ERRCODE = '22023';
|
|
412
|
-
END IF;
|
|
413
|
-
INSERT INTO app.membership_units (tenant_id, user_id, unit_id) VALUES (v_tenant, NEW.user_id, NEW.location_id);
|
|
414
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
415
|
-
NEW.id := coalesce(NEW.id, gen_random_uuid());
|
|
416
|
-
RETURN NEW;
|
|
417
|
-
ELSIF TG_OP = 'UPDATE' THEN
|
|
418
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
419
|
-
RAISE EXCEPTION 'location_members is a compat projection: rows are added and removed, not edited' USING ERRCODE = '0A000';
|
|
420
|
-
ELSE
|
|
421
|
-
DELETE FROM app.membership_units mu WHERE mu.unit_id = OLD.location_id AND mu.user_id = OLD.user_id;
|
|
422
|
-
GET DIAGNOSTICS v_n = ROW_COUNT;
|
|
423
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
424
|
-
IF v_n = 0 THEN RETURN NULL; END IF;
|
|
425
|
-
RETURN OLD;
|
|
426
|
-
END IF;
|
|
427
|
-
END $$;
|
|
428
|
-
REVOKE ALL ON FUNCTION public.trg_location_members_forward() FROM public, anon;
|
|
429
|
-
DROP TRIGGER IF EXISTS location_members_forward ON public.location_members;
|
|
430
|
-
CREATE TRIGGER location_members_forward BEFORE INSERT OR UPDATE OR DELETE ON public.location_members
|
|
431
|
-
FOR EACH ROW EXECUTE FUNCTION public.trg_location_members_forward();
|
|
432
|
-
|
|
433
|
-
-- ─────────────────────── data migration (idempotent) ─────────────────────
|
|
434
|
-
DO $migrate$
|
|
435
|
-
DECLARE
|
|
436
|
-
t record;
|
|
437
|
-
l record;
|
|
438
|
-
v_has_hq boolean;
|
|
439
|
-
v_kind text;
|
|
440
|
-
v_first uuid;
|
|
441
|
-
BEGIN
|
|
442
|
-
PERFORM set_config('fayz.compat_sync', 'off', true);
|
|
443
|
-
PERFORM set_config('fayz.compat_forward', 'off', true);
|
|
444
|
-
|
|
445
|
-
-- 1. roles for tenants that hold legacy memberships but no roles yet
|
|
446
|
-
FOR t IN
|
|
447
|
-
SELECT DISTINCT tm.tenant_id AS id
|
|
448
|
-
FROM public.tenant_members tm
|
|
449
|
-
WHERE NOT EXISTS (SELECT 1 FROM app.roles r WHERE r.tenant_id = tm.tenant_id)
|
|
450
|
-
LOOP
|
|
451
|
-
PERFORM app.seed_role_template(t.id, 'generic');
|
|
452
|
-
UPDATE public.tenants SET role_template = coalesce(role_template, 'generic') WHERE id = t.id;
|
|
453
|
-
END LOOP;
|
|
454
|
-
|
|
455
|
-
-- 2. tenant_members → memberships (ids preserved; owner ⇒ all_units)
|
|
456
|
-
FOR t IN
|
|
457
|
-
SELECT tm.*
|
|
458
|
-
FROM public.tenant_members tm
|
|
459
|
-
WHERE NOT EXISTS (SELECT 1 FROM app.memberships m WHERE m.tenant_id = tm.tenant_id AND m.user_id = tm.user_id)
|
|
460
|
-
AND EXISTS (SELECT 1 FROM auth.users u WHERE u.id = tm.user_id)
|
|
461
|
-
ORDER BY tm.created_at, tm.id
|
|
462
|
-
LOOP
|
|
463
|
-
DECLARE v_role uuid := app.compat_legacy_role_id(t.tenant_id, t.role);
|
|
464
|
-
BEGIN
|
|
465
|
-
IF v_role IS NULL THEN
|
|
466
|
-
RAISE EXCEPTION 'legacy tenancy: tenant % has no role to receive tenant_members.role = %', t.tenant_id, t.role;
|
|
467
|
-
END IF;
|
|
468
|
-
INSERT INTO app.memberships (tenant_id, user_id, id, role_id, all_units, active, person_id, created_at)
|
|
469
|
-
VALUES (t.tenant_id, t.user_id,
|
|
470
|
-
CASE WHEN EXISTS (SELECT 1 FROM app.memberships m WHERE m.id = t.id) THEN gen_random_uuid() ELSE t.id END,
|
|
471
|
-
v_role, t.role = 'owner', true, t.person_id, t.created_at)
|
|
472
|
-
ON CONFLICT (tenant_id, user_id) DO NOTHING;
|
|
473
|
-
END;
|
|
474
|
-
END LOOP;
|
|
475
|
-
|
|
476
|
-
-- 3. locations → units (same ids); one hq per tenant
|
|
477
|
-
FOR t IN SELECT DISTINCT loc.tenant_id AS id FROM public.locations loc
|
|
478
|
-
WHERE NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = loc.id)
|
|
479
|
-
LOOP
|
|
480
|
-
v_has_hq := EXISTS (SELECT 1 FROM app.units u WHERE u.tenant_id = t.id AND u.kind = 'hq');
|
|
481
|
-
FOR l IN SELECT * FROM public.locations x
|
|
482
|
-
WHERE x.tenant_id = t.id AND NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = x.id)
|
|
483
|
-
ORDER BY coalesce(x.is_headquarters, false) DESC, x.created_at, x.id
|
|
484
|
-
LOOP
|
|
485
|
-
v_kind := CASE WHEN NOT v_has_hq AND coalesce(l.is_headquarters, false) THEN 'hq' ELSE 'branch' END;
|
|
486
|
-
IF v_kind = 'hq' THEN v_has_hq := true; END IF;
|
|
487
|
-
INSERT INTO app.units (tenant_id, id, kind, name, active, metadata, created_at, updated_at)
|
|
488
|
-
VALUES (t.id, l.id, v_kind, l.name, coalesce(l.is_active, true),
|
|
489
|
-
coalesce(l.metadata, '{}'::jsonb) || jsonb_strip_nulls(jsonb_build_object(
|
|
490
|
-
'email', l.email, 'phone', l.phone, 'address', l.address, 'city', l.city, 'state', l.state,
|
|
491
|
-
'country', l.country, 'postal_code', l.postal_code,
|
|
492
|
-
'tags', CASE WHEN l.tags IS NULL OR cardinality(l.tags) = 0 THEN NULL ELSE to_jsonb(l.tags) END,
|
|
493
|
-
'notes', l.notes,
|
|
494
|
-
'custom_fields', CASE WHEN l.custom_fields = '{}'::jsonb THEN NULL ELSE l.custom_fields END,
|
|
495
|
-
'created_by', l.created_by,
|
|
496
|
-
'legacy_kind', CASE WHEN l.kind IN ('hq', 'branch') THEN NULL ELSE l.kind END)),
|
|
497
|
-
coalesce(l.created_at, now()), coalesce(l.updated_at, now()))
|
|
498
|
-
ON CONFLICT (id) DO NOTHING;
|
|
499
|
-
END LOOP;
|
|
500
|
-
END LOOP;
|
|
501
|
-
|
|
502
|
-
-- every tenant has a headquarters: promote the earliest unit, or create one
|
|
503
|
-
FOR t IN SELECT x.id, x.name FROM public.tenants x
|
|
504
|
-
WHERE NOT EXISTS (SELECT 1 FROM app.units u WHERE u.tenant_id = x.id AND u.kind = 'hq')
|
|
505
|
-
LOOP
|
|
506
|
-
SELECT u.id INTO v_first FROM app.units u WHERE u.tenant_id = t.id ORDER BY u.created_at, u.id LIMIT 1;
|
|
507
|
-
IF v_first IS NOT NULL THEN
|
|
508
|
-
UPDATE app.units SET kind = 'hq' WHERE id = v_first;
|
|
509
|
-
ELSE
|
|
510
|
-
INSERT INTO app.units (tenant_id, kind, name, slug, active)
|
|
511
|
-
VALUES (t.id, 'hq', coalesce(nullif(btrim(t.name), ''), 'Sede'), 'hq', true);
|
|
512
|
-
END IF;
|
|
513
|
-
END LOOP;
|
|
514
|
-
|
|
515
|
-
-- 4. location_members → membership_units (only members of the tenant)
|
|
516
|
-
INSERT INTO app.membership_units (tenant_id, user_id, unit_id)
|
|
517
|
-
SELECT u.tenant_id, lm.user_id, lm.location_id
|
|
518
|
-
FROM public.location_members lm
|
|
519
|
-
JOIN app.units u ON u.id = lm.location_id
|
|
520
|
-
JOIN app.memberships m ON m.tenant_id = u.tenant_id AND m.user_id = lm.user_id
|
|
521
|
-
ON CONFLICT DO NOTHING;
|
|
522
|
-
|
|
523
|
-
-- 5. re-derive the projections completely and re-check the share table
|
|
524
|
-
PERFORM app.compat_rebuild();
|
|
525
|
-
PERFORM app.recompute_all_user_unit_access('migration');
|
|
526
|
-
END $migrate$;
|
|
527
|
-
|
|
528
|
-
-- ─────────────────── policies on the projection tables ───────────────────
|
|
529
|
-
-- Reads through the seam; writes need the platform permission key AND are then
|
|
530
|
-
-- forwarded to app.* whose own policies decide again. The legacy self-insert
|
|
531
|
-
-- (members_insert_own) is gone.
|
|
532
|
-
DROP POLICY IF EXISTS "members_insert_own" ON public.tenant_members;
|
|
533
|
-
DROP POLICY IF EXISTS "members_select" ON public.tenant_members;
|
|
534
|
-
CREATE POLICY "members_select" ON public.tenant_members FOR SELECT TO authenticated
|
|
535
|
-
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
536
|
-
DROP POLICY IF EXISTS "members_insert" ON public.tenant_members;
|
|
537
|
-
CREATE POLICY "members_insert" ON public.tenant_members FOR INSERT TO authenticated
|
|
538
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')) AND user_id <> (select auth.uid()));
|
|
539
|
-
DROP POLICY IF EXISTS "members_update" ON public.tenant_members;
|
|
540
|
-
CREATE POLICY "members_update" ON public.tenant_members FOR UPDATE TO authenticated
|
|
541
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')))
|
|
542
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')));
|
|
543
|
-
DROP POLICY IF EXISTS "members_delete" ON public.tenant_members;
|
|
544
|
-
CREATE POLICY "members_delete" ON public.tenant_members FOR DELETE TO authenticated
|
|
545
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('members.manage')) AND user_id <> (select auth.uid()));
|
|
546
|
-
|
|
547
|
-
DROP POLICY IF EXISTS "locations_select" ON public.locations;
|
|
548
|
-
CREATE POLICY "locations_select" ON public.locations FOR SELECT TO authenticated
|
|
549
|
-
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
550
|
-
DROP POLICY IF EXISTS "locations_manage" ON public.locations;
|
|
551
|
-
CREATE POLICY "locations_manage" ON public.locations FOR ALL TO authenticated
|
|
552
|
-
USING (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('units.manage')))
|
|
553
|
-
WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (select app.has_permission('units.manage')));
|
|
554
|
-
|
|
555
|
-
DROP POLICY IF EXISTS "loc_members_select" ON public.location_members;
|
|
556
|
-
CREATE POLICY "loc_members_select" ON public.location_members FOR SELECT TO authenticated
|
|
557
|
-
USING (location_id IN (SELECT l.id FROM public.locations l WHERE l.tenant_id IN (SELECT public.user_tenant_ids())));
|
|
558
|
-
DROP POLICY IF EXISTS "loc_members_manage" ON public.location_members;
|
|
559
|
-
CREATE POLICY "loc_members_manage" ON public.location_members FOR ALL TO authenticated
|
|
560
|
-
USING (location_id IN (SELECT l.id FROM public.locations l WHERE l.tenant_id = (select app.current_tenant_id())) AND (select app.has_permission('members.manage')))
|
|
561
|
-
WITH CHECK (location_id IN (SELECT l.id FROM public.locations l WHERE l.tenant_id = (select app.current_tenant_id())) AND (select app.has_permission('members.manage')));
|
|
562
|
-
|
|
563
|
-
COMMENT ON TABLE public.tenant_members IS
|
|
564
|
-
'COMPAT PROJECTION of app.memberships (#134, 2026-08-18): same ids, role = app.roles.key, active memberships only. Derived by triggers; direct writes are forwarded to app.memberships under its RLS. Read it if you must; write app.* or use the RPCs. Removal: Phase 1.';
|
|
565
|
-
COMMENT ON TABLE public.locations IS
|
|
566
|
-
'COMPAT PROJECTION of app.units (#134, 2026-08-18): same ids; is_headquarters = (kind = hq); contact/address columns come from units.metadata. Direct writes are forwarded to app.units under its RLS. Removal: Phase 1.';
|
|
567
|
-
COMMENT ON TABLE public.location_members IS
|
|
568
|
-
'COMPAT PROJECTION of app.membership_units (#134, 2026-08-18): (location_id, user_id) = (unit_id, user_id) of active members; role = effective role key. Direct inserts/deletes are forwarded. Removal: Phase 1.';
|