@fayz-ai/db 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/canonical-schema.json +106 -23
  2. package/dist/index.cjs +4 -4
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/schema/spine.d.ts +42 -37
  9. package/dist/schema/spine.d.ts.map +1 -1
  10. package/migrations/000_baseline.sql +29667 -0
  11. package/migrations/001_the_tenant_has_apps.sql +314 -0
  12. package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
  13. package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
  14. package/migrations/004_the_projections_get_a_successor.sql +312 -0
  15. package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
  16. package/migrations/006_the_helpers_read_the_source.sql +153 -0
  17. package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
  18. package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
  19. package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
  20. package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
  21. package/migrations/011_leadcontrol_joins_the_family.sql +116 -0
  22. package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
  23. package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
  24. package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
  25. package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
  26. package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
  27. package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
  28. package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
  29. package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
  30. package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
  31. package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
  32. package/package.json +1 -1
  33. package/migrations/0000_legacy_quarantine.sql +0 -86
  34. package/migrations/0000b_straggler_sweep.sql +0 -37
  35. package/migrations/000_core_v1_convert.sql +0 -343
  36. package/migrations/001_core.sql +0 -372
  37. package/migrations/002_rls_user_tenant_ids.sql +0 -64
  38. package/migrations/003_invitations.sql +0 -17
  39. package/migrations/004_archetypes.sql +0 -343
  40. package/migrations/005_locations_archetype.sql +0 -18
  41. package/migrations/006_archetype_rls.sql +0 -74
  42. package/migrations/007_archetype_grants.sql +0 -4
  43. package/migrations/008_grants.sql +0 -5
  44. package/migrations/009_anon_hardening.sql +0 -19
  45. package/migrations/010_migration_ledger.sql +0 -46
  46. package/migrations/011_anon_write_revoke.sql +0 -12
  47. package/migrations/012_rename_migration_ledger.sql +0 -61
  48. package/migrations/013_sequences_rls.sql +0 -22
  49. package/migrations/014_plan_entitlements.sql +0 -28
  50. package/migrations/015_agent_guard.sql +0 -108
  51. package/migrations/016_agent_guard_actor.sql +0 -102
  52. package/migrations/017_core_addresses.sql +0 -115
  53. package/migrations/018_global_search.sql +0 -482
  54. package/migrations/019_tenant_members_person_id.sql +0 -20
  55. package/migrations/020_fayz_projects.sql +0 -63
  56. package/migrations/021_tenant_roles.sql +0 -74
  57. package/migrations/022_invite_acceptance.sql +0 -91
  58. package/migrations/023_team_visibility.sql +0 -75
  59. package/migrations/024_analytics_engine.sql +0 -317
  60. package/migrations/025_created_by.sql +0 -75
  61. package/migrations/026_audit_trail.sql +0 -73
  62. package/migrations/027_domain_events.sql +0 -266
  63. package/migrations/028_tenant_scoped_token.sql +0 -127
  64. package/migrations/029_connections.sql +0 -186
  65. package/migrations/030_effect_idempotency.sql +0 -159
  66. package/migrations/031_sync_run_message.sql +0 -39
  67. package/migrations/032_connection_secrets.sql +0 -227
  68. package/migrations/033_sync_schedule.sql +0 -651
  69. package/migrations/034_custom_fields.sql +0 -55
  70. package/migrations/035_field_registry.sql +0 -148
  71. package/migrations/036_analytics_run_batch.sql +0 -84
  72. package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
  73. package/migrations/038_onboarding_responses.sql +0 -103
  74. package/migrations/039_unit_tree.sql +0 -270
  75. package/migrations/040_resource_grants.sql +0 -474
  76. package/migrations/041_scoped_columns.sql +0 -192
  77. package/migrations/042_unit_scope_policies.sql +0 -145
  78. package/migrations/043_view_invoker.sql +0 -90
  79. package/migrations/044_unit_member_facts.sql +0 -47
  80. package/migrations/045_unit_entry.sql +0 -236
  81. package/migrations/046_membership_visible_to_members.sql +0 -85
  82. package/migrations/047_tasks.sql +0 -266
  83. package/migrations/048_every_login_is_a_person.sql +0 -190
  84. package/migrations/049_bookable_people.sql +0 -126
  85. package/migrations/050_authz_core.sql +0 -230
  86. package/migrations/051_authz_helpers.sql +0 -330
  87. package/migrations/052_authz_fanout.sql +0 -253
  88. package/migrations/053_authz_catalog_templates.sql +0 -402
  89. package/migrations/060_tenancy_seam.sql +0 -132
  90. package/migrations/061_create_tenant.sql +0 -125
  91. package/migrations/062_invitations.sql +0 -329
  92. package/migrations/063_legacy_tenancy_retired.sql +0 -568
  93. package/migrations/064_agent_guard_authz.sql +0 -118
  94. package/migrations/064b_supersede_unit_scope.sql +0 -205
  95. package/migrations/065_scaffold_sweep.sql +0 -139
  96. package/migrations/066_shadow_report.sql +0 -122
  97. package/migrations/070_permission_catalog.sql +0 -205
  98. package/migrations/071_v1_permission_map.sql +0 -266
  99. package/migrations/072_role_rpcs.sql +0 -126
  100. package/migrations/073_legacy_rbac_retire.sql +0 -471
  101. package/migrations/075_company_legal_entities.sql +0 -169
  102. package/migrations/076_tenant_settings.sql +0 -251
  103. package/migrations/080_people_roles.sql +0 -269
  104. package/migrations/081_people_identity.sql +0 -271
  105. package/migrations/082_people_membership_link.sql +0 -142
  106. package/migrations/083_people_data_migration.sql +0 -75
  107. package/migrations/084_people_registrations_tokens.sql +0 -282
  108. package/migrations/085_catalog_products.sql +0 -333
  109. package/migrations/086_catalog_pricing.sql +0 -194
  110. package/migrations/087_catalog_packages.sql +0 -134
  111. package/migrations/090_orders_venda.sql +0 -166
  112. package/migrations/091_orders_scaffold.sql +0 -92
  113. package/migrations/095_config_field_rules.sql +0 -261
  114. package/migrations/096_audit_append_only.sql +0 -256
  115. package/migrations/097_sequences_authorized.sql +0 -111
  116. package/migrations/098_holidays.sql +0 -80
  117. package/migrations/099_access_grants.sql +0 -293
  118. package/migrations/100_migration_schema.sql +0 -217
  119. package/migrations/101_migration_allowlist.sql +0 -202
  120. package/migrations/102_migration_rpcs.sql +0 -933
  121. package/migrations/103_migration_reconcile_ponr.sql +0 -132
  122. package/migrations/105_policies_to_authenticated.sql +0 -71
  123. package/migrations/106_function_search_path.sql +0 -55
  124. package/migrations/107_core_table_prefix.sql +0 -361
  125. package/migrations/108_authz_write_guards.sql +0 -149
  126. package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
  127. package/migrations/110_people_kind_vocabulary.sql +0 -120
  128. package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
  129. package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
  130. package/migrations/112_money_precision_standard.sql +0 -132
  131. package/migrations/113_addresses_units_companies.sql +0 -114
  132. package/migrations/114_migrations_ledger_immutable.sql +0 -65
  133. package/migrations/115_registration_honours_field_rules.sql +0 -172
  134. package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
  135. package/migrations/117_authz_reconciliation_schedule.sql +0 -82
  136. package/migrations/118_migration_catalog_writers.sql +0 -450
  137. package/migrations/119_migration_config_writers.sql +0 -193
  138. package/migrations/120_migration_facet_writer.sql +0 -139
  139. package/migrations/121_migration_financial_writers.sql +0 -294
  140. package/migrations/122_migration_inventory_writers.sql +0 -245
  141. package/migrations/123_migration_order_item_writer.sql +0 -144
  142. package/migrations/124_migration_ledger_writers.sql +0 -170
  143. package/migrations/125_migration_authz_people_writers.sql +0 -205
  144. package/migrations/126_owner_scoping_is_configuration.sql +0 -247
  145. package/migrations/127_scaffold_audit_hook.sql +0 -98
  146. package/migrations/128_shadow_tables_enforced.sql +0 -93
  147. package/migrations/129_order_kind_terminal_states.sql +0 -57
  148. package/migrations/130_every_login_is_a_person.sql +0 -196
  149. package/migrations/131_migration_invoice_split.sql +0 -127
  150. package/migrations/132_audit_prune_scheduled.sql +0 -28
  151. package/migrations/133_unit_is_a_session_property.sql +0 -155
  152. package/migrations/134_field_rules_on_read.sql +0 -341
  153. package/migrations/135_distribution_polarity.sql +0 -158
  154. package/migrations/136_unit_id_means_locality.sql +0 -358
  155. package/migrations/137_vendor_named_spine_objects.sql +0 -126
  156. package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
  157. package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
  158. package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
  159. package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
  160. package/migrations/142_a_person_has_addresses.sql +0 -212
  161. package/migrations/143_a_professional_has_a_rate.sql +0 -75
  162. package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
  163. package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
  164. package/migrations/146_products_stock_has_one_writer.sql +0 -67
  165. package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
  166. package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
  167. package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
  168. package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
  169. package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
  170. package/migrations/152_a_file_is_an_archetype.sql +0 -222
  171. package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
  172. package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
  173. package/migrations/155_a_tenant_can_leave.sql +0 -271
  174. package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
  175. package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
  176. package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
  177. package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
  178. package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
  179. package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
  180. package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
  181. package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
  182. package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
  183. package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
  184. package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
  185. package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
  186. package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
  187. package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
@@ -1,388 +0,0 @@
1
- -- ============================================================================
2
- -- 147_every_archetype_has_a_kind_registry.sql — a species has a declared owner,
3
- -- and the database is what says so.
4
- --
5
- -- Phase 1, ADR 0017 + the round-3 decision. The Venda has had a registry since
6
- -- 090: `public.order_kinds`, a natural-key text PK with the owning plugin and
7
- -- the lifecycle it allows. Every other archetype had `kind text NOT NULL` and
8
- -- nothing else — any value entered, and nobody could say whose it was. With
9
- -- closed and third-party plugins that stops being a hypothesis: two plugins
10
- -- reach for `reservation`, both write, and the row belongs to no lifecycle.
11
- --
12
- -- WHY TEXT AND NOT A SURROGATE ID. The species is written literally by plugin
13
- -- code (`kind = 'appointment'`), read by RLS predicates, carried in the plugin
14
- -- manifest, the API, the event log and the support dump. An integer id would
15
- -- force a lookup on every insert and a join on every read, and would make a log
16
- -- line unreadable, in exchange for nothing: the cardinality is dozens, and the
17
- -- name is a public contract identifier that cannot be renamed freely either way.
18
- -- The format CHECK is what makes it an identifier rather than free user text.
19
- --
20
- -- WHY A REAL FK AND NOT A TRIGGER. 090 guarded `orders.kind` with a BEFORE
21
- -- trigger because the registry is seeded by plugin migrations that run after the
22
- -- core, and legacy rows could arrive before the row existed. That reasoning is
23
- -- retired: V1 rows that do not fit go to `migration.quarantine` — a human
24
- -- decision — and never a weakened constraint. A FK is cheaper than a trigger,
25
- -- it is declarative, and on uninstall it refuses to drop a species while rows
26
- -- still use it, which is exactly the question we want asked out loud.
27
- --
28
- -- WHAT `status` IS FOR. Live pools already hold values written before any
29
- -- registry existed. Refusing them would break writing; inventing an owner for
30
- -- them would be a lie. They are ADOPTED: registered so the FK can be created and
31
- -- the data keeps working, marked so the census can list what still has no owner.
32
- -- `declared` is a plugin saying "mine"; `adopted` is the database saying "this
33
- -- was here and nobody has claimed it".
34
- --
35
- -- PEOPLE NEEDS TWO REGISTRIES, and that is not duplication. `person_roles.role`
36
- -- is the canonical hat (customer, professional, employee, contact, supplier,
37
- -- lead) and `people.kind` is the denormalised primary-role mirror, whose
38
- -- vocabulary is deliberately WIDER: 080's `person_kind_to_role` maps 'staff' to
39
- -- employee and 'vendor' to supplier, and 110 added 'client' as a synonym of
40
- -- customer. So the mirror gets its own registry carrying the mapping AS DATA —
41
- -- which retires the hard-coded CASE and lets a vertical register 'patient'
42
- -- instead of resolving to no role at all (the shape of #204).
43
- --
44
- -- Idempotent and replay-safe: every object guarded, every backfill a no-op on
45
- -- a second pass, every FK created only when absent.
46
- -- ============================================================================
47
-
48
- -- ─────────────────────────────────────────────────────────────────────────
49
- -- §1 The registry shape, applied five times
50
- --
51
- -- One table per archetype rather than one table with an (archetype, kind) key:
52
- -- a composite key would force every archetype to carry a constant discriminator
53
- -- column just so the FK could reference it. Five small tables of identical shape
54
- -- keep the FK a one-liner and keep each registry readable on its own.
55
- -- ─────────────────────────────────────────────────────────────────────────
56
-
57
- DO $$
58
- DECLARE
59
- v_table text;
60
- BEGIN
61
- FOREACH v_table IN ARRAY ARRAY['person_role_kinds', 'person_kinds', 'category_kinds', 'appointment_kinds', 'schedule_kinds']
62
- LOOP
63
- EXECUTE format($ddl$
64
- CREATE TABLE IF NOT EXISTS public.%I (
65
- kind text PRIMARY KEY,
66
- plugin text NOT NULL,
67
- description text,
68
- statuses text[],
69
- transitions jsonb,
70
- status text NOT NULL DEFAULT 'declared',
71
- created_at timestamptz NOT NULL DEFAULT now(),
72
- updated_at timestamptz NOT NULL DEFAULT now(),
73
- CONSTRAINT %I CHECK (kind ~ '^[a-z][a-z0-9_]*$'),
74
- CONSTRAINT %I CHECK (status IN ('declared', 'adopted'))
75
- )$ddl$, v_table, v_table || '_format', v_table || '_status');
76
-
77
- EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', v_table);
78
- EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', v_table || '_select', v_table);
79
- EXECUTE format($pol$
80
- CREATE POLICY %I ON public.%I FOR SELECT TO anon, authenticated, service_role USING (true)
81
- $pol$, v_table || '_select', v_table);
82
- EXECUTE format('GRANT SELECT ON public.%I TO anon, authenticated, service_role', v_table);
83
- EXECUTE format('GRANT ALL ON public.%I TO service_role', v_table);
84
- EXECUTE format('REVOKE INSERT, UPDATE, DELETE ON public.%I FROM anon, authenticated', v_table);
85
-
86
- EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', v_table || '_updated_at', v_table);
87
- EXECUTE format($trg$
88
- CREATE TRIGGER %I BEFORE UPDATE ON public.%I
89
- FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()
90
- $trg$, v_table || '_updated_at', v_table);
91
- END LOOP;
92
- END $$;
93
-
94
- -- `order_kinds` predates this file and already carries kind/plugin/description/
95
- -- statuses/transitions; it only lacks the status column, so the census can speak
96
- -- about all six registries in one sentence.
97
- ALTER TABLE public.order_kinds ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'declared';
98
- DO $$
99
- BEGIN
100
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'order_kinds_status') THEN
101
- ALTER TABLE public.order_kinds ADD CONSTRAINT order_kinds_status CHECK (status IN ('declared', 'adopted'));
102
- END IF;
103
- END $$;
104
-
105
- -- The mirror registry carries the map that 080 hard-coded in a CASE.
106
- ALTER TABLE public.person_kinds ADD COLUMN IF NOT EXISTS maps_to_role text;
107
-
108
- COMMENT ON TABLE public.person_role_kinds IS
109
- 'The species registry of the Person hat: which roles exist and who declared them (147). person_roles.role has a FK to this.';
110
- COMMENT ON TABLE public.person_kinds IS
111
- 'The species registry of people.kind — the denormalised primary-role mirror, whose vocabulary is wider than the roles themselves. maps_to_role is 080''s CASE turned into data (147).';
112
- COMMENT ON TABLE public.category_kinds IS
113
- 'The species registry of the Category archetype (147).';
114
- COMMENT ON TABLE public.appointment_kinds IS
115
- 'The species registry of the Compromisso archetype: appointment, table reservation, shift, class — each declared by the plugin that owns its lifecycle (147).';
116
- COMMENT ON TABLE public.schedule_kinds IS
117
- 'The species registry of the Escala archetype (147).';
118
- COMMENT ON COLUMN public.person_kinds.maps_to_role IS
119
- 'Which canonical role this mirror value resolves to. NULL means the vertical registered a kind with no role equivalent — legible, unlike the NULL that person_kind_to_role used to return for it.';
120
-
121
- -- ─────────────────────────────────────────────────────────────────────────
122
- -- §2 register_kind — one entry point, dispatching by archetype
123
- --
124
- -- Same semantics as 090's register_order_kind, which stays as a thin alias so
125
- -- no existing caller changes: idempotent upsert; NULL statuses AND NULL
126
- -- transitions on an existing row leave the lifecycle untouched, so a
127
- -- re-register with defaults never loosens what a plugin tightened.
128
- -- ─────────────────────────────────────────────────────────────────────────
129
-
130
- CREATE OR REPLACE FUNCTION public.kind_registry_table(p_archetype text)
131
- RETURNS text
132
- LANGUAGE sql IMMUTABLE PARALLEL SAFE
133
- AS $$
134
- SELECT CASE p_archetype
135
- WHEN 'order' THEN 'order_kinds'
136
- WHEN 'person_role' THEN 'person_role_kinds'
137
- WHEN 'person' THEN 'person_kinds'
138
- WHEN 'category' THEN 'category_kinds'
139
- WHEN 'appointment' THEN 'appointment_kinds'
140
- WHEN 'schedule' THEN 'schedule_kinds'
141
- END
142
- $$;
143
-
144
- COMMENT ON FUNCTION public.kind_registry_table(text) IS
145
- 'Which registry holds the species of this archetype. NULL for an archetype with no registry — register_kind turns that into an error rather than a silent no-op (147).';
146
-
147
- CREATE OR REPLACE FUNCTION public.register_kind(
148
- p_archetype text,
149
- p_kind text,
150
- p_plugin text,
151
- p_statuses text[] DEFAULT NULL,
152
- p_transitions jsonb DEFAULT NULL,
153
- p_description text DEFAULT NULL
154
- ) RETURNS void
155
- LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
156
- AS $$
157
- DECLARE
158
- v_table text := public.kind_registry_table(p_archetype);
159
- BEGIN
160
- IF v_table IS NULL THEN
161
- RAISE EXCEPTION 'register_kind: % is not an archetype with a species registry', p_archetype
162
- USING ERRCODE = '22023',
163
- HINT = 'The archetypes with registries are order, person_role, person, category, appointment, schedule.';
164
- END IF;
165
-
166
- IF v_table = 'order_kinds' THEN
167
- PERFORM public.register_order_kind(p_kind, p_plugin, p_statuses, p_transitions, p_description);
168
- RETURN;
169
- END IF;
170
-
171
- EXECUTE format($sql$
172
- INSERT INTO public.%I (kind, plugin, description, statuses, transitions, status)
173
- VALUES ($1, $2, $3, $4, $5, 'declared')
174
- ON CONFLICT (kind) DO UPDATE
175
- SET plugin = EXCLUDED.plugin,
176
- description = coalesce(EXCLUDED.description, public.%I.description),
177
- statuses = CASE WHEN $4 IS NULL AND $5 IS NULL THEN public.%I.statuses ELSE EXCLUDED.statuses END,
178
- transitions = CASE WHEN $4 IS NULL AND $5 IS NULL THEN public.%I.transitions ELSE EXCLUDED.transitions END,
179
- status = 'declared'
180
- $sql$, v_table, v_table, v_table, v_table)
181
- USING p_kind, p_plugin, p_description, p_statuses, p_transitions;
182
- END $$;
183
-
184
- COMMENT ON FUNCTION public.register_kind(text, text, text, text[], jsonb, text) IS
185
- 'A plugin declares a species as a ROW, never by DDL (147). Re-registering an adopted species is how it stops being ownerless: status returns to declared.';
186
-
187
- REVOKE ALL ON FUNCTION public.register_kind(text, text, text, text[], jsonb, text) FROM public, anon, authenticated;
188
- GRANT EXECUTE ON FUNCTION public.register_kind(text, text, text, text[], jsonb, text) TO service_role;
189
-
190
- -- Registering a Person mirror value also states which role it resolves to.
191
- CREATE OR REPLACE FUNCTION public.register_person_kind(
192
- p_kind text,
193
- p_plugin text,
194
- p_maps_to_role text DEFAULT NULL,
195
- p_description text DEFAULT NULL
196
- ) RETURNS void
197
- LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
198
- AS $$
199
- BEGIN
200
- PERFORM public.register_kind('person', p_kind, p_plugin, NULL, NULL, p_description);
201
- UPDATE public.person_kinds SET maps_to_role = p_maps_to_role WHERE kind = p_kind;
202
- END $$;
203
-
204
- REVOKE ALL ON FUNCTION public.register_person_kind(text, text, text, text) FROM public, anon, authenticated;
205
- GRANT EXECUTE ON FUNCTION public.register_person_kind(text, text, text, text) TO service_role;
206
-
207
- -- ─────────────────────────────────────────────────────────────────────────
208
- -- §3 What the base declares
209
- --
210
- -- The base owns the vocabulary that exists because the archetype exists. A
211
- -- plugin's own species is declared in the plugin's own migration — that is the
212
- -- rule this file is here to enforce, so nothing plugin-shaped is seeded here.
213
- -- ─────────────────────────────────────────────────────────────────────────
214
-
215
- SELECT public.register_kind('person_role', r.role, 'base', NULL, NULL, r.note)
216
- FROM (VALUES
217
- ('customer', 'Someone the tenant sells to'),
218
- ('professional', 'Someone who performs services and appears on the calendar'),
219
- ('employee', 'Someone on the payroll'),
220
- ('contact', 'Someone related to the tenant without buying or working'),
221
- ('supplier', 'Someone the tenant buys from'),
222
- ('lead', 'Someone not yet a customer')
223
- ) AS r(role, note);
224
-
225
- -- The mirror vocabulary, carrying 080's CASE and 110's synonym as rows.
226
- SELECT public.register_person_kind(k.kind, 'base', k.role, k.note)
227
- FROM (VALUES
228
- ('customer', 'customer', 'The mirror of the customer hat'),
229
- ('client', 'customer', 'Synonym of customer, kept because shipped plugin versions wrote it (110)'),
230
- ('staff', 'employee', 'Legacy spelling of the employee hat'),
231
- ('employee', 'employee', 'The mirror of the employee hat'),
232
- ('professional', 'professional', 'The mirror of the professional hat'),
233
- ('lead', 'lead', 'The mirror of the lead hat'),
234
- ('supplier', 'supplier', 'The mirror of the supplier hat'),
235
- ('vendor', 'supplier', 'Legacy spelling of the supplier hat'),
236
- ('contact', 'contact', 'The mirror of the contact hat')
237
- ) AS k(kind, role, note);
238
-
239
- -- The Category taxonomy the base itself ships. `service` and `good` mirror the
240
- -- catalog discriminator; a plugin's own taxonomy (the shop's `shop`, the menu's
241
- -- `menu_category`) is declared in that plugin's migration, which is the rule
242
- -- this file exists to make enforceable.
243
- SELECT public.register_kind('category', c.kind, 'base', NULL, NULL, c.note)
244
- FROM (VALUES
245
- ('service', 'Groups the services a tenant sells'),
246
- ('good', 'Groups the goods a tenant sells'),
247
- ('product', 'The V1 vocabulary the extractor writes by default (102) — a synonym of good, kept because the data says it')
248
- ) AS c(kind, note);
249
-
250
- -- NOT products.kind. That column is a deliberate closed binary (good|service,
251
- -- 085's CHECK) — the archetype's own discriminator, with `subkind` and
252
- -- `service_kind` as the open extension points. A registry there would offer an
253
- -- openness the catalog contract does not have.
254
-
255
- -- person_kind_to_role stops being a CASE and becomes a lookup. Same signature,
256
- -- same NULL-for-unknown contract, so every existing caller is unaffected —
257
- -- except that a vertical can now teach it a value by registering one.
258
- CREATE OR REPLACE FUNCTION public.person_kind_to_role(p_kind text)
259
- RETURNS text LANGUAGE sql STABLE PARALLEL SAFE
260
- SET search_path = ''
261
- AS $$
262
- SELECT k.maps_to_role FROM public.person_kinds k WHERE k.kind = p_kind
263
- $$;
264
-
265
- COMMENT ON FUNCTION public.person_kind_to_role(text) IS
266
- 'Which role a people.kind value resolves to. Since 147 this reads public.person_kinds instead of a hard-coded CASE — registering a kind is how a vertical teaches it (was: NULL, the shape of #204).';
267
-
268
- -- ─────────────────────────────────────────────────────────────────────────
269
- -- §4 Adoption — what is already in the data, made legible
270
- --
271
- -- Every distinct value present in a live pool and absent from its registry is
272
- -- registered as `adopted`, owner unknown. Nothing is refused and nothing is
273
- -- invented: the census in §6 is what turns this into work.
274
- -- ─────────────────────────────────────────────────────────────────────────
275
-
276
- CREATE OR REPLACE FUNCTION public.adopt_existing_kinds(p_archetype text, p_table text, p_column text)
277
- RETURNS integer
278
- LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
279
- AS $$
280
- DECLARE
281
- v_registry text := public.kind_registry_table(p_archetype);
282
- v_count integer := 0;
283
- BEGIN
284
- IF v_registry IS NULL OR to_regclass('public.' || quote_ident(p_table)) IS NULL THEN
285
- RETURN 0;
286
- END IF;
287
- EXECUTE format($sql$
288
- INSERT INTO public.%I (kind, plugin, description, status)
289
- SELECT DISTINCT t.%I, 'unattributed',
290
- 'Present in the data before any registry existed; adopted by 147 so the FK could be created. Needs an owner.',
291
- 'adopted'
292
- FROM public.%I t
293
- WHERE t.%I IS NOT NULL
294
- AND t.%I ~ '^[a-z][a-z0-9_]*$'
295
- ON CONFLICT (kind) DO NOTHING
296
- $sql$, v_registry, p_column, p_table, p_column, p_column);
297
- GET DIAGNOSTICS v_count = ROW_COUNT;
298
- RETURN v_count;
299
- END $$;
300
-
301
- COMMENT ON FUNCTION public.adopt_existing_kinds(text, text, text) IS
302
- 'Registers, as adopted, every species already present in the data and missing from its registry — so the FK in §5 can be added without refusing a live write or inventing an owner (147).';
303
-
304
- REVOKE ALL ON FUNCTION public.adopt_existing_kinds(text, text, text) FROM public, anon, authenticated;
305
-
306
- SELECT public.adopt_existing_kinds('person_role', 'person_roles', 'role');
307
- SELECT public.adopt_existing_kinds('person', 'people', 'kind');
308
- SELECT public.adopt_existing_kinds('category', 'categories', 'kind');
309
- SELECT public.adopt_existing_kinds('appointment', 'appointments', 'kind');
310
- SELECT public.adopt_existing_kinds('schedule', 'schedules', 'kind');
311
- SELECT public.adopt_existing_kinds('order', 'orders', 'kind');
312
-
313
- -- A value that does not even look like an identifier cannot be adopted, and must
314
- -- not be silently dropped either. It is a data question, and the machine that
315
- -- exists for data questions is the quarantine.
316
- DO $$
317
- DECLARE
318
- v_pair record;
319
- v_bad integer;
320
- BEGIN
321
- IF to_regclass('migration.quarantine') IS NULL THEN
322
- RETURN;
323
- END IF;
324
- FOR v_pair IN SELECT * FROM (VALUES
325
- ('person_roles', 'role'), ('people', 'kind'), ('categories', 'kind'),
326
- ('appointments', 'kind'), ('schedules', 'kind'), ('orders', 'kind')
327
- ) AS p(tbl, col)
328
- LOOP
329
- IF to_regclass('public.' || quote_ident(v_pair.tbl)) IS NULL THEN CONTINUE; END IF;
330
- EXECUTE format('SELECT count(*)::int FROM public.%I WHERE %I IS NOT NULL AND %I !~ ''^[a-z][a-z0-9_]*$''',
331
- v_pair.tbl, v_pair.col, v_pair.col) INTO v_bad;
332
- IF v_bad > 0 THEN
333
- RAISE EXCEPTION '147: %.% holds % value(s) that are not identifiers and cannot be registered', v_pair.tbl, v_pair.col, v_bad
334
- USING HINT = 'Quarantine or correct those rows first — a species outside the identifier format has no owner and no lifecycle.';
335
- END IF;
336
- END LOOP;
337
- END $$;
338
-
339
- -- ─────────────────────────────────────────────────────────────────────────
340
- -- §5 The FKs — the whole point of the file
341
- -- ─────────────────────────────────────────────────────────────────────────
342
-
343
- DO $$
344
- DECLARE
345
- v_fk record;
346
- BEGIN
347
- FOR v_fk IN SELECT * FROM (VALUES
348
- ('person_roles', 'role', 'person_role_kinds', 'person_roles_role_registered'),
349
- ('people', 'kind', 'person_kinds', 'people_kind_registered'),
350
- ('categories', 'kind', 'category_kinds', 'categories_kind_registered'),
351
- ('appointments', 'kind', 'appointment_kinds', 'appointments_kind_registered'),
352
- ('schedules', 'kind', 'schedule_kinds', 'schedules_kind_registered'),
353
- ('orders', 'kind', 'order_kinds', 'orders_kind_registered')
354
- ) AS f(tbl, col, registry, conname)
355
- LOOP
356
- IF to_regclass('public.' || quote_ident(v_fk.tbl)) IS NULL THEN CONTINUE; END IF;
357
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = v_fk.conname) THEN
358
- EXECUTE format('ALTER TABLE public.%I ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES public.%I(kind)',
359
- v_fk.tbl, v_fk.conname, v_fk.col, v_fk.registry);
360
- END IF;
361
- END LOOP;
362
- END $$;
363
-
364
- -- person_roles carried the vocabulary as a CHECK. The FK supersedes it: a CHECK
365
- -- can only be widened by a migration, which is precisely what made a vertical's
366
- -- 'patient' impossible to express.
367
- ALTER TABLE public.person_roles DROP CONSTRAINT IF EXISTS person_roles_role_check;
368
-
369
- COMMENT ON CONSTRAINT people_kind_registered ON public.people IS
370
- 'A species with no declared owner cannot be written (147). Registering one is a row in public.person_kinds, never DDL.';
371
-
372
- -- ─────────────────────────────────────────────────────────────────────────
373
- -- §6 The census — one view over all six registries
374
- -- ─────────────────────────────────────────────────────────────────────────
375
-
376
- CREATE OR REPLACE VIEW public.v_archetype_kinds
377
- WITH (security_invoker = true) AS
378
- SELECT 'order'::text AS archetype, kind, plugin, description, statuses, transitions, status FROM public.order_kinds
379
- UNION ALL SELECT 'person_role', kind, plugin, description, statuses, transitions, status FROM public.person_role_kinds
380
- UNION ALL SELECT 'person', kind, plugin, description, statuses, transitions, status FROM public.person_kinds
381
- UNION ALL SELECT 'category', kind, plugin, description, statuses, transitions, status FROM public.category_kinds
382
- UNION ALL SELECT 'appointment', kind, plugin, description, statuses, transitions, status FROM public.appointment_kinds
383
- UNION ALL SELECT 'schedule', kind, plugin, description, statuses, transitions, status FROM public.schedule_kinds;
384
-
385
- COMMENT ON VIEW public.v_archetype_kinds IS
386
- 'Every declared species of every archetype, with its owner (147). status = adopted is the backlog: a species the data already had and no plugin has claimed.';
387
-
388
- GRANT SELECT ON public.v_archetype_kinds TO anon, authenticated, service_role;
@@ -1,154 +0,0 @@
1
- -- ============================================================================
2
- -- 148_the_legacy_archetype_stops_being_writable.sql — one book, and the tables
3
- -- that carry a base name get a declared owner (#285).
4
- --
5
- -- Three cleanups the decisions already settled, in one file because they are the
6
- -- same shape: something in `public` has no owner, or two things answer the same
7
- -- question.
8
- --
9
- -- §1 public.transactions — 065 wrote it down in the file itself: "legacy
10
- -- generic archetype; O2C's ledger supersedes it", and 101 repeats it in the
11
- -- allowlist: "superseded by financial_movements". Two books for one fact,
12
- -- and the newer one is the canonical one. This file does not DROP it: a
13
- -- live pool holds rows nobody has read in months, and dropping is a decision
14
- -- with no undo. It does the thing that makes the second book stop being a
15
- -- book — REVOKE the writes, leaving reads alone — and records the retirement
16
- -- so the drop is a later, deliberate step with the count in hand.
17
- --
18
- -- §2 sequences — created by the base (a numbering service every domain needs)
19
- -- AND by plugin-financial's 027. Two creators, one name, and whichever ran
20
- -- last owned it. The base is the owner; the plugin's copy is guarded to a
21
- -- no-op by the IF NOT EXISTS it already carries, so what is left is to say
22
- -- so out loud and let the census enforce it.
23
- --
24
- -- §3 shipping_zones / shipping_settings — created by packages/shop's 0021,
25
- -- living in `public` with no prefix, which reads as base and is not. Same
26
- -- class as documents (forms) and sequences (financial). Renaming a table
27
- -- that live storefront code reads is a synchronised deploy, and 141 already
28
- -- built the mechanism for exactly this: the object moves and the old name
29
- -- keeps answering, with the table's grants copied onto the alias so the
30
- -- callers keep the access they have today. That is what happens here —
31
- -- WRITES INCLUDED, because the shop writes both of these through the old
32
- -- name and a read-only bridge is not a bridge for a writer.
33
- --
34
- -- Idempotent and replay-safe.
35
- -- ============================================================================
36
-
37
- -- ─────────────────────────────────────────────────────────────────────────
38
- -- §1 The legacy archetype becomes read-only
39
- -- ─────────────────────────────────────────────────────────────────────────
40
-
41
- DO $$
42
- BEGIN
43
- IF to_regclass('public.transactions') IS NULL THEN
44
- RETURN;
45
- END IF;
46
-
47
- -- Reads keep working: dashboards, the migration ledger and any support query
48
- -- can still see what is there. Writes stop, which is what makes the ledger the
49
- -- only book being kept from here on.
50
- REVOKE INSERT, UPDATE, DELETE ON public.transactions FROM anon, authenticated;
51
-
52
- EXECUTE $c$ COMMENT ON TABLE public.transactions IS
53
- 'RETIRED (148). The legacy generic archetype the two-leg ledger superseded — plg_financial_movements is the book. Reads left in place for history; writes revoked. Drop is a separate, deliberate step once every pool reports zero rows unaccounted for in the ledger.' $c$;
54
- END $$;
55
-
56
- -- The drop gate — "which rows here is the ledger not accounting for?" — is as
57
- -- much a question about a plugin's table as about this one, so it lives with the
58
- -- plugin that keeps the book: plugin-financial 034.
59
-
60
- -- ─────────────────────────────────────────────────────────────────────────
61
- -- §2 sequences belongs to the base, and the census can prove it
62
- --
63
- -- The prefix rule (ADR 0011) says `plg_` means "owned by plugin X". The inverse
64
- -- has had no register: an unprefixed table in `public` is base-owned by
65
- -- convention only. This is that register — the base declares what is its, and
66
- -- check-schema-ownership reads it.
67
- -- ─────────────────────────────────────────────────────────────────────────
68
-
69
- CREATE TABLE IF NOT EXISTS app.base_owned_tables (
70
- table_name text PRIMARY KEY,
71
- note text,
72
- created_at timestamptz NOT NULL DEFAULT now()
73
- );
74
-
75
- COMMENT ON TABLE app.base_owned_tables IS
76
- 'Unprefixed tables in public that the BASE owns (148). A plugin creating a table whose name is in here is the violation the ownership census reports — the prefix rule only ever answered the other direction.';
77
-
78
- ALTER TABLE app.base_owned_tables ENABLE ROW LEVEL SECURITY;
79
- DROP POLICY IF EXISTS base_owned_tables_read ON app.base_owned_tables;
80
- CREATE POLICY base_owned_tables_read ON app.base_owned_tables FOR SELECT TO authenticated, service_role USING (true);
81
- GRANT SELECT ON app.base_owned_tables TO authenticated, service_role;
82
-
83
- INSERT INTO app.base_owned_tables (table_name, note) VALUES
84
- ('sequences', 'Numbering for every domain. plugin-financial 027 also creates it, guarded — the base is the owner.'),
85
- ('people', 'The Person archetype.'),
86
- ('person_roles', 'The hats a Person wears.'),
87
- ('products', 'The Item de catálogo archetype.'),
88
- ('services', 'The service half of the catalog.'),
89
- ('product_variants', 'The shape of what is sold — the archetype, not the shop copy (ADR 0024).'),
90
- ('categories', 'The Category archetype.'),
91
- ('orders', 'The Venda archetype.'),
92
- ('order_items', 'The lines of a Venda.'),
93
- ('appointments', 'The Compromisso archetype.'),
94
- ('appointment_items', 'The lines of a Compromisso.'),
95
- ('schedules', 'The Escala archetype.'),
96
- ('addresses', 'Where a Person, a unit or a tenant is.'),
97
- ('company_legal_entities','The Empresa.'),
98
- ('tenants', 'The tenant itself.'),
99
- ('invitations', 'An invite to become a member.'),
100
- ('audit_logs', 'The append-only audit trail.'),
101
- ('holidays', 'The calendar the tenant does not work.'),
102
- ('access_grants', 'Delegated access (ADR 0011).'),
103
- ('access_sessions', 'The session a grant issues.')
104
- ON CONFLICT (table_name) DO UPDATE SET note = EXCLUDED.note;
105
-
106
- -- ─────────────────────────────────────────────────────────────────────────
107
- -- §3 Shipping moves under the shop's prefix, and the old name keeps answering
108
- --
109
- -- 141's mechanism: rename, then leave a view with the old name so no deploy has
110
- -- to be synchronised. Guarded on every side — a pool without the shop installed
111
- -- has neither name and skips the whole block.
112
- -- ─────────────────────────────────────────────────────────────────────────
113
-
114
- DO $$
115
- DECLARE
116
- v_pair record;
117
- g record;
118
- BEGIN
119
- FOR v_pair IN SELECT * FROM (VALUES
120
- ('shipping_zones', 'plg_shop_shipping_zones'),
121
- ('shipping_settings', 'plg_shop_shipping_settings')
122
- ) AS p(old_name, new_name)
123
- LOOP
124
- IF to_regclass('public.' || quote_ident(v_pair.old_name)) IS NOT NULL
125
- AND to_regclass('public.' || quote_ident(v_pair.new_name)) IS NULL THEN
126
- -- Only a TABLE moves; if the old name is already the compat view this
127
- -- migration wrote on a previous pass, there is nothing to do.
128
- IF EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
129
- WHERE n.nspname = 'public' AND c.relname = v_pair.old_name AND c.relkind = 'r') THEN
130
- EXECUTE format('ALTER TABLE public.%I RENAME TO %I', v_pair.old_name, v_pair.new_name);
131
- EXECUTE format('CREATE OR REPLACE VIEW public.%I WITH (security_invoker = true) AS SELECT * FROM public.%I',
132
- v_pair.old_name, v_pair.new_name);
133
- -- The alias is exactly as reachable as what it points at — 141's rule,
134
- -- and the reason this is a loop and not a hardcoded GRANT SELECT.
135
- -- Shipping zones and settings are WRITTEN through the old name by the
136
- -- published SDK (`packages/shop/src/data/tables.ts` still says
137
- -- 'shipping_zones'), so a read-only alias does not make the rename
138
- -- unsynchronised — it moves the breakage from "the table is gone" to
139
- -- "permission denied for view", on the merchant's delivery screen.
140
- FOR g IN
141
- SELECT DISTINCT grantee, privilege_type
142
- FROM information_schema.role_table_grants
143
- WHERE table_schema = 'public'
144
- AND table_name = v_pair.new_name
145
- AND grantee IN ('anon', 'authenticated', 'service_role')
146
- LOOP
147
- EXECUTE format('GRANT %s ON public.%I TO %I', g.privilege_type, v_pair.old_name, g.grantee);
148
- END LOOP;
149
- EXECUTE format($c$ COMMENT ON VIEW public.%I IS 'Compat name for public.%I (148, mechanism from 141). The table carries the shop prefix now; this view is what lets the rename not be a synchronised deploy.' $c$,
150
- v_pair.old_name, v_pair.new_name);
151
- END IF;
152
- END IF;
153
- END LOOP;
154
- END $$;