@fayz-ai/db 0.12.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 +132 -0
- 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 +3 -2
- 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 -36
- 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 -81
- 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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 004_the_projections_get_a_successor — what replaces locations,
|
|
3
|
+
-- location_members and tenant_members (#214 / #352).
|
|
4
|
+
--
|
|
5
|
+
-- #134 moved tenancy into app.* and left three COMPAT PROJECTIONS in public so
|
|
6
|
+
-- the screens kept answering. They are real tables kept in step by triggers,
|
|
7
|
+
-- and their comments have said "Removal: Phase 1" since 2026-08-18.
|
|
8
|
+
--
|
|
9
|
+
-- They could not simply be dropped, for a reason worth writing down: PostgREST
|
|
10
|
+
-- cannot reach the `app` schema, so "read app.* directly" was never available
|
|
11
|
+
-- to a caller. The projection was not laziness — it was the only door.
|
|
12
|
+
--
|
|
13
|
+
-- A security_invoker VIEW is the door that should have been built. RLS on
|
|
14
|
+
-- app.* still decides every row (the policies are already there:
|
|
15
|
+
-- membership_units_write wants members.manage), the shape stays what the 41
|
|
16
|
+
-- callers already expect, and nothing is a second copy of anything.
|
|
17
|
+
--
|
|
18
|
+
-- So the three views below are DROP-IN: same column names, same derivation as
|
|
19
|
+
-- the trigger that maintained the table. app.compat_sync_unit is where the
|
|
20
|
+
-- unit mapping comes from, expression for expression, which is why this reads
|
|
21
|
+
-- like it was copied — it was, deliberately, so the two cannot disagree.
|
|
22
|
+
--
|
|
23
|
+
-- TWO THINGS A VIEW CANNOT DO, and what happens instead:
|
|
24
|
+
--
|
|
25
|
+
-- · app.membership_units has no `id` column; the projection synthesised one.
|
|
26
|
+
-- Here it is derived from (tenant, user, unit), which is the key that was
|
|
27
|
+
-- already unique — so it is stable across reads rather than a fresh uuid
|
|
28
|
+
-- each time, which is what a React key needs it to be.
|
|
29
|
+
-- · PostgREST's upsert needs a unique CONSTRAINT, and a view has none. The
|
|
30
|
+
-- two call sites that upserted a unit membership get an RPC instead, which
|
|
31
|
+
-- is better anyway: assigning somebody to a unit is a verb, and it now
|
|
32
|
+
-- checks members.manage in one place instead of trusting the policy to
|
|
33
|
+
-- catch a malformed write.
|
|
34
|
+
-- ============================================================================
|
|
35
|
+
|
|
36
|
+
CREATE OR REPLACE VIEW public.v_team_members WITH (security_invoker='true') AS
|
|
37
|
+
SELECT m.id, m.tenant_id, m.user_id, r.key AS role, m.created_at, m.person_id
|
|
38
|
+
FROM app.memberships m
|
|
39
|
+
LEFT JOIN app.roles r ON r.id = m.role_id
|
|
40
|
+
WHERE m.active;
|
|
41
|
+
|
|
42
|
+
COMMENT ON VIEW public.v_team_members IS
|
|
43
|
+
'Active memberships with the role KEY (#214). Drop-in successor to public.tenant_members: same columns, security_invoker, so app.memberships RLS decides the rows.';
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
46
|
+
SELECT u.id,
|
|
47
|
+
u.tenant_id,
|
|
48
|
+
coalesce(u.metadata ->> 'legacy_kind', u.kind) AS kind,
|
|
49
|
+
u.name,
|
|
50
|
+
u.metadata ->> 'email' AS email,
|
|
51
|
+
u.metadata ->> 'phone' AS phone,
|
|
52
|
+
u.metadata ->> 'address' AS address,
|
|
53
|
+
u.metadata ->> 'city' AS city,
|
|
54
|
+
u.metadata ->> 'state' AS state,
|
|
55
|
+
coalesce(u.metadata ->> 'country', 'BR') AS country,
|
|
56
|
+
u.metadata ->> 'postal_code' AS postal_code,
|
|
57
|
+
(u.kind = 'hq') AS is_headquarters,
|
|
58
|
+
u.active AS is_active,
|
|
59
|
+
coalesce((SELECT array_agg(t) FROM jsonb_array_elements_text(
|
|
60
|
+
CASE WHEN jsonb_typeof(u.metadata -> 'tags') = 'array'
|
|
61
|
+
THEN u.metadata -> 'tags' ELSE '[]'::jsonb END) t), '{}'::text[]) AS tags,
|
|
62
|
+
u.metadata ->> 'notes' AS notes,
|
|
63
|
+
u.metadata,
|
|
64
|
+
CASE WHEN jsonb_typeof(u.metadata -> 'custom_fields') = 'object'
|
|
65
|
+
THEN u.metadata -> 'custom_fields' ELSE '{}'::jsonb END AS custom_fields,
|
|
66
|
+
-- app.compat_uuid inlined: it is a guarded cast, and calling it here
|
|
67
|
+
-- would make every reader of this view need EXECUTE on an app.* function
|
|
68
|
+
-- as well as SELECT on the view. A view should not carry a second grant.
|
|
69
|
+
CASE WHEN u.metadata ->> 'created_by' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
70
|
+
THEN (u.metadata ->> 'created_by')::uuid END AS created_by,
|
|
71
|
+
u.created_at,
|
|
72
|
+
u.updated_at,
|
|
73
|
+
u.parent_unit_id AS parent_id,
|
|
74
|
+
u.metadata ->> 'code' AS code,
|
|
75
|
+
-- Guarded casts: metadata is merchant-writable, and a view that throws
|
|
76
|
+
-- on one bad string takes the whole units screen down with it.
|
|
77
|
+
CASE WHEN u.metadata ->> 'latitude' ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
78
|
+
THEN (u.metadata ->> 'latitude')::numeric END AS latitude,
|
|
79
|
+
CASE WHEN u.metadata ->> 'longitude' ~ '^-?[0-9]+(\.[0-9]+)?$'
|
|
80
|
+
THEN (u.metadata ->> 'longitude')::numeric END AS longitude
|
|
81
|
+
FROM app.units u;
|
|
82
|
+
|
|
83
|
+
COMMENT ON VIEW public.v_units IS
|
|
84
|
+
'Units in the shape the units screen reads (#214). Drop-in successor to public.locations, deriving the same columns from app.units + metadata as app.compat_sync_unit did.';
|
|
85
|
+
|
|
86
|
+
CREATE OR REPLACE VIEW public.v_unit_members WITH (security_invoker='true') AS
|
|
87
|
+
SELECT md5(mu.tenant_id::text || ':' || mu.user_id::text || ':' || mu.unit_id::text)::uuid AS id,
|
|
88
|
+
mu.unit_id AS location_id,
|
|
89
|
+
mu.user_id,
|
|
90
|
+
coalesce(ro.key, r.key) AS role,
|
|
91
|
+
mu.tenant_id,
|
|
92
|
+
mu.created_at
|
|
93
|
+
FROM app.membership_units mu
|
|
94
|
+
LEFT JOIN app.roles ro ON ro.id = mu.role_override_id
|
|
95
|
+
LEFT JOIN app.memberships m ON m.tenant_id = mu.tenant_id AND m.user_id = mu.user_id
|
|
96
|
+
LEFT JOIN app.roles r ON r.id = m.role_id;
|
|
97
|
+
|
|
98
|
+
COMMENT ON VIEW public.v_unit_members IS
|
|
99
|
+
'Who is bound to which unit, with the EFFECTIVE role key — the per-unit override when there is one, otherwise the membership role (#214). Drop-in successor to public.location_members; `id` is derived from (tenant, user, unit) because app.membership_units has none and that triple was already the key.';
|
|
100
|
+
|
|
101
|
+
REVOKE ALL ON public.v_team_members, public.v_units, public.v_unit_members FROM PUBLIC;
|
|
102
|
+
GRANT SELECT ON public.v_team_members, public.v_units, public.v_unit_members TO authenticated, service_role;
|
|
103
|
+
|
|
104
|
+
-- ── the two verbs a view cannot carry ──────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
CREATE OR REPLACE FUNCTION public.unit_member_set(p_unit uuid, p_user uuid, p_role text DEFAULT NULL)
|
|
107
|
+
RETURNS void
|
|
108
|
+
LANGUAGE plpgsql
|
|
109
|
+
SECURITY DEFINER
|
|
110
|
+
SET search_path TO ''
|
|
111
|
+
AS $function$
|
|
112
|
+
DECLARE
|
|
113
|
+
v_tenant uuid := app.current_tenant_id();
|
|
114
|
+
v_role uuid;
|
|
115
|
+
BEGIN
|
|
116
|
+
IF auth.uid() IS NULL OR v_tenant IS NULL THEN
|
|
117
|
+
RAISE EXCEPTION 'unit_member_set: no active tenant for this session' USING ERRCODE = '42501';
|
|
118
|
+
END IF;
|
|
119
|
+
IF NOT app.has_permission('members.manage') THEN
|
|
120
|
+
RAISE EXCEPTION 'unit_member_set: members.manage is required' USING ERRCODE = '42501';
|
|
121
|
+
END IF;
|
|
122
|
+
IF NOT EXISTS (SELECT 1 FROM app.units u WHERE u.tenant_id = v_tenant AND u.id = p_unit) THEN
|
|
123
|
+
RAISE EXCEPTION 'unit_member_set: no unit % in the active tenant', p_unit USING ERRCODE = 'P0002';
|
|
124
|
+
END IF;
|
|
125
|
+
IF NOT EXISTS (SELECT 1 FROM app.memberships m WHERE m.tenant_id = v_tenant AND m.user_id = p_user) THEN
|
|
126
|
+
RAISE EXCEPTION 'unit_member_set: % is not a member of this tenant', p_user USING ERRCODE = '23503';
|
|
127
|
+
END IF;
|
|
128
|
+
|
|
129
|
+
-- NULL role = bound to the unit with no override, which is not the same as
|
|
130
|
+
-- "no role": the membership's own role still applies. Only a NAMED role is
|
|
131
|
+
-- an override, and an unknown one is refused rather than silently ignored.
|
|
132
|
+
IF p_role IS NOT NULL THEN
|
|
133
|
+
SELECT r.id INTO v_role FROM app.roles r WHERE r.tenant_id = v_tenant AND r.key = p_role;
|
|
134
|
+
IF v_role IS NULL THEN
|
|
135
|
+
RAISE EXCEPTION 'unit_member_set: no role % in this tenant', p_role USING ERRCODE = '23503';
|
|
136
|
+
END IF;
|
|
137
|
+
END IF;
|
|
138
|
+
|
|
139
|
+
INSERT INTO app.membership_units (tenant_id, user_id, unit_id, role_override_id)
|
|
140
|
+
VALUES (v_tenant, p_user, p_unit, v_role)
|
|
141
|
+
ON CONFLICT (tenant_id, user_id, unit_id) DO UPDATE SET role_override_id = EXCLUDED.role_override_id;
|
|
142
|
+
END $function$;
|
|
143
|
+
|
|
144
|
+
CREATE OR REPLACE FUNCTION public.unit_member_remove(p_unit uuid, p_user uuid)
|
|
145
|
+
RETURNS void
|
|
146
|
+
LANGUAGE plpgsql
|
|
147
|
+
SECURITY DEFINER
|
|
148
|
+
SET search_path TO ''
|
|
149
|
+
AS $function$
|
|
150
|
+
DECLARE v_tenant uuid := app.current_tenant_id();
|
|
151
|
+
BEGIN
|
|
152
|
+
IF auth.uid() IS NULL OR v_tenant IS NULL THEN
|
|
153
|
+
RAISE EXCEPTION 'unit_member_remove: no active tenant for this session' USING ERRCODE = '42501';
|
|
154
|
+
END IF;
|
|
155
|
+
IF NOT app.has_permission('members.manage') THEN
|
|
156
|
+
RAISE EXCEPTION 'unit_member_remove: members.manage is required' USING ERRCODE = '42501';
|
|
157
|
+
END IF;
|
|
158
|
+
DELETE FROM app.membership_units
|
|
159
|
+
WHERE tenant_id = v_tenant AND user_id = p_user AND unit_id = p_unit;
|
|
160
|
+
END $function$;
|
|
161
|
+
|
|
162
|
+
REVOKE ALL ON FUNCTION public.unit_member_set(uuid, uuid, text) FROM PUBLIC;
|
|
163
|
+
REVOKE ALL ON FUNCTION public.unit_member_remove(uuid, uuid) FROM PUBLIC;
|
|
164
|
+
GRANT EXECUTE ON FUNCTION public.unit_member_set(uuid, uuid, text) TO authenticated, service_role;
|
|
165
|
+
GRANT EXECUTE ON FUNCTION public.unit_member_remove(uuid, uuid) TO authenticated, service_role;
|
|
166
|
+
|
|
167
|
+
COMMENT ON FUNCTION public.unit_member_set(uuid, uuid, text) IS
|
|
168
|
+
'Bind a member to a unit, optionally with a role override (#214). Replaces the upsert the location_members projection accepted.';
|
|
169
|
+
COMMENT ON FUNCTION public.unit_member_remove(uuid, uuid) IS
|
|
170
|
+
'Unbind a member from a unit (#214).';
|
|
171
|
+
|
|
172
|
+
-- ── the team-membership verbs the projection used to accept as writes ──────
|
|
173
|
+
-- The Team screen updated a role, removed a member and created a unit by
|
|
174
|
+
-- writing the projection and letting a trigger forward it. A joined view
|
|
175
|
+
-- cannot accept those, and should not: each is a verb with a rule attached.
|
|
176
|
+
|
|
177
|
+
CREATE OR REPLACE FUNCTION public.member_set_role(p_membership uuid, p_role text)
|
|
178
|
+
RETURNS void
|
|
179
|
+
LANGUAGE plpgsql
|
|
180
|
+
SECURITY DEFINER
|
|
181
|
+
SET search_path TO ''
|
|
182
|
+
AS $function$
|
|
183
|
+
DECLARE
|
|
184
|
+
v_tenant uuid := app.current_tenant_id();
|
|
185
|
+
v_role uuid;
|
|
186
|
+
v_owners integer;
|
|
187
|
+
v_was uuid;
|
|
188
|
+
BEGIN
|
|
189
|
+
IF auth.uid() IS NULL OR v_tenant IS NULL THEN
|
|
190
|
+
RAISE EXCEPTION 'member_set_role: no active tenant for this session' USING ERRCODE = '42501';
|
|
191
|
+
END IF;
|
|
192
|
+
IF NOT app.has_permission('members.manage') THEN
|
|
193
|
+
RAISE EXCEPTION 'member_set_role: members.manage is required' USING ERRCODE = '42501';
|
|
194
|
+
END IF;
|
|
195
|
+
|
|
196
|
+
SELECT r.id INTO v_role FROM app.roles r WHERE r.tenant_id = v_tenant AND r.key = p_role;
|
|
197
|
+
IF v_role IS NULL THEN
|
|
198
|
+
RAISE EXCEPTION 'member_set_role: no role % in this tenant', p_role USING ERRCODE = '23503';
|
|
199
|
+
END IF;
|
|
200
|
+
|
|
201
|
+
SELECT m.role_id INTO v_was FROM app.memberships m WHERE m.tenant_id = v_tenant AND m.id = p_membership;
|
|
202
|
+
IF v_was IS NULL THEN
|
|
203
|
+
RAISE EXCEPTION 'member_set_role: membership not found in the active tenant' USING ERRCODE = 'P0002';
|
|
204
|
+
END IF;
|
|
205
|
+
|
|
206
|
+
-- Demoting the last owner locks everybody out of the tenant, and the screen
|
|
207
|
+
-- that does it has no way to know it was the last one. Refuse here, where
|
|
208
|
+
-- the count is cheap and correct.
|
|
209
|
+
IF EXISTS (SELECT 1 FROM app.roles r WHERE r.id = v_was AND r.key = 'owner')
|
|
210
|
+
AND NOT EXISTS (SELECT 1 FROM app.roles r WHERE r.id = v_role AND r.key = 'owner') THEN
|
|
211
|
+
SELECT count(*) INTO v_owners
|
|
212
|
+
FROM app.memberships m JOIN app.roles r ON r.id = m.role_id
|
|
213
|
+
WHERE m.tenant_id = v_tenant AND m.active AND r.key = 'owner';
|
|
214
|
+
IF v_owners <= 1 THEN
|
|
215
|
+
RAISE EXCEPTION 'member_set_role: this is the last owner — promote somebody else first'
|
|
216
|
+
USING ERRCODE = '23503';
|
|
217
|
+
END IF;
|
|
218
|
+
END IF;
|
|
219
|
+
|
|
220
|
+
UPDATE app.memberships SET role_id = v_role, updated_at = now()
|
|
221
|
+
WHERE tenant_id = v_tenant AND id = p_membership;
|
|
222
|
+
END $function$;
|
|
223
|
+
|
|
224
|
+
CREATE OR REPLACE FUNCTION public.member_remove(p_membership uuid)
|
|
225
|
+
RETURNS void
|
|
226
|
+
LANGUAGE plpgsql
|
|
227
|
+
SECURITY DEFINER
|
|
228
|
+
SET search_path TO ''
|
|
229
|
+
AS $function$
|
|
230
|
+
DECLARE
|
|
231
|
+
v_tenant uuid := app.current_tenant_id();
|
|
232
|
+
v_owner boolean;
|
|
233
|
+
v_owners integer;
|
|
234
|
+
BEGIN
|
|
235
|
+
IF auth.uid() IS NULL OR v_tenant IS NULL THEN
|
|
236
|
+
RAISE EXCEPTION 'member_remove: no active tenant for this session' USING ERRCODE = '42501';
|
|
237
|
+
END IF;
|
|
238
|
+
IF NOT app.has_permission('members.manage') THEN
|
|
239
|
+
RAISE EXCEPTION 'member_remove: members.manage is required' USING ERRCODE = '42501';
|
|
240
|
+
END IF;
|
|
241
|
+
|
|
242
|
+
SELECT EXISTS (SELECT 1 FROM app.roles r WHERE r.id = m.role_id AND r.key = 'owner')
|
|
243
|
+
INTO v_owner FROM app.memberships m WHERE m.tenant_id = v_tenant AND m.id = p_membership;
|
|
244
|
+
IF v_owner IS NULL THEN
|
|
245
|
+
RAISE EXCEPTION 'member_remove: membership not found in the active tenant' USING ERRCODE = 'P0002';
|
|
246
|
+
END IF;
|
|
247
|
+
IF v_owner THEN
|
|
248
|
+
SELECT count(*) INTO v_owners
|
|
249
|
+
FROM app.memberships m JOIN app.roles r ON r.id = m.role_id
|
|
250
|
+
WHERE m.tenant_id = v_tenant AND m.active AND r.key = 'owner';
|
|
251
|
+
IF v_owners <= 1 THEN
|
|
252
|
+
RAISE EXCEPTION 'member_remove: this is the last owner — a tenant nobody owns cannot be administered'
|
|
253
|
+
USING ERRCODE = '23503';
|
|
254
|
+
END IF;
|
|
255
|
+
END IF;
|
|
256
|
+
|
|
257
|
+
-- DEACTIVATE, not delete. The projection's DELETE was forwarded to an UPDATE
|
|
258
|
+
-- that set active = false, and the history is the point: who was on the team
|
|
259
|
+
-- in March is a question an audit asks. v_team_members filters on active, so
|
|
260
|
+
-- the row disappears from every screen either way.
|
|
261
|
+
UPDATE app.memberships SET active = false, updated_at = now()
|
|
262
|
+
WHERE tenant_id = v_tenant AND id = p_membership;
|
|
263
|
+
END $function$;
|
|
264
|
+
|
|
265
|
+
CREATE OR REPLACE FUNCTION public.unit_create(p_name text, p_kind text DEFAULT 'branch',
|
|
266
|
+
p_parent_unit uuid DEFAULT NULL, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
267
|
+
RETURNS uuid
|
|
268
|
+
LANGUAGE plpgsql
|
|
269
|
+
SECURITY DEFINER
|
|
270
|
+
SET search_path TO ''
|
|
271
|
+
AS $function$
|
|
272
|
+
DECLARE
|
|
273
|
+
v_tenant uuid := app.current_tenant_id();
|
|
274
|
+
v_id uuid;
|
|
275
|
+
BEGIN
|
|
276
|
+
IF auth.uid() IS NULL OR v_tenant IS NULL THEN
|
|
277
|
+
RAISE EXCEPTION 'unit_create: no active tenant for this session' USING ERRCODE = '42501';
|
|
278
|
+
END IF;
|
|
279
|
+
IF NOT app.has_permission('units.manage') THEN
|
|
280
|
+
RAISE EXCEPTION 'unit_create: units.manage is required' USING ERRCODE = '42501';
|
|
281
|
+
END IF;
|
|
282
|
+
IF p_name IS NULL OR btrim(p_name) = '' THEN
|
|
283
|
+
RAISE EXCEPTION 'unit_create: a name is required' USING ERRCODE = '22023';
|
|
284
|
+
END IF;
|
|
285
|
+
IF p_kind NOT IN ('hq', 'branch') THEN
|
|
286
|
+
RAISE EXCEPTION 'unit_create: kind must be hq or branch (got %)', p_kind USING ERRCODE = '22023';
|
|
287
|
+
END IF;
|
|
288
|
+
IF p_parent_unit IS NOT NULL
|
|
289
|
+
AND NOT EXISTS (SELECT 1 FROM app.units u WHERE u.tenant_id = v_tenant AND u.id = p_parent_unit) THEN
|
|
290
|
+
RAISE EXCEPTION 'unit_create: no parent unit % in this tenant', p_parent_unit USING ERRCODE = 'P0002';
|
|
291
|
+
END IF;
|
|
292
|
+
|
|
293
|
+
INSERT INTO app.units (tenant_id, kind, name, parent_unit_id, active, metadata)
|
|
294
|
+
VALUES (v_tenant, p_kind, btrim(p_name), p_parent_unit, true, coalesce(p_metadata, '{}'::jsonb))
|
|
295
|
+
RETURNING id INTO v_id;
|
|
296
|
+
|
|
297
|
+
RETURN v_id;
|
|
298
|
+
END $function$;
|
|
299
|
+
|
|
300
|
+
REVOKE ALL ON FUNCTION public.member_set_role(uuid, text) FROM PUBLIC;
|
|
301
|
+
REVOKE ALL ON FUNCTION public.member_remove(uuid) FROM PUBLIC;
|
|
302
|
+
REVOKE ALL ON FUNCTION public.unit_create(text, text, uuid, jsonb) FROM PUBLIC;
|
|
303
|
+
GRANT EXECUTE ON FUNCTION public.member_set_role(uuid, text) TO authenticated, service_role;
|
|
304
|
+
GRANT EXECUTE ON FUNCTION public.member_remove(uuid) TO authenticated, service_role;
|
|
305
|
+
GRANT EXECUTE ON FUNCTION public.unit_create(text, text, uuid, jsonb) TO authenticated, service_role;
|
|
306
|
+
|
|
307
|
+
COMMENT ON FUNCTION public.member_set_role(uuid, text) IS
|
|
308
|
+
'Change a member''s role (#214). Refuses to demote the last owner — the screen doing it cannot know it was the last.';
|
|
309
|
+
COMMENT ON FUNCTION public.member_remove(uuid) IS
|
|
310
|
+
'Deactivate a membership (#214). Refuses the last owner. Deactivates rather than deletes: the projection it replaces did the same, and who was on the team last month is a question an audit asks.';
|
|
311
|
+
COMMENT ON FUNCTION public.unit_create(text, text, uuid, jsonb) IS
|
|
312
|
+
'Create a unit (#214). Replaces the INSERT the locations projection accepted; the address lives in metadata, which is where app.units keeps it.';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 005_the_archetypes_anchor_to_the_unit — eight foreign keys stop pointing at
|
|
3
|
+
-- a projection (#214 / #352).
|
|
4
|
+
--
|
|
5
|
+
-- Found by applying the chain and then trying to drop public.locations: eight
|
|
6
|
+
-- core tables have a FOREIGN KEY into it.
|
|
7
|
+
--
|
|
8
|
+
-- appointments.location_id orders.location_id schedules.location_id
|
|
9
|
+
-- categories.unit_id people.unit_id products.unit_id
|
|
10
|
+
-- tasks.unit_id user_unit_prefs.unit_id
|
|
11
|
+
--
|
|
12
|
+
-- That is not merely why the projection could not be dropped. It is a defect on
|
|
13
|
+
-- its own terms: public.locations is a COMPAT PROJECTION of app.units, kept in
|
|
14
|
+
-- step by a trigger (app.compat_sync_unit), and referential integrity for the
|
|
15
|
+
-- whole unit-scoping layer was resting on a DERIVED table. If the trigger ever
|
|
16
|
+
-- fell behind, the constraint would be enforcing against a copy.
|
|
17
|
+
--
|
|
18
|
+
-- Repointed to app.units(id), which is the real thing and already has the
|
|
19
|
+
-- primary key to be referenced by. ON DELETE is preserved exactly — SET NULL
|
|
20
|
+
-- everywhere except user_unit_prefs, which CASCADEs, because a preference for
|
|
21
|
+
-- a unit that no longer exists is not a preference.
|
|
22
|
+
--
|
|
23
|
+
-- SAFE BECAUSE THE IDS ARE THE SAME. The projection's own comment says so
|
|
24
|
+
-- ("same ids"), and the guard below proves it per pool before touching
|
|
25
|
+
-- anything: if a single row names a unit that app.units does not have, the
|
|
26
|
+
-- migration says so and changes nothing, rather than dropping a constraint and
|
|
27
|
+
-- failing to re-add it.
|
|
28
|
+
--
|
|
29
|
+
-- locations_parent_same_tenant is deliberately not moved: it is locations' own
|
|
30
|
+
-- self-reference and leaves with the table. Neither is location_members', for
|
|
31
|
+
-- the same reason.
|
|
32
|
+
--
|
|
33
|
+
-- DISCOVERED, NOT NAMED. The first version of this file listed the eight
|
|
34
|
+
-- constraints it had seen. Then a real pool turned out to have NINE — it
|
|
35
|
+
-- predates #351, so public.services still exists and still carries
|
|
36
|
+
-- services_unit_id_fkey. A hardcoded list is a photograph of one database, and
|
|
37
|
+
-- the fleet is not one database. So this reads pg_constraint and moves whatever
|
|
38
|
+
-- it finds, which is also what makes it correct on a pool nobody has looked at.
|
|
39
|
+
-- ============================================================================
|
|
40
|
+
|
|
41
|
+
DO $fk$
|
|
42
|
+
DECLARE
|
|
43
|
+
r record;
|
|
44
|
+
v_orph bigint;
|
|
45
|
+
v_moved integer := 0;
|
|
46
|
+
BEGIN
|
|
47
|
+
IF to_regclass('app.units') IS NULL OR to_regclass('public.locations') IS NULL THEN RETURN; END IF;
|
|
48
|
+
|
|
49
|
+
FOR r IN
|
|
50
|
+
SELECT c.conname,
|
|
51
|
+
src.relname AS src_table,
|
|
52
|
+
a.attname AS src_col,
|
|
53
|
+
CASE c.confdeltype WHEN 'c' THEN 'CASCADE' WHEN 'n' THEN 'SET NULL'
|
|
54
|
+
WHEN 'd' THEN 'SET DEFAULT' WHEN 'r' THEN 'RESTRICT'
|
|
55
|
+
ELSE 'NO ACTION' END AS on_delete
|
|
56
|
+
FROM pg_constraint c
|
|
57
|
+
JOIN pg_class src ON src.oid = c.conrelid
|
|
58
|
+
JOIN pg_class tgt ON tgt.oid = c.confrelid
|
|
59
|
+
JOIN pg_namespace tn ON tn.oid = tgt.relnamespace
|
|
60
|
+
JOIN pg_namespace sn ON sn.oid = src.relnamespace
|
|
61
|
+
JOIN pg_attribute a ON a.attrelid = src.oid AND a.attnum = c.conkey[1]
|
|
62
|
+
WHERE c.contype = 'f'
|
|
63
|
+
AND tn.nspname = 'public' AND tgt.relname = 'locations'
|
|
64
|
+
AND sn.nspname = 'public'
|
|
65
|
+
-- Single-column only. The one multi-column FK here is locations' own
|
|
66
|
+
-- (parent_id, tenant_id) self-reference, which leaves with the table.
|
|
67
|
+
AND array_length(c.conkey, 1) = 1
|
|
68
|
+
-- Anything whose own table is going away in the same wave: its FK goes
|
|
69
|
+
-- with it, and repointing it first would be work thrown away.
|
|
70
|
+
AND src.relname NOT IN ('locations', 'location_members')
|
|
71
|
+
LOOP
|
|
72
|
+
EXECUTE format(
|
|
73
|
+
'SELECT count(*) FROM public.%I x WHERE x.%I IS NOT NULL
|
|
74
|
+
AND NOT EXISTS (SELECT 1 FROM app.units u WHERE u.id = x.%I)',
|
|
75
|
+
r.src_table, r.src_col, r.src_col) INTO v_orph;
|
|
76
|
+
IF v_orph > 0 THEN
|
|
77
|
+
RAISE EXCEPTION '005: %.% has % row(s) naming a unit app.units does not have — reconcile before repointing',
|
|
78
|
+
r.src_table, r.src_col, v_orph USING ERRCODE = '23503';
|
|
79
|
+
END IF;
|
|
80
|
+
|
|
81
|
+
EXECUTE format('ALTER TABLE public.%I DROP CONSTRAINT %I', r.src_table, r.conname);
|
|
82
|
+
EXECUTE format('ALTER TABLE public.%I ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES app.units(id) ON DELETE %s',
|
|
83
|
+
r.src_table, r.conname, r.src_col, r.on_delete);
|
|
84
|
+
v_moved := v_moved + 1;
|
|
85
|
+
END LOOP;
|
|
86
|
+
|
|
87
|
+
IF v_moved > 0 THEN
|
|
88
|
+
RAISE NOTICE '005: % foreign key(s) repointed from public.locations to app.units', v_moved;
|
|
89
|
+
END IF;
|
|
90
|
+
END $fk$;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 006_the_helpers_read_the_source — six SQL functions and one policy stop
|
|
3
|
+
-- reading the compat projections (#214 / #352).
|
|
4
|
+
--
|
|
5
|
+
-- Found by applying the chain and then trying to drop the three projections.
|
|
6
|
+
-- One object BLOCKED the drop — the profiles_select policy. Six others did
|
|
7
|
+
-- not block it and were the real hazard:
|
|
8
|
+
--
|
|
9
|
+
-- is_tenant_member user_admin_tenant_ids user_unit_ids
|
|
10
|
+
-- unit_bootstrap tenant_member_activity unlinked_member_count
|
|
11
|
+
--
|
|
12
|
+
-- All six are LANGUAGE sql, and a classic SQL function body is a string that
|
|
13
|
+
-- Postgres does not resolve until it runs. So the DROP would have succeeded,
|
|
14
|
+
-- and the breakage would have arrived later, at a call — and three of these
|
|
15
|
+
-- are called from inside RLS across the whole database. `is_tenant_member` and
|
|
16
|
+
-- `user_unit_ids` are in the N17 allowlist precisely because policies lean on
|
|
17
|
+
-- them. Dropping a table under them would not have failed loudly; it would
|
|
18
|
+
-- have quietly emptied every policy that asks who you are.
|
|
19
|
+
--
|
|
20
|
+
-- That is the argument for this file existing separately from 005: a foreign
|
|
21
|
+
-- key announces itself, and a function body does not.
|
|
22
|
+
--
|
|
23
|
+
-- Each body is the original with the relation swapped, and nothing else:
|
|
24
|
+
--
|
|
25
|
+
-- public.tenant_members -> app.memberships (active only, which is what
|
|
26
|
+
-- the projection contained)
|
|
27
|
+
-- public.location_members -> app.membership_units
|
|
28
|
+
-- public.locations -> app.units, or the successor views from 004
|
|
29
|
+
-- where the shape is what the caller wanted
|
|
30
|
+
--
|
|
31
|
+
-- `role` is the one place the swap is not one word: the projection carried the
|
|
32
|
+
-- role KEY and app.memberships carries role_id, so the two that filtered on it
|
|
33
|
+
-- join app.roles to get the key back.
|
|
34
|
+
-- ============================================================================
|
|
35
|
+
|
|
36
|
+
CREATE OR REPLACE FUNCTION public.is_tenant_member(p_tenant_id uuid, p_user_id uuid)
|
|
37
|
+
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public', 'pg_temp'
|
|
38
|
+
AS $function$
|
|
39
|
+
SELECT EXISTS (
|
|
40
|
+
SELECT 1 FROM app.memberships m
|
|
41
|
+
WHERE m.tenant_id = p_tenant_id AND m.user_id = p_user_id AND m.active
|
|
42
|
+
);
|
|
43
|
+
$function$;
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE FUNCTION public.user_admin_tenant_ids()
|
|
46
|
+
RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public', 'pg_temp'
|
|
47
|
+
AS $function$
|
|
48
|
+
SELECT m.tenant_id
|
|
49
|
+
FROM app.memberships m
|
|
50
|
+
JOIN app.roles r ON r.id = m.role_id
|
|
51
|
+
WHERE m.user_id = auth.uid() AND m.active
|
|
52
|
+
AND r.key IN ('owner', 'admin');
|
|
53
|
+
$function$;
|
|
54
|
+
|
|
55
|
+
CREATE OR REPLACE FUNCTION public.user_unit_ids()
|
|
56
|
+
RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public', 'pg_temp'
|
|
57
|
+
AS $function$
|
|
58
|
+
WITH RECURSIVE seed AS (
|
|
59
|
+
SELECT mu.unit_id AS id, 0 AS depth
|
|
60
|
+
FROM app.membership_units mu
|
|
61
|
+
WHERE mu.user_id = auth.uid()
|
|
62
|
+
),
|
|
63
|
+
tree AS (
|
|
64
|
+
SELECT id, depth FROM seed
|
|
65
|
+
UNION -- UNION, not UNION ALL: dedupes, and
|
|
66
|
+
SELECT u.id, t.depth + 1 -- therefore terminates on a cycle
|
|
67
|
+
FROM app.units u
|
|
68
|
+
JOIN tree t ON u.parent_unit_id = t.id
|
|
69
|
+
WHERE t.depth < 8 -- a policy must never be able to hang
|
|
70
|
+
)
|
|
71
|
+
SELECT id FROM tree;
|
|
72
|
+
$function$;
|
|
73
|
+
|
|
74
|
+
CREATE OR REPLACE FUNCTION public.tenant_member_activity(p_tenant_id uuid)
|
|
75
|
+
RETURNS TABLE(user_id uuid, last_sign_in_at timestamp with time zone, signed_up_at timestamp with time zone)
|
|
76
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public', 'pg_temp'
|
|
77
|
+
AS $function$
|
|
78
|
+
SELECT u.id, u.last_sign_in_at, u.created_at
|
|
79
|
+
FROM auth.users u
|
|
80
|
+
JOIN app.memberships m ON m.user_id = u.id
|
|
81
|
+
WHERE m.tenant_id = p_tenant_id AND m.active
|
|
82
|
+
-- The gate, and the only one: an admin of THIS tenant, and nobody else.
|
|
83
|
+
-- Without it the function is a directory of every last_sign_in_at in the
|
|
84
|
+
-- pool, reachable by anyone with an anon key.
|
|
85
|
+
AND p_tenant_id IN (SELECT public.user_admin_tenant_ids());
|
|
86
|
+
$function$;
|
|
87
|
+
|
|
88
|
+
CREATE OR REPLACE FUNCTION public.unlinked_member_count()
|
|
89
|
+
RETURNS integer LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public', 'pg_temp'
|
|
90
|
+
AS $function$
|
|
91
|
+
SELECT count(*)::integer FROM app.memberships WHERE person_id IS NULL AND active;
|
|
92
|
+
$function$;
|
|
93
|
+
|
|
94
|
+
CREATE OR REPLACE FUNCTION public.unit_bootstrap(p_tenant_id uuid, p_device_id text DEFAULT NULL::text)
|
|
95
|
+
RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'public', 'pg_temp'
|
|
96
|
+
AS $function$
|
|
97
|
+
SELECT jsonb_build_object(
|
|
98
|
+
'mode', COALESCE((
|
|
99
|
+
SELECT t.unit_scoping_enabled FROM public.tenants t WHERE t.id = p_tenant_id
|
|
100
|
+
), false),
|
|
101
|
+
-- v_units, not app.units: this shape is the one the shell already parses,
|
|
102
|
+
-- and the view exists to keep saying it after the projection is gone.
|
|
103
|
+
'units', COALESCE((
|
|
104
|
+
SELECT jsonb_agg(to_jsonb(l) ORDER BY l.is_headquarters DESC NULLS LAST, l.name)
|
|
105
|
+
FROM public.v_units l WHERE l.tenant_id = p_tenant_id
|
|
106
|
+
), '[]'::jsonb),
|
|
107
|
+
'bindings', COALESCE((
|
|
108
|
+
SELECT jsonb_agg(jsonb_build_object('location_id', lm.location_id, 'role', lm.role))
|
|
109
|
+
FROM public.v_unit_members lm WHERE lm.user_id = (SELECT auth.uid())
|
|
110
|
+
), '[]'::jsonb),
|
|
111
|
+
'scoped', COALESCE((
|
|
112
|
+
SELECT jsonb_object_agg(sr.resource_table, jsonb_build_object(
|
|
113
|
+
'unit_column', sr.unit_column, 'shareable', sr.shareable))
|
|
114
|
+
FROM public.scoped_resources sr
|
|
115
|
+
), '{}'::jsonb),
|
|
116
|
+
-- O pino desta máquina ganha do pino da conta quando os dois existem: a
|
|
117
|
+
-- máquina fica na loja, a conta viaja com a pessoa.
|
|
118
|
+
'device_pin', (
|
|
119
|
+
SELECT p.unit_id FROM public.user_unit_prefs p
|
|
120
|
+
WHERE p.user_id = (SELECT auth.uid())
|
|
121
|
+
AND p.tenant_id = p_tenant_id
|
|
122
|
+
AND p.device_id = p_device_id
|
|
123
|
+
),
|
|
124
|
+
'device_pin_set', EXISTS (
|
|
125
|
+
SELECT 1 FROM public.user_unit_prefs p
|
|
126
|
+
WHERE p.user_id = (SELECT auth.uid())
|
|
127
|
+
AND p.tenant_id = p_tenant_id
|
|
128
|
+
AND p.device_id = p_device_id
|
|
129
|
+
),
|
|
130
|
+
'account_pin', (
|
|
131
|
+
SELECT p.unit_id FROM public.user_unit_prefs p
|
|
132
|
+
WHERE p.user_id = (SELECT auth.uid())
|
|
133
|
+
AND p.tenant_id = p_tenant_id
|
|
134
|
+
ORDER BY p.updated_at DESC
|
|
135
|
+
LIMIT 1
|
|
136
|
+
),
|
|
137
|
+
'account_pin_set', EXISTS (
|
|
138
|
+
SELECT 1 FROM public.user_unit_prefs p
|
|
139
|
+
WHERE p.user_id = (SELECT auth.uid()) AND p.tenant_id = p_tenant_id
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
$function$;
|
|
143
|
+
|
|
144
|
+
-- The one hard dependency: a policy naming the table blocks the DROP outright.
|
|
145
|
+
DROP POLICY IF EXISTS "profiles_select" ON public.profiles;
|
|
146
|
+
CREATE POLICY "profiles_select" ON public.profiles FOR SELECT TO authenticated
|
|
147
|
+
USING (
|
|
148
|
+
id = (SELECT auth.uid())
|
|
149
|
+
OR id IN (
|
|
150
|
+
SELECT m.user_id FROM app.memberships m
|
|
151
|
+
WHERE m.active AND m.tenant_id IN (SELECT public.user_tenant_ids())
|
|
152
|
+
)
|
|
153
|
+
);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 007_the_compat_machinery_leaves_with_the_tables — what MAINTAINED the three
|
|
3
|
+
-- projections goes with them (#214 / #352).
|
|
4
|
+
--
|
|
5
|
+
-- Found by `node packages/db/test/chain/run.mjs --full`, which is the run that
|
|
6
|
+
-- applies the optional plugins too — so it is the only run where shop 0110
|
|
7
|
+
-- actually drops the projections, and therefore the only run that could catch
|
|
8
|
+
-- this. The core-A chain had been green throughout.
|
|
9
|
+
--
|
|
10
|
+
-- The seed failed with `relation "public.locations" does not exist`, and the
|
|
11
|
+
-- cause was not the seed: FOUR TRIGGERS ON app.* still fanned every write out
|
|
12
|
+
-- into the projections.
|
|
13
|
+
--
|
|
14
|
+
-- app.units units_compat -> app.compat_sync_unit
|
|
15
|
+
-- app.memberships memberships_compat -> app.compat_sync_membership
|
|
16
|
+
-- app.membership_units membership_units_compat -> app.compat_sync_membership
|
|
17
|
+
-- app.roles roles_compat -> app.compat_sync_membership
|
|
18
|
+
--
|
|
19
|
+
-- So after 0110, CREATING A UNIT OR A MEMBERSHIP RAISED. A projection is two
|
|
20
|
+
-- things — the copy and the machine that keeps it — and dropping only the copy
|
|
21
|
+
-- leaves the machine writing to something that is not there. The `plpgsql
|
|
22
|
+
-- resolves names at call time` property that made these safe to drop AROUND is
|
|
23
|
+
-- exactly what stopped it being noticed at drop time.
|
|
24
|
+
--
|
|
25
|
+
-- ensure_member_person and backfill_member_persons are NOT dropped: linking a
|
|
26
|
+
-- login to its Person is still a live concern (#137). Only the relation they
|
|
27
|
+
-- read changes, taken from the pool's own body so nothing else moves with it.
|
|
28
|
+
-- ============================================================================
|
|
29
|
+
|
|
30
|
+
-- GUARDED, because where this runs is not where the tables go. The projections
|
|
31
|
+
-- are dropped by shop 0110, and shop is an OPTIONAL plugin: on a core-A pool it
|
|
32
|
+
-- never runs, and the tables are still there and still have to be maintained.
|
|
33
|
+
-- Dropping the machinery here unconditionally would leave a core-A cluster with
|
|
34
|
+
-- three projection tables nothing keeps in step — worse than either end state.
|
|
35
|
+
--
|
|
36
|
+
-- So this only fires once the tables are actually gone, which on a --full chain
|
|
37
|
+
-- is shop 0111, and on a pool that already has them dropped is right here.
|
|
38
|
+
DO $guard$
|
|
39
|
+
BEGIN
|
|
40
|
+
IF to_regclass('public.locations') IS NOT NULL
|
|
41
|
+
OR to_regclass('public.tenant_members') IS NOT NULL
|
|
42
|
+
OR to_regclass('public.location_members') IS NOT NULL THEN
|
|
43
|
+
RAISE NOTICE '007: the projections are still present — the machinery stays with them';
|
|
44
|
+
RETURN;
|
|
45
|
+
END IF;
|
|
46
|
+
|
|
47
|
+
EXECUTE $sql$
|
|
48
|
+
DROP TRIGGER IF EXISTS units_compat ON app.units;
|
|
49
|
+
DROP TRIGGER IF EXISTS memberships_compat ON app.memberships;
|
|
50
|
+
DROP TRIGGER IF EXISTS membership_units_compat ON app.membership_units;
|
|
51
|
+
DROP TRIGGER IF EXISTS roles_compat ON app.roles;
|
|
52
|
+
|
|
53
|
+
DROP FUNCTION IF EXISTS app.trg_units_compat() CASCADE;
|
|
54
|
+
DROP FUNCTION IF EXISTS app.trg_memberships_compat() CASCADE;
|
|
55
|
+
DROP FUNCTION IF EXISTS app.trg_membership_units_compat() CASCADE;
|
|
56
|
+
DROP FUNCTION IF EXISTS app.trg_roles_compat() CASCADE;
|
|
57
|
+
DROP FUNCTION IF EXISTS app.compat_sync_unit(uuid) CASCADE;
|
|
58
|
+
DROP FUNCTION IF EXISTS app.compat_sync_membership(uuid, uuid) CASCADE;
|
|
59
|
+
DROP FUNCTION IF EXISTS app.compat_rebuild() CASCADE;
|
|
60
|
+
DROP FUNCTION IF EXISTS public.handle_location_member_tenant() CASCADE;
|
|
61
|
+
$sql$;
|
|
62
|
+
RAISE NOTICE '007: the compat sync machinery is retired';
|
|
63
|
+
END $guard$;
|
|
64
|
+
|
|
65
|
+
DO $fix$
|
|
66
|
+
DECLARE r record; v_def text; v_new text; v_n int := 0;
|
|
67
|
+
BEGIN
|
|
68
|
+
FOR r IN
|
|
69
|
+
SELECT p.oid FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
70
|
+
WHERE n.nspname = 'public' AND p.proname IN ('ensure_member_person','backfill_member_persons')
|
|
71
|
+
LOOP
|
|
72
|
+
v_def := pg_get_functiondef(r.oid);
|
|
73
|
+
v_new := replace(v_def, 'public.tenant_members', 'app.memberships');
|
|
74
|
+
IF v_new <> v_def THEN EXECUTE v_new; v_n := v_n + 1; END IF;
|
|
75
|
+
END LOOP;
|
|
76
|
+
RAISE NOTICE '007: % person-link function(s) moved to app.memberships', v_n;
|
|
77
|
+
END $fix$;
|