@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 013_the_workspace_switch_reaches_the_other_half.sql — the unit dropdown
|
|
3
|
+
-- stops listing other people's businesses.
|
|
4
|
+
--
|
|
5
|
+
-- THE BUG, as seen: the "Unidade" filter and the "Selecionar local…" picker in
|
|
6
|
+
-- a beauty account offered Américas Shopping, Depósito Central, Papa Léguas
|
|
7
|
+
-- Tacos and Mesa 1 — units belonging to eight OTHER tenants. Picking one wrote
|
|
8
|
+
-- a foreign unit id onto a booking. This is a tenant boundary failure, not a
|
|
9
|
+
-- filter that forgot a WHERE.
|
|
10
|
+
--
|
|
11
|
+
-- THE CAUSE, and it is one sentence: the database has TWO ideas of "my tenant"
|
|
12
|
+
-- and only one of them can hear the workspace switch.
|
|
13
|
+
--
|
|
14
|
+
-- app.current_tenant_id() — signed JWT claim, else the `x-fayz-tenant`
|
|
15
|
+
-- header the client sends, else "your only
|
|
16
|
+
-- account". This is the ACTIVE workspace.
|
|
17
|
+
-- public.user_tenant_ids() — every account you belong to. It reads the JWT
|
|
18
|
+
-- app.is_member(tenant) claim but has never read the HEADER, and nobody
|
|
19
|
+
-- mints a claim.
|
|
20
|
+
--
|
|
21
|
+
-- 145 added `x-fayz-tenant` so a two-account user could read public.people /
|
|
22
|
+
-- orders / products at all — those policies were already on current_tenant_id()
|
|
23
|
+
-- and were returning NOTHING. The other half of the schema was left on
|
|
24
|
+
-- user_tenant_ids(), where the same header is invisible. So the switch narrows
|
|
25
|
+
-- one half of the database and the other half keeps answering for all of them
|
|
26
|
+
-- at once: 133 tables, every plugin, plus app.units, app.roles and
|
|
27
|
+
-- app.role_permissions.
|
|
28
|
+
--
|
|
29
|
+
-- Nobody saw it on a single-account tenant, which is every real customer today
|
|
30
|
+
-- and every QA login but one. It is visible the moment somebody has two.
|
|
31
|
+
--
|
|
32
|
+
-- THE FIX: user_tenant_ids() means the ACTIVE workspace, like everything else.
|
|
33
|
+
-- The one caller that genuinely wants "every account of mine" — the app
|
|
34
|
+
-- switcher, whose whole job (008) is to reach across accounts — says so out
|
|
35
|
+
-- loud, through a function with that name.
|
|
36
|
+
-- ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
-- ── §1 "my tenant" means one tenant ───────────────────────────────────────
|
|
39
|
+
--
|
|
40
|
+
-- Delegating rather than re-deriving: the claim/header/single-account ladder is
|
|
41
|
+
-- subtle (a header pointing at an account that is not yours must DENY, not fall
|
|
42
|
+
-- back), it already exists in current_tenant_id, and two copies of it would be
|
|
43
|
+
-- two chances to disagree about who you are.
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE FUNCTION public.user_tenant_ids() RETURNS SETOF uuid
|
|
46
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
47
|
+
SET search_path TO ''
|
|
48
|
+
AS $$
|
|
49
|
+
SELECT t.id FROM (SELECT app.current_tenant_id() AS id) t WHERE t.id IS NOT NULL;
|
|
50
|
+
$$;
|
|
51
|
+
|
|
52
|
+
COMMENT ON FUNCTION public.user_tenant_ids() IS
|
|
53
|
+
'The ACTIVE workspace, as a set, for the ~130 policies written as `tenant_id IN (SELECT user_tenant_ids())` (013). It used to mean "every account I belong to", which made the workspace switch invisible to every table that used it. Cross-account readers want user_all_tenant_ids() and must say so.';
|
|
54
|
+
|
|
55
|
+
-- Admin rights are read the same way: which accounts you administer is not the
|
|
56
|
+
-- question a policy on THIS account's row is asking.
|
|
57
|
+
CREATE OR REPLACE FUNCTION public.user_admin_tenant_ids() RETURNS SETOF uuid
|
|
58
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
59
|
+
SET search_path TO ''
|
|
60
|
+
AS $$
|
|
61
|
+
SELECT m.tenant_id
|
|
62
|
+
FROM app.memberships m
|
|
63
|
+
JOIN app.roles r ON r.id = m.role_id
|
|
64
|
+
WHERE m.user_id = auth.uid() AND m.active
|
|
65
|
+
AND m.tenant_id = app.current_tenant_id()
|
|
66
|
+
AND r.key IN ('owner', 'admin');
|
|
67
|
+
$$;
|
|
68
|
+
|
|
69
|
+
COMMENT ON FUNCTION public.user_admin_tenant_ids() IS
|
|
70
|
+
'Whether you administer the ACTIVE workspace, as a set (013). Narrowed for the same reason as user_tenant_ids: being an owner somewhere else is not permission here.';
|
|
71
|
+
|
|
72
|
+
-- ── §2 the switcher asks its own question ─────────────────────────────────
|
|
73
|
+
|
|
74
|
+
CREATE OR REPLACE FUNCTION public.user_all_tenant_ids() RETURNS SETOF uuid
|
|
75
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
76
|
+
SET search_path TO ''
|
|
77
|
+
AS $$
|
|
78
|
+
SELECT m.tenant_id FROM app.memberships m
|
|
79
|
+
WHERE m.user_id = auth.uid() AND m.active;
|
|
80
|
+
$$;
|
|
81
|
+
|
|
82
|
+
COMMENT ON FUNCTION public.user_all_tenant_ids() IS
|
|
83
|
+
'Every account the caller belongs to (013). For the app/workspace switcher ONLY — the one screen whose question is "which of my accounts", asked before an account is chosen. A policy that reaches for this is claiming to be cross-account; almost none should be.';
|
|
84
|
+
|
|
85
|
+
REVOKE ALL ON FUNCTION public.user_all_tenant_ids() FROM PUBLIC;
|
|
86
|
+
GRANT EXECUTE ON FUNCTION public.user_all_tenant_ids() TO authenticated, service_role;
|
|
87
|
+
|
|
88
|
+
-- app.tenant_apps is that screen's table: v_tenant_apps (008) lists which of
|
|
89
|
+
-- MY accounts run the app I am looking at, and that list is drawn before the
|
|
90
|
+
-- account is picked. Narrowing it would leave the switcher with one row.
|
|
91
|
+
DROP POLICY IF EXISTS tenant_apps_read ON app.tenant_apps;
|
|
92
|
+
CREATE POLICY tenant_apps_read ON app.tenant_apps FOR SELECT TO authenticated
|
|
93
|
+
USING (tenant_id IN (SELECT public.user_all_tenant_ids()));
|
|
94
|
+
|
|
95
|
+
-- ── §3 the four app.* reads still on "any account of mine" ────────────────
|
|
96
|
+
--
|
|
97
|
+
-- Their siblings (units_insert/update/delete, roles_update, …) already compare
|
|
98
|
+
-- to current_tenant_id(). Only the SELECT was written with is_member, which is
|
|
99
|
+
-- why writing was scoped and READING was not.
|
|
100
|
+
|
|
101
|
+
DROP POLICY IF EXISTS units_select ON app.units;
|
|
102
|
+
CREATE POLICY units_select ON app.units FOR SELECT TO authenticated
|
|
103
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
104
|
+
|
|
105
|
+
DROP POLICY IF EXISTS roles_select ON app.roles;
|
|
106
|
+
CREATE POLICY roles_select ON app.roles FOR SELECT TO authenticated
|
|
107
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
108
|
+
|
|
109
|
+
DROP POLICY IF EXISTS role_permissions_select ON app.role_permissions;
|
|
110
|
+
CREATE POLICY role_permissions_select ON app.role_permissions FOR SELECT TO authenticated
|
|
111
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
112
|
+
|
|
113
|
+
DROP POLICY IF EXISTS resource_grants_select ON app.resource_grants;
|
|
114
|
+
CREATE POLICY resource_grants_select ON app.resource_grants FOR SELECT TO authenticated
|
|
115
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
116
|
+
|
|
117
|
+
-- ── §4 a table with no boundary at all ────────────────────────────────────
|
|
118
|
+
--
|
|
119
|
+
-- USING (true): which stock engine each tenant's products are counted by, plus
|
|
120
|
+
-- their product ids, readable by anybody with a login to anything. 146 wrote it
|
|
121
|
+
-- open on the argument that the row is "configuration"; a row keyed by
|
|
122
|
+
-- tenant_id and product_id is that tenant's, whatever it holds.
|
|
123
|
+
|
|
124
|
+
DROP POLICY IF EXISTS product_stock_source_read ON app.product_stock_source;
|
|
125
|
+
CREATE POLICY product_stock_source_read ON app.product_stock_source FOR SELECT TO authenticated
|
|
126
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
127
|
+
|
|
128
|
+
-- ── §5 the legacy role tables ─────────────────────────────────────────────
|
|
129
|
+
--
|
|
130
|
+
-- tenant_roles_read is older still: it joins public.tenant_members, which 007
|
|
131
|
+
-- removed, so the policy is dead weight that would come back to life — wide —
|
|
132
|
+
-- if anything ever recreated that name.
|
|
133
|
+
|
|
134
|
+
-- Guarded: a pool that never took the permission-profiles tables has neither,
|
|
135
|
+
-- and a boundary fix is not the place to demand them.
|
|
136
|
+
DO $$
|
|
137
|
+
BEGIN
|
|
138
|
+
IF to_regclass('public.tenant_roles') IS NOT NULL THEN
|
|
139
|
+
DROP POLICY IF EXISTS tenant_roles_read ON public.tenant_roles;
|
|
140
|
+
DROP POLICY IF EXISTS tenant_roles_select ON public.tenant_roles;
|
|
141
|
+
CREATE POLICY tenant_roles_select ON public.tenant_roles FOR SELECT TO authenticated
|
|
142
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
143
|
+
END IF;
|
|
144
|
+
|
|
145
|
+
IF to_regclass('public.tenant_role_overrides') IS NOT NULL THEN
|
|
146
|
+
DROP POLICY IF EXISTS overrides_select ON public.tenant_role_overrides;
|
|
147
|
+
CREATE POLICY overrides_select ON public.tenant_role_overrides FOR SELECT TO authenticated
|
|
148
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
149
|
+
END IF;
|
|
150
|
+
END $$;
|
|
151
|
+
|
|
152
|
+
-- ── §6 the audit row is written into the account it happened in ───────────
|
|
153
|
+
|
|
154
|
+
DROP POLICY IF EXISTS audit_insert ON public.audit_logs;
|
|
155
|
+
CREATE POLICY audit_insert ON public.audit_logs FOR INSERT TO authenticated
|
|
156
|
+
WITH CHECK (tenant_id = (SELECT app.current_tenant_id()));
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 014_the_chooser_can_read_what_it_offers.sql — the workspace picker stops
|
|
3
|
+
-- asking a question it is not allowed to hear the answer to.
|
|
4
|
+
--
|
|
5
|
+
-- THE BUG, as seen: an account belonging to seven workspaces signs in and the
|
|
6
|
+
-- shell offers "Nova área de trabalho" — as if it belonged to none. The
|
|
7
|
+
-- browser console shows the real failure a layer down:
|
|
8
|
+
--
|
|
9
|
+
-- TypeError: Cannot read properties of null (reading 'charAt')
|
|
10
|
+
-- at capitalize (admin/src/org/adapters/supabase.ts)
|
|
11
|
+
-- at async loadOrgs
|
|
12
|
+
--
|
|
13
|
+
-- THE CAUSE: 013 finished half a thought. It redefined user_tenant_ids() to
|
|
14
|
+
-- mean the ACTIVE workspace — correct, and the fix for 133 leaking tables —
|
|
15
|
+
-- and it understood that the SWITCHER is the one screen that must read across
|
|
16
|
+
-- accounts, because it runs BEFORE an account is chosen. But it granted that
|
|
17
|
+
-- exception to exactly one table, app.tenant_apps, and the switcher reads
|
|
18
|
+
-- three:
|
|
19
|
+
--
|
|
20
|
+
-- app.tenant_apps — which of my accounts run this app. 013 fixed it.
|
|
21
|
+
-- public.tenants — what those accounts are CALLED. Still narrow.
|
|
22
|
+
-- app.roles — what I am in each of them. Still narrow.
|
|
23
|
+
--
|
|
24
|
+
-- Before a choice exists current_tenant_id() is NULL, so the last two answer
|
|
25
|
+
-- with nothing: v_team_members returns the seven memberships (its own table
|
|
26
|
+
-- lets you see your own rows) with `tenant` null and `role` null. The client
|
|
27
|
+
-- capitalises the role, hits null, throws, and the whole membership load dies
|
|
28
|
+
-- with it — which is why the screen says "no workspace" rather than "seven
|
|
29
|
+
-- workspaces with blank names".
|
|
30
|
+
--
|
|
31
|
+
-- It is a chicken-and-egg, and it is invisible on a single-workspace account:
|
|
32
|
+
-- there the client's own fallback picks the only membership before anything
|
|
33
|
+
-- needs a name. Every real customer today is single-workspace. Every QA login
|
|
34
|
+
-- but one is too.
|
|
35
|
+
--
|
|
36
|
+
-- THE FIX: the two remaining reads on the pre-selection path say out loud that
|
|
37
|
+
-- they are cross-account, the same way 013 made app.tenant_apps say it. This
|
|
38
|
+
-- restores exactly what these two tables answered BEFORE 013 and nothing more
|
|
39
|
+
-- — 013 narrowed them as collateral, not as intent; its subject was units and
|
|
40
|
+
-- bookings leaking to NON-members, which neither of these ever did.
|
|
41
|
+
-- ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
-- ── §1 the accounts have names again ──────────────────────────────────────
|
|
44
|
+
--
|
|
45
|
+
-- A workspace row is readable when you are a MEMBER of that workspace — which
|
|
46
|
+
-- is what the policy said until 013, and what the picker needs to draw a list
|
|
47
|
+
-- you can choose from. Membership, not the active choice, is the boundary
|
|
48
|
+
-- here: the rows are your own accounts either way.
|
|
49
|
+
|
|
50
|
+
DROP POLICY IF EXISTS tenants_select ON public.tenants;
|
|
51
|
+
CREATE POLICY tenants_select ON public.tenants FOR SELECT TO authenticated
|
|
52
|
+
USING (id IN (SELECT public.user_all_tenant_ids()));
|
|
53
|
+
|
|
54
|
+
COMMENT ON POLICY tenants_select ON public.tenants IS
|
|
55
|
+
'Every workspace you belong to, named (014). Cross-account on purpose: the picker that chooses the active workspace has to render the list before one exists, so scoping this to the active choice makes the choice unmakeable.';
|
|
56
|
+
|
|
57
|
+
-- ── §2 my own role travels with my own membership ─────────────────────────
|
|
58
|
+
--
|
|
59
|
+
-- Not the whole role TABLE of every account — 013 was right that the role list
|
|
60
|
+
-- of a workspace you are not looking at is none of this request's business.
|
|
61
|
+
-- The narrow thing the picker needs is one row per membership: the role I hold
|
|
62
|
+
-- THERE, which v_team_members joins to turn a role_id into a key ('owner').
|
|
63
|
+
--
|
|
64
|
+
-- SECURITY DEFINER for the same reason the 013 helpers are: a policy
|
|
65
|
+
-- expression is evaluated as the caller, and reaching into app.memberships
|
|
66
|
+
-- from inside a policy that app.memberships' own policies can reach back into
|
|
67
|
+
-- is how recursion starts.
|
|
68
|
+
|
|
69
|
+
CREATE OR REPLACE FUNCTION public.user_own_role_ids() RETURNS SETOF uuid
|
|
70
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
71
|
+
SET search_path TO ''
|
|
72
|
+
AS $$
|
|
73
|
+
SELECT m.role_id FROM app.memberships m
|
|
74
|
+
WHERE m.user_id = auth.uid() AND m.active AND m.role_id IS NOT NULL;
|
|
75
|
+
$$;
|
|
76
|
+
|
|
77
|
+
COMMENT ON FUNCTION public.user_own_role_ids() IS
|
|
78
|
+
'The roles the caller personally holds, across every account they belong to (014). For the workspace picker, which shows you what you are in each — not for reading an account''s role list, which stays on the active workspace.';
|
|
79
|
+
|
|
80
|
+
REVOKE ALL ON FUNCTION public.user_own_role_ids() FROM PUBLIC;
|
|
81
|
+
GRANT EXECUTE ON FUNCTION public.user_own_role_ids() TO authenticated, service_role;
|
|
82
|
+
|
|
83
|
+
DROP POLICY IF EXISTS roles_select ON app.roles;
|
|
84
|
+
CREATE POLICY roles_select ON app.roles FOR SELECT TO authenticated
|
|
85
|
+
USING (
|
|
86
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
87
|
+
OR id IN (SELECT public.user_own_role_ids())
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
COMMENT ON POLICY roles_select ON app.roles IS
|
|
91
|
+
'The active workspace''s roles (013), plus the caller''s OWN role in each account they belong to (014) — the second clause is what lets the workspace picker say "Owner" next to a workspace you have not entered yet.';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 015_the_unit_bindings_belong_to_one_workspace.sql — the Unidades menu stops
|
|
3
|
+
-- going blank in the workspace you are actually standing in.
|
|
4
|
+
--
|
|
5
|
+
-- THE BUG, as seen: "Espaço Facial não lista as unidades". The account has four
|
|
6
|
+
-- ACTIVE units in that workspace and the menu drew its "Unidades" heading with
|
|
7
|
+
-- nothing underneath it.
|
|
8
|
+
--
|
|
9
|
+
-- THE CAUSE: `unit_bootstrap` filters every clause it has by p_tenant_id — the
|
|
10
|
+
-- units, the scoping mode, both pins — except one:
|
|
11
|
+
--
|
|
12
|
+
-- 'bindings', ( SELECT ... FROM public.v_unit_members lm
|
|
13
|
+
-- WHERE lm.user_id = auth.uid() ) -- and nothing else
|
|
14
|
+
--
|
|
15
|
+
-- RLS does not save it. `membership_units_select` lets you read your OWN rows
|
|
16
|
+
-- unconditionally (`user_id = auth.uid() OR ...`) — it has to, or you could not
|
|
17
|
+
-- find yourself — and "your own rows" spans every account you belong to. So the
|
|
18
|
+
-- call returned a binding to `Ideale 633`, a unit of Espaco Renova Rio, while
|
|
19
|
+
-- the caller was standing in Espaço Facial.
|
|
20
|
+
--
|
|
21
|
+
-- The client then does the reasonable thing with an unreasonable answer.
|
|
22
|
+
-- UnitMenuSection narrows the list to what you are bound to:
|
|
23
|
+
--
|
|
24
|
+
-- reachable = myUnitIds.length > 0 ? myUnitIds : <every unit>
|
|
25
|
+
--
|
|
26
|
+
-- One foreign binding is enough to take the first branch, and no unit of THIS
|
|
27
|
+
-- tenant is in it. Not an empty list — a list of somewhere else. The fallback
|
|
28
|
+
-- that would have shown all four units never runs.
|
|
29
|
+
--
|
|
30
|
+
-- Invisible on a single-workspace account, like everything else in this family:
|
|
31
|
+
-- there, every binding you have is in the only tenant you are in.
|
|
32
|
+
--
|
|
33
|
+
-- THE FIX: the one clause that forgot says which workspace it means, like its
|
|
34
|
+
-- four siblings in the same function.
|
|
35
|
+
-- ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
CREATE OR REPLACE FUNCTION public.unit_bootstrap(p_tenant_id uuid, p_device_id text DEFAULT NULL::text)
|
|
38
|
+
RETURNS jsonb
|
|
39
|
+
LANGUAGE sql
|
|
40
|
+
STABLE
|
|
41
|
+
SET search_path TO 'public', 'pg_temp'
|
|
42
|
+
AS $function$
|
|
43
|
+
SELECT jsonb_build_object(
|
|
44
|
+
'mode', COALESCE((
|
|
45
|
+
SELECT t.unit_scoping_enabled FROM public.tenants t WHERE t.id = p_tenant_id
|
|
46
|
+
), false),
|
|
47
|
+
'units', COALESCE((
|
|
48
|
+
SELECT jsonb_agg(to_jsonb(l) ORDER BY l.is_headquarters DESC NULLS LAST, l.name)
|
|
49
|
+
FROM public.v_units l WHERE l.tenant_id = p_tenant_id
|
|
50
|
+
), '[]'::jsonb),
|
|
51
|
+
-- The tenant filter (015). Without it a binding from another workspace
|
|
52
|
+
-- comes back here and the switcher narrows to a unit this tenant does not
|
|
53
|
+
-- have, which reads on screen as "this account has no units".
|
|
54
|
+
'bindings', COALESCE((
|
|
55
|
+
SELECT jsonb_agg(jsonb_build_object('location_id', lm.location_id, 'role', lm.role))
|
|
56
|
+
FROM public.v_unit_members lm
|
|
57
|
+
WHERE lm.user_id = (SELECT auth.uid())
|
|
58
|
+
AND lm.tenant_id = p_tenant_id
|
|
59
|
+
), '[]'::jsonb),
|
|
60
|
+
'scoped', COALESCE((
|
|
61
|
+
SELECT jsonb_object_agg(sr.resource_table, jsonb_build_object(
|
|
62
|
+
'unit_column', sr.unit_column, 'shareable', sr.shareable))
|
|
63
|
+
FROM public.scoped_resources sr
|
|
64
|
+
), '{}'::jsonb),
|
|
65
|
+
'device_pin', (
|
|
66
|
+
SELECT p.unit_id FROM public.user_unit_prefs p
|
|
67
|
+
WHERE p.user_id = (SELECT auth.uid()) AND p.tenant_id = p_tenant_id AND p.device_id = p_device_id
|
|
68
|
+
),
|
|
69
|
+
'device_pin_set', EXISTS (
|
|
70
|
+
SELECT 1 FROM public.user_unit_prefs p
|
|
71
|
+
WHERE p.user_id = (SELECT auth.uid()) AND p.tenant_id = p_tenant_id AND p.device_id = p_device_id
|
|
72
|
+
),
|
|
73
|
+
'account_pin', (
|
|
74
|
+
SELECT p.unit_id FROM public.user_unit_prefs p
|
|
75
|
+
WHERE p.user_id = (SELECT auth.uid()) AND p.tenant_id = p_tenant_id
|
|
76
|
+
ORDER BY p.updated_at DESC LIMIT 1
|
|
77
|
+
),
|
|
78
|
+
'account_pin_set', EXISTS (
|
|
79
|
+
SELECT 1 FROM public.user_unit_prefs p
|
|
80
|
+
WHERE p.user_id = (SELECT auth.uid()) AND p.tenant_id = p_tenant_id
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
$function$;
|
|
84
|
+
|
|
85
|
+
COMMENT ON FUNCTION public.unit_bootstrap(uuid, text) IS
|
|
86
|
+
'Everything the unit switcher needs for ONE workspace, in one round trip. Every clause is scoped to p_tenant_id — including bindings, which was not, and leaked a unit of another account into this one''s menu (015).';
|