@fayz-ai/db 0.12.0 → 0.13.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 +49 -0
- package/migrations/010_migration_ledger.sql +11 -1
- package/migrations/040_resource_grants.sql +2 -2
- package/migrations/041_scoped_columns.sql +2 -2
- package/migrations/042_unit_scope_policies.sql +1 -1
- package/migrations/043_view_invoker.sql +12 -3
- package/migrations/044_unit_member_facts.sql +1 -1
- package/migrations/045_unit_entry.sql +1 -1
- package/migrations/046_membership_visible_to_members.sql +1 -1
- package/migrations/047_tasks.sql +4 -4
- package/migrations/048_every_login_is_a_person.sql +1 -1
- package/migrations/049_bookable_people.sql +1 -1
- package/migrations/050_authz_core.sql +230 -0
- package/migrations/051_authz_helpers.sql +330 -0
- package/migrations/052_authz_fanout.sql +253 -0
- package/migrations/053_authz_catalog_templates.sql +402 -0
- package/migrations/060_tenancy_seam.sql +132 -0
- package/migrations/061_create_tenant.sql +125 -0
- package/migrations/062_invitations.sql +329 -0
- package/migrations/063_legacy_tenancy_retired.sql +568 -0
- package/migrations/064_agent_guard_authz.sql +118 -0
- package/migrations/064b_supersede_unit_scope.sql +205 -0
- package/migrations/065_scaffold_sweep.sql +139 -0
- package/migrations/066_shadow_report.sql +122 -0
- package/migrations/070_permission_catalog.sql +205 -0
- package/migrations/071_v1_permission_map.sql +266 -0
- package/migrations/072_role_rpcs.sql +126 -0
- package/migrations/073_legacy_rbac_retire.sql +471 -0
- package/migrations/075_company_legal_entities.sql +169 -0
- package/migrations/076_tenant_settings.sql +251 -0
- package/migrations/080_people_roles.sql +269 -0
- package/migrations/081_people_identity.sql +271 -0
- package/migrations/082_people_membership_link.sql +142 -0
- package/migrations/083_people_data_migration.sql +75 -0
- package/migrations/084_people_registrations_tokens.sql +282 -0
- package/migrations/085_catalog_products.sql +333 -0
- package/migrations/086_catalog_pricing.sql +194 -0
- package/migrations/087_catalog_packages.sql +134 -0
- package/migrations/090_orders_venda.sql +166 -0
- package/migrations/091_orders_scaffold.sql +92 -0
- package/migrations/095_config_field_rules.sql +261 -0
- package/migrations/096_audit_append_only.sql +256 -0
- package/migrations/097_sequences_authorized.sql +111 -0
- package/migrations/098_holidays.sql +80 -0
- package/migrations/099_access_grants.sql +293 -0
- package/migrations/100_migration_schema.sql +217 -0
- package/migrations/101_migration_allowlist.sql +202 -0
- package/migrations/102_migration_rpcs.sql +933 -0
- package/migrations/103_migration_reconcile_ponr.sql +132 -0
- package/migrations/105_policies_to_authenticated.sql +71 -0
- package/migrations/106_function_search_path.sql +55 -0
- package/migrations/107_core_table_prefix.sql +361 -0
- package/migrations/108_authz_write_guards.sql +149 -0
- package/migrations/109_catalog_migration_promoted_columns.sql +126 -0
- package/migrations/110_people_kind_vocabulary.sql +120 -0
- package/migrations/111_migration_excluded_is_a_decision.sql +259 -0
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +132 -0
- package/migrations/112_money_precision_standard.sql +132 -0
- package/migrations/113_addresses_units_companies.sql +114 -0
- package/migrations/114_migrations_ledger_immutable.sql +65 -0
- package/migrations/115_registration_honours_field_rules.sql +172 -0
- package/migrations/116_field_rules_check_needs_a_tenant.sql +72 -0
- package/migrations/117_authz_reconciliation_schedule.sql +82 -0
- package/migrations/118_migration_catalog_writers.sql +450 -0
- package/migrations/119_migration_config_writers.sql +193 -0
- package/migrations/120_migration_facet_writer.sql +139 -0
- package/migrations/121_migration_financial_writers.sql +294 -0
- package/migrations/122_migration_inventory_writers.sql +245 -0
- package/migrations/123_migration_order_item_writer.sql +144 -0
- package/migrations/124_migration_ledger_writers.sql +170 -0
- package/migrations/125_migration_authz_people_writers.sql +205 -0
- package/migrations/126_owner_scoping_is_configuration.sql +247 -0
- package/migrations/127_scaffold_audit_hook.sql +98 -0
- package/migrations/128_shadow_tables_enforced.sql +93 -0
- package/migrations/129_order_kind_terminal_states.sql +57 -0
- package/migrations/130_every_login_is_a_person.sql +196 -0
- package/migrations/131_migration_invoice_split.sql +127 -0
- package/migrations/132_audit_prune_scheduled.sql +28 -0
- package/migrations/133_unit_is_a_session_property.sql +155 -0
- package/migrations/134_field_rules_on_read.sql +341 -0
- package/migrations/135_distribution_polarity.sql +158 -0
- package/migrations/136_unit_id_means_locality.sql +358 -0
- package/migrations/137_vendor_named_spine_objects.sql +126 -0
- package/migrations/138_grants_inherit_down_the_tree.sql +178 -0
- package/migrations/139_distributable_is_a_scaffold_option.sql +109 -0
- package/migrations/140_masking_is_a_scaffold_option.sql +85 -0
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +192 -0
- package/migrations/142_a_person_has_addresses.sql +212 -0
- package/migrations/143_a_professional_has_a_rate.sql +75 -0
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +57 -0
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +123 -0
- package/migrations/146_products_stock_has_one_writer.sql +67 -0
- package/migrations/147_every_archetype_has_a_kind_registry.sql +388 -0
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +154 -0
- package/migrations/149_the_event_log_gets_a_consumer.sql +331 -0
- package/migrations/150_the_app_is_configured_in_the_database.sql +285 -0
- package/migrations/151_stock_balance_is_an_archetype.sql +144 -0
- package/migrations/152_a_file_is_an_archetype.sql +222 -0
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +199 -0
- package/migrations/154_the_plan_is_a_ceiling.sql +258 -0
- package/migrations/155_a_tenant_can_leave.sql +271 -0
- package/migrations/156_the_service_door_has_a_server_side.sql +335 -0
- package/migrations/157_nobody_creates_objects_in_public.sql +74 -0
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +153 -0
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +132 -0
- package/migrations/160_the_vertical_decides_what_starts_on.sql +246 -0
- package/migrations/161_a_tenant_leaves_with_its_data.sql +363 -0
- package/migrations/162_every_name_has_one_creator_for_real.sql +126 -0
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +184 -0
- package/migrations/164_the_sequence_table_joins_the_register.sql +26 -0
- package/migrations/165_a_view_is_not_a_way_in.sql +90 -0
- package/migrations/166_the_platform_billing_leftovers_go.sql +72 -0
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +112 -0
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +33 -0
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +45 -0
- package/package.json +3 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "The canonical schema (#279, ADR 0022). ONE schema on every cluster: the spine plus every plugin that has passed the capability gate. The pool profiles in cli/pool-profiles/ stop deciding what a cluster HAS and become activation defaults — what a new tenant of that industry gets turned on, which is configuration and lives in the database (ADR 0023, migration 150). What varies per cluster is data, never DDL. A plugin joins `plugins` below when its gate is green; until then it is in `pending`, with the reason, and it exists in no cluster at all.",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"plugins": [
|
|
5
|
+
{
|
|
6
|
+
"id": "financial",
|
|
7
|
+
"dir": "plugins/plugin-financial/src/migrations",
|
|
8
|
+
"gate": "capability",
|
|
9
|
+
"$note": "core-A. In three of the four live pools already."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "inventory",
|
|
13
|
+
"dir": "plugins/plugin-inventory/src/migrations",
|
|
14
|
+
"gate": "capability",
|
|
15
|
+
"$note": "core-A. Sole writer of the stock archetype since #276."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "tasks",
|
|
19
|
+
"dir": "plugins/plugin-tasks/src/migrations",
|
|
20
|
+
"gate": "enforced",
|
|
21
|
+
"$note": "The only plugin at ENFORCED — the reference implementation."
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"pending": [
|
|
25
|
+
{ "id": "agenda", "reason": "Owns the species of the Compromisso archetype; the Google Calendar integration inside it has to come out as an addon first (#285's rule, ADR 0024)." },
|
|
26
|
+
{ "id": "crm", "reason": "Installed in three pools; capability gate not green (seed + capability tests missing)." },
|
|
27
|
+
{ "id": "forms", "reason": "Creates public.documents, which the base owns since 152 — the guarded CREATE has to go (#282)." },
|
|
28
|
+
{ "id": "marketing", "reason": "Capability gate not green." },
|
|
29
|
+
{ "id": "menu", "reason": "Nine tables of its own and no capability suite yet." },
|
|
30
|
+
{ "id": "shop", "reason": "Its data lives in packages/shop, not in the plugin, and it still keeps a second stock book (#276's remaining half)." },
|
|
31
|
+
{ "id": "courses", "reason": "Its data lives in packages/courses, not in the plugin (#285)." },
|
|
32
|
+
{ "id": "conversations", "reason": "Capability gate not green." },
|
|
33
|
+
{ "id": "automations", "reason": "Creates public.event_log, which the base owns; the copy is a guarded no-op that should go." },
|
|
34
|
+
{ "id": "blog", "reason": "Capability gate not green." },
|
|
35
|
+
{ "id": "reputation", "reason": "Capability gate not green." },
|
|
36
|
+
{ "id": "scribe", "reason": "Capability gate not green." },
|
|
37
|
+
{ "id": "banking-br", "reason": "Addon of financial; capability gate not green." },
|
|
38
|
+
{ "id": "admin", "reason": "No data of its own — the base absorbed what it was for (#274 rule 1)." },
|
|
39
|
+
{ "id": "auth", "reason": "No data of its own — same." },
|
|
40
|
+
{ "id": "orders", "reason": "The Venda is a base archetype; nothing is left for a plugin to own." },
|
|
41
|
+
{ "id": "dashboard", "reason": "A mechanism other plugins feed; no capability suite yet." },
|
|
42
|
+
{ "id": "reports", "reason": "Same." },
|
|
43
|
+
{ "id": "payments", "reason": "Never built." },
|
|
44
|
+
{ "id": "sites", "reason": "Never built." },
|
|
45
|
+
{ "id": "linkinbio", "reason": "Never built." },
|
|
46
|
+
{ "id": "tables", "reason": "Never built; will own the restaurant floor when it is." },
|
|
47
|
+
{ "id": "agent-runtime", "reason": "A library, not a plugin — it is base (rodada 3 Q1)." }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -33,4 +33,14 @@ ALTER TABLE public._migrations ENABLE ROW LEVEL SECURITY;
|
|
|
33
33
|
-- service_role (which bypasses RLS anyway). No policies = no anon/authenticated access.
|
|
34
34
|
REVOKE ALL ON public._migrations FROM anon, authenticated;
|
|
35
35
|
GRANT ALL ON public._migrations TO service_role;
|
|
36
|
-
|
|
36
|
+
-- The sequence, WHATEVER IT IS CALLED. A legacy pool whose table was renamed
|
|
37
|
+
-- ahead of 012 still owns fayz_migration_ledger_id_seq, and naming the new one
|
|
38
|
+
-- here would stop the chain twelve files before the migration that renames it.
|
|
39
|
+
-- pg_get_serial_sequence asks the column instead of guessing.
|
|
40
|
+
DO $$
|
|
41
|
+
DECLARE v_seq text := pg_get_serial_sequence('public._migrations', 'id');
|
|
42
|
+
BEGIN
|
|
43
|
+
IF v_seq IS NOT NULL THEN
|
|
44
|
+
EXECUTE format('GRANT USAGE, SELECT ON SEQUENCE %s TO service_role', v_seq);
|
|
45
|
+
END IF;
|
|
46
|
+
END $$;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 050_resource_grants.sql — the exception to the default, written down.
|
|
3
3
|
--
|
|
4
4
|
-- Unit scoping (039) answers "which units do I reach". It cannot answer the two
|
|
5
5
|
-- questions the product actually asks:
|
|
@@ -201,7 +201,7 @@ ALTER TABLE public.resource_grants ENABLE ROW LEVEL SECURITY;
|
|
|
201
201
|
|
|
202
202
|
-- ── Helpers ─────────────────────────────────────────────────────────────────
|
|
203
203
|
-- Same discipline as 039: one argument at most, and it is a CONSTANT baked into
|
|
204
|
-
-- the policy text by
|
|
204
|
+
-- the policy text by 052's generator, never a column. A helper taking the row's
|
|
205
205
|
-- own values would be correlated and re-run per row.
|
|
206
206
|
--
|
|
207
207
|
-- SECURITY DEFINER because they read `resource_grants`, whose own policies
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 051_scoped_columns.sql — the signature every record starts carrying.
|
|
3
3
|
--
|
|
4
4
|
-- 025 gave every core table a `created_by`, because "who registered this?" had
|
|
5
5
|
-- no answer anywhere. This file adds the other two halves of the same question:
|
|
@@ -170,7 +170,7 @@ END $$;
|
|
|
170
170
|
-- that actually turned unit scoping on, because that is the only set 042 ever
|
|
171
171
|
-- asks about — and it keeps this set small enough to hash once per query.
|
|
172
172
|
--
|
|
173
|
-
-- Two arguments, both constants baked into the policy text by
|
|
173
|
+
-- Two arguments, both constants baked into the policy text by 052's generator,
|
|
174
174
|
-- so it stays uncorrelated: one InitPlan per query, not one call per row.
|
|
175
175
|
CREATE OR REPLACE FUNCTION public.resource_mode_tenants(p_table text, p_mode text)
|
|
176
176
|
RETURNS SETOF uuid
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 052_unit_scope_policies.sql — the only file here that changes an answer.
|
|
3
3
|
--
|
|
4
4
|
-- WHY A RESTRICTIVE POLICY AND NOT A REWRITE. The obvious move is to extend the
|
|
5
5
|
-- canonical predicate — `tenant_id IN (SELECT public.user_tenant_ids()) AND …`
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 053_view_invoker.sql — the views that were never anybody's tenant.
|
|
3
3
|
--
|
|
4
4
|
-- THIS IS NOT PART OF UNIT SCOPING. It is a cross-tenant hole that predates it,
|
|
5
|
-
-- found while auditing what
|
|
5
|
+
-- found while auditing what 052's predicate would and would not reach, and it
|
|
6
6
|
-- has to close first or "the guarantee is the API's" is false on the day it
|
|
7
7
|
-- ships.
|
|
8
8
|
--
|
|
@@ -31,7 +31,16 @@
|
|
|
31
31
|
--
|
|
32
32
|
-- WHAT IS DELIBERATELY LEFT ALONE — the three anon-facing views:
|
|
33
33
|
--
|
|
34
|
-
-- public.plg_shop_catalog the storefront
|
|
34
|
+
-- public.plg_shop_catalog built for the storefront, but NOT what it
|
|
35
|
+
-- reads: the anon path goes at
|
|
36
|
+
-- plg_shop_products under the
|
|
37
|
+
-- `storefront_public_read` policy, which
|
|
38
|
+
-- demands the store header (0019/0020). This
|
|
39
|
+
-- view has no TypeScript caller at all. Kept
|
|
40
|
+
-- definer because that is what it would need
|
|
41
|
+
-- if anyone ever adopted it — and adopting it
|
|
42
|
+
-- would TRADE AWAY that policy, so it is not
|
|
43
|
+
-- the upgrade its shape suggests.
|
|
35
44
|
-- public.v_public_services public booking
|
|
36
45
|
-- public.v_public_blog_posts the public blog
|
|
37
46
|
--
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 054_unit_member_facts.sql — the two facts a unit roster is asked for.
|
|
3
3
|
--
|
|
4
4
|
-- "Quem trabalha aqui" is never only a list of names. The two questions that
|
|
5
5
|
-- follow it are always the same: since when, and are they actually using it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 055_unit_entry.sql — entrar no sistema já na unidade certa.
|
|
3
3
|
--
|
|
4
4
|
-- A fronteira de unidade existe desde a 039. O que faltava era a pergunta que
|
|
5
5
|
-- todo funcionário de rede responde toda manhã: "em qual loja eu estou?".
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 056_membership_visible_to_members.sql — a tela de equipe consegue ler a
|
|
3
3
|
-- própria equipe.
|
|
4
4
|
--
|
|
5
5
|
-- `members_select` da 001 é `user_id = auth.uid()`: cada pessoa enxerga **só a
|
package/migrations/047_tasks.sql
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 057_tasks.sql — public.tasks: the one table admin work lives in.
|
|
3
3
|
--
|
|
4
4
|
-- WHY THIS IS SPINE AND NOT A PLUGIN TABLE. plugin-tasks has carried
|
|
5
5
|
-- plg_tasks_tasks since its first release, with almost exactly these columns.
|
|
@@ -105,9 +105,9 @@ END $$;
|
|
|
105
105
|
-- 041 generates one of these per table it knows about, but it is applied and
|
|
106
106
|
-- checksum-frozen: it only ever iterates the list it shipped with. A table
|
|
107
107
|
-- added afterwards brings its own copy. Keep this body in step with the
|
|
108
|
-
-- generator in
|
|
108
|
+
-- generator in 051_scoped_columns.sql — including the fact that IT MUST NEVER
|
|
109
109
|
-- RAISE (SECURITY DEFINER RPCs insert here and a complaining BEFORE INSERT
|
|
110
|
-
-- trigger would take them down; validation is
|
|
110
|
+
-- trigger would take them down; validation is 052's WITH CHECK).
|
|
111
111
|
CREATE OR REPLACE FUNCTION public.stamp_scope_tasks() RETURNS trigger
|
|
112
112
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $body$
|
|
113
113
|
BEGIN
|
|
@@ -195,7 +195,7 @@ BEGIN
|
|
|
195
195
|
-- the factory setting, and re-running must not walk back an operator's choice.
|
|
196
196
|
END $$;
|
|
197
197
|
|
|
198
|
-
-- The restrictive policy, from
|
|
198
|
+
-- The restrictive policy, from 052's generator. Same reason as the trigger
|
|
199
199
|
-- above: 042 is frozen and only saw the registry as it stood then. The predicate
|
|
200
200
|
-- text is read straight out of the registry so the two stay identical in shape;
|
|
201
201
|
-- if 042 is ever superseded, this block must be revisited with it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 058_every_login_is_a_person.sql — quem entra no sistema é uma pessoa.
|
|
3
3
|
--
|
|
4
4
|
-- A 019 deixou o vínculo opcional nos dois sentidos, e documentou o porquê:
|
|
5
5
|
-- "login-only account (e.g. the owner who signed up) -> person_id NULL". Na
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- ============================================================================
|
|
2
|
-
--
|
|
2
|
+
-- 059_bookable_people.sql — trabalhar aqui não é atender cliente.
|
|
3
3
|
--
|
|
4
4
|
-- A 048 passou a criar uma ficha para todo login, e a agenda lista como
|
|
5
5
|
-- profissional toda pessoa de `kind = staff` que esteja ativa. O resultado
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 050_authz_core.sql — the authz core (ADR 0006 / PRD 01 / #133): schema §3.
|
|
3
|
+
--
|
|
4
|
+
-- Tenancy, RBAC catalog, membership, the precomputed unit scope and the generic
|
|
5
|
+
-- relation tuples of the Multi-Tenant Authorization & Data Scoping
|
|
6
|
+
-- Architecture v2. Exactly two authorization surfaces (user_unit_access,
|
|
7
|
+
-- resource_grants) plus the RBAC catalog; business tables carry tenant_id /
|
|
8
|
+
-- unit_id / owner_id and one RLS template (043) — nothing else decides access.
|
|
9
|
+
--
|
|
10
|
+
-- Where it lives (DECISIONS 2026-08-18): schema `app`, per the spec and the
|
|
11
|
+
-- issue's default until the platform session rules otherwise. `public.tenants`
|
|
12
|
+
-- stays the tenant table — every FK in the fleet already points at it — and
|
|
13
|
+
-- gains `kind`. Reads for UI go through public.v_authz_* (043); writes through
|
|
14
|
+
-- the RLS write policies below or the RPCs of #134.
|
|
15
|
+
--
|
|
16
|
+
-- Idempotent and replay-safe: IF NOT EXISTS / guarded DO blocks throughout;
|
|
17
|
+
-- policies are DROP IF EXISTS + CREATE. Nothing here touches tenant_members
|
|
18
|
+
-- or locations — retiring them is #134's migration, on top of this.
|
|
19
|
+
-- ============================================================================
|
|
20
|
+
|
|
21
|
+
CREATE SCHEMA IF NOT EXISTS app;
|
|
22
|
+
GRANT USAGE ON SCHEMA app TO authenticated, service_role;
|
|
23
|
+
-- anon deliberately gets nothing: the authz core has no anonymous surface.
|
|
24
|
+
|
|
25
|
+
-- ─────────────────────────────── Tenancy ───────────────────────────────
|
|
26
|
+
-- tenants(kind network|business): a network groups units under franchisee /
|
|
27
|
+
-- regional parent units; a business is one company. ADR 0002 maps V1's licence
|
|
28
|
+
-- to a tenant and V1's franchise mode to kind='network'.
|
|
29
|
+
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT 'business';
|
|
30
|
+
DO $$
|
|
31
|
+
BEGIN
|
|
32
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tenants_kind_check' AND conrelid = 'public.tenants'::regclass) THEN
|
|
33
|
+
ALTER TABLE public.tenants ADD CONSTRAINT tenants_kind_check CHECK (kind IN ('network', 'business'));
|
|
34
|
+
END IF;
|
|
35
|
+
END $$;
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS app.units (
|
|
38
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
39
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
40
|
+
kind text NOT NULL CHECK (kind IN ('hq', 'branch')),
|
|
41
|
+
parent_unit_id uuid, -- grouping: franchisee, region (subtree propagation)
|
|
42
|
+
name text NOT NULL,
|
|
43
|
+
slug text,
|
|
44
|
+
active boolean NOT NULL DEFAULT true,
|
|
45
|
+
company_legal_entity_id uuid, -- Empresa: public.company_legal_entities (CNPJ/CPF); FK added by 065 (#138)
|
|
46
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
47
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
48
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
49
|
+
PRIMARY KEY (id),
|
|
50
|
+
UNIQUE (tenant_id, id),
|
|
51
|
+
UNIQUE (tenant_id, slug),
|
|
52
|
+
FOREIGN KEY (tenant_id, parent_unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL,
|
|
53
|
+
CHECK (parent_unit_id IS DISTINCT FROM id)
|
|
54
|
+
);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS units_tenant_parent_idx ON app.units (tenant_id, parent_unit_id);
|
|
56
|
+
CREATE UNIQUE INDEX IF NOT EXISTS units_one_hq_per_tenant ON app.units (tenant_id) WHERE kind = 'hq';
|
|
57
|
+
DROP TRIGGER IF EXISTS units_updated_at ON app.units;
|
|
58
|
+
CREATE TRIGGER units_updated_at BEFORE UPDATE ON app.units
|
|
59
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
60
|
+
|
|
61
|
+
-- ─────────────────────────────── RBAC catalog ──────────────────────────
|
|
62
|
+
-- Global catalog: keys are '{plugin}.{action}', seeded by the platform and by
|
|
63
|
+
-- each plugin's declaredFeatures (#136). A key is a row, never DDL.
|
|
64
|
+
CREATE TABLE IF NOT EXISTS app.permissions (
|
|
65
|
+
key text PRIMARY KEY,
|
|
66
|
+
plugin text NOT NULL,
|
|
67
|
+
description text,
|
|
68
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
69
|
+
CHECK (key ~ '^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_-]*)+$')
|
|
70
|
+
);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS permissions_plugin_idx ON app.permissions (plugin);
|
|
72
|
+
|
|
73
|
+
-- Roles per tenant. is_system rows come from the vertical template (owner,
|
|
74
|
+
-- manager, reception, professional, …) and cannot be deleted; a tenant may add
|
|
75
|
+
-- its own ("kit por cargo" of a network is a tenant role, not a system one).
|
|
76
|
+
CREATE TABLE IF NOT EXISTS app.roles (
|
|
77
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
78
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
79
|
+
key text NOT NULL,
|
|
80
|
+
name text NOT NULL,
|
|
81
|
+
description text,
|
|
82
|
+
is_system boolean NOT NULL DEFAULT false,
|
|
83
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
84
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
85
|
+
PRIMARY KEY (tenant_id, id),
|
|
86
|
+
UNIQUE (id),
|
|
87
|
+
UNIQUE (tenant_id, key),
|
|
88
|
+
CHECK (key ~ '^[a-z][a-z0-9_-]*$')
|
|
89
|
+
);
|
|
90
|
+
DROP TRIGGER IF EXISTS roles_updated_at ON app.roles;
|
|
91
|
+
CREATE TRIGGER roles_updated_at BEFORE UPDATE ON app.roles
|
|
92
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS app.role_permissions (
|
|
95
|
+
tenant_id uuid NOT NULL,
|
|
96
|
+
role_id uuid NOT NULL,
|
|
97
|
+
permission text NOT NULL REFERENCES app.permissions(key) ON DELETE CASCADE,
|
|
98
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
99
|
+
PRIMARY KEY (tenant_id, role_id, permission),
|
|
100
|
+
FOREIGN KEY (tenant_id, role_id) REFERENCES app.roles(tenant_id, id) ON DELETE CASCADE
|
|
101
|
+
);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS role_permissions_perm_idx ON app.role_permissions (tenant_id, permission);
|
|
103
|
+
|
|
104
|
+
-- ─────────────────────────────── Membership ────────────────────────────
|
|
105
|
+
-- One membership per (tenant, user): the base role, whether the user sees every
|
|
106
|
+
-- unit (franchisor / network owner), and whether it is active. person_id links
|
|
107
|
+
-- the login to the one Person of that human in the tenant (#137).
|
|
108
|
+
CREATE TABLE IF NOT EXISTS app.memberships (
|
|
109
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
110
|
+
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
111
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
112
|
+
role_id uuid NOT NULL,
|
|
113
|
+
all_units boolean NOT NULL DEFAULT false,
|
|
114
|
+
active boolean NOT NULL DEFAULT true,
|
|
115
|
+
person_id uuid,
|
|
116
|
+
invited_by uuid,
|
|
117
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
118
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
119
|
+
PRIMARY KEY (tenant_id, user_id),
|
|
120
|
+
UNIQUE (id),
|
|
121
|
+
FOREIGN KEY (tenant_id, role_id) REFERENCES app.roles(tenant_id, id)
|
|
122
|
+
);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS memberships_user_idx ON app.memberships (user_id) WHERE active;
|
|
124
|
+
CREATE INDEX IF NOT EXISTS memberships_role_idx ON app.memberships (tenant_id, role_id);
|
|
125
|
+
DROP TRIGGER IF EXISTS memberships_updated_at ON app.memberships;
|
|
126
|
+
CREATE TRIGGER memberships_updated_at BEFORE UPDATE ON app.memberships
|
|
127
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
128
|
+
|
|
129
|
+
-- Explicit units of a member (Toast/Square style) with an optional per-unit
|
|
130
|
+
-- role override — the effective role in that unit (§8).
|
|
131
|
+
CREATE TABLE IF NOT EXISTS app.membership_units (
|
|
132
|
+
tenant_id uuid NOT NULL,
|
|
133
|
+
user_id uuid NOT NULL,
|
|
134
|
+
unit_id uuid NOT NULL,
|
|
135
|
+
role_override_id uuid,
|
|
136
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
137
|
+
PRIMARY KEY (tenant_id, user_id, unit_id),
|
|
138
|
+
FOREIGN KEY (tenant_id, user_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE CASCADE,
|
|
139
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE,
|
|
140
|
+
FOREIGN KEY (tenant_id, role_override_id) REFERENCES app.roles(tenant_id, id)
|
|
141
|
+
);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS membership_units_unit_idx ON app.membership_units (tenant_id, unit_id);
|
|
143
|
+
|
|
144
|
+
-- ─────────────── Precomputed scope (Salesforce-style share table) ───────
|
|
145
|
+
-- DERIVED. Maintained by the fan-out triggers (042) and the reconciliation job.
|
|
146
|
+
-- Never written by app code: no INSERT/UPDATE/DELETE grant to any API role.
|
|
147
|
+
CREATE TABLE IF NOT EXISTS app.user_unit_access (
|
|
148
|
+
tenant_id uuid NOT NULL,
|
|
149
|
+
user_id uuid NOT NULL,
|
|
150
|
+
unit_id uuid NOT NULL,
|
|
151
|
+
via text NOT NULL CHECK (via IN ('all_units', 'explicit', 'subtree')),
|
|
152
|
+
PRIMARY KEY (tenant_id, user_id, unit_id),
|
|
153
|
+
FOREIGN KEY (tenant_id, user_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE CASCADE,
|
|
154
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE CASCADE
|
|
155
|
+
);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS user_unit_access_unit_idx ON app.user_unit_access (tenant_id, unit_id);
|
|
157
|
+
|
|
158
|
+
-- Diff-log of every reconciliation run (§6). diff <> 0 outside a migration is
|
|
159
|
+
-- an incident, not a perf issue: v_authz_reconciliation_alerts (043) lists them.
|
|
160
|
+
CREATE TABLE IF NOT EXISTS app.authz_reconciliation_log (
|
|
161
|
+
id bigserial PRIMARY KEY,
|
|
162
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
163
|
+
run_at timestamptz NOT NULL DEFAULT now(),
|
|
164
|
+
source text NOT NULL CHECK (source IN ('migration', 'scheduled', 'manual', 'test')),
|
|
165
|
+
added integer NOT NULL DEFAULT 0,
|
|
166
|
+
removed integer NOT NULL DEFAULT 0,
|
|
167
|
+
details jsonb NOT NULL DEFAULT '[]'::jsonb
|
|
168
|
+
);
|
|
169
|
+
CREATE INDEX IF NOT EXISTS authz_reconciliation_log_tenant_idx ON app.authz_reconciliation_log (tenant_id, run_at DESC);
|
|
170
|
+
|
|
171
|
+
-- ─────────────── Generic relation tuples (ERPNext UserPermission / Zanzibar) ───
|
|
172
|
+
CREATE TABLE IF NOT EXISTS app.resource_grants (
|
|
173
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
174
|
+
resource_type text NOT NULL, -- plugin-namespaced: 'catalog.product', 'people.person', …
|
|
175
|
+
resource_id uuid NOT NULL,
|
|
176
|
+
relation text NOT NULL, -- 'available_in', 'owner', 'shared_with', 'visible_to', …
|
|
177
|
+
subject_type text NOT NULL CHECK (subject_type IN ('user', 'unit', 'role')),
|
|
178
|
+
subject_id uuid NOT NULL,
|
|
179
|
+
created_by uuid,
|
|
180
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
181
|
+
PRIMARY KEY (tenant_id, resource_type, resource_id, relation, subject_type, subject_id),
|
|
182
|
+
CHECK (resource_type ~ '^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$'),
|
|
183
|
+
CHECK (relation ~ '^[a-z][a-z0-9_]*$')
|
|
184
|
+
);
|
|
185
|
+
-- Reverse lookup: "what can this subject access?"
|
|
186
|
+
CREATE INDEX IF NOT EXISTS resource_grants_subject_idx
|
|
187
|
+
ON app.resource_grants (tenant_id, subject_type, subject_id, relation, resource_type);
|
|
188
|
+
|
|
189
|
+
-- ─────────────────────────────── Grants ────────────────────────────────
|
|
190
|
+
-- Reads for every API role that has a session; RLS (below) narrows to the
|
|
191
|
+
-- caller's tenant. Writes only where a policy exists to gate them.
|
|
192
|
+
GRANT SELECT ON app.units, app.permissions, app.roles, app.role_permissions,
|
|
193
|
+
app.memberships, app.membership_units, app.user_unit_access,
|
|
194
|
+
app.resource_grants, app.authz_reconciliation_log TO authenticated;
|
|
195
|
+
GRANT INSERT, UPDATE, DELETE ON app.units, app.roles, app.role_permissions,
|
|
196
|
+
app.memberships, app.membership_units, app.resource_grants TO authenticated;
|
|
197
|
+
GRANT ALL ON ALL TABLES IN SCHEMA app TO service_role;
|
|
198
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO service_role;
|
|
199
|
+
-- user_unit_access is derived: no API role may write it (the trigger functions
|
|
200
|
+
-- are SECURITY DEFINER and owned by the migration role).
|
|
201
|
+
REVOKE INSERT, UPDATE, DELETE ON app.user_unit_access FROM authenticated, anon;
|
|
202
|
+
REVOKE ALL ON app.permissions FROM anon;
|
|
203
|
+
REVOKE INSERT, UPDATE, DELETE ON app.permissions FROM authenticated;
|
|
204
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA app REVOKE ALL ON TABLES FROM anon;
|
|
205
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA app REVOKE EXECUTE ON FUNCTIONS FROM public, anon;
|
|
206
|
+
|
|
207
|
+
-- ─────────────────────────────── RLS on the authz tables ────────────────
|
|
208
|
+
-- The helpers of 041 are needed by the policies below; 041 runs after this file,
|
|
209
|
+
-- so the policies are (re)created at the end of 041 where the functions exist.
|
|
210
|
+
ALTER TABLE app.units ENABLE ROW LEVEL SECURITY;
|
|
211
|
+
ALTER TABLE app.units FORCE ROW LEVEL SECURITY;
|
|
212
|
+
ALTER TABLE app.permissions ENABLE ROW LEVEL SECURITY;
|
|
213
|
+
ALTER TABLE app.roles ENABLE ROW LEVEL SECURITY;
|
|
214
|
+
ALTER TABLE app.roles FORCE ROW LEVEL SECURITY;
|
|
215
|
+
ALTER TABLE app.role_permissions ENABLE ROW LEVEL SECURITY;
|
|
216
|
+
ALTER TABLE app.role_permissions FORCE ROW LEVEL SECURITY;
|
|
217
|
+
ALTER TABLE app.memberships ENABLE ROW LEVEL SECURITY;
|
|
218
|
+
ALTER TABLE app.memberships FORCE ROW LEVEL SECURITY;
|
|
219
|
+
ALTER TABLE app.membership_units ENABLE ROW LEVEL SECURITY;
|
|
220
|
+
ALTER TABLE app.membership_units FORCE ROW LEVEL SECURITY;
|
|
221
|
+
ALTER TABLE app.user_unit_access ENABLE ROW LEVEL SECURITY;
|
|
222
|
+
ALTER TABLE app.user_unit_access FORCE ROW LEVEL SECURITY;
|
|
223
|
+
ALTER TABLE app.resource_grants ENABLE ROW LEVEL SECURITY;
|
|
224
|
+
ALTER TABLE app.resource_grants FORCE ROW LEVEL SECURITY;
|
|
225
|
+
ALTER TABLE app.authz_reconciliation_log ENABLE ROW LEVEL SECURITY;
|
|
226
|
+
ALTER TABLE app.authz_reconciliation_log FORCE ROW LEVEL SECURITY;
|
|
227
|
+
|
|
228
|
+
-- The catalog is global and harmless to read: every signed-in user may list it.
|
|
229
|
+
DROP POLICY IF EXISTS permissions_select ON app.permissions;
|
|
230
|
+
CREATE POLICY permissions_select ON app.permissions FOR SELECT TO authenticated USING (true);
|