@fayz-ai/db 0.1.1 → 0.8.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/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/helpers.d.ts +1 -1
- package/dist/index.cjs +8 -10
- 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 +8 -9
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +19 -19
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/0000_legacy_quarantine.sql +86 -0
- package/migrations/0000b_straggler_sweep.sql +37 -0
- package/migrations/000_core_v1_convert.sql +343 -0
- package/migrations/001_core.sql +372 -0
- package/migrations/002_rls_user_tenant_ids.sql +63 -0
- package/migrations/003_invitations.sql +17 -0
- package/migrations/004_archetypes.sql +343 -0
- package/migrations/005_locations_archetype.sql +18 -0
- package/migrations/006_archetype_rls.sql +74 -0
- package/migrations/007_archetype_grants.sql +4 -0
- package/migrations/008_grants.sql +5 -0
- package/migrations/009_anon_hardening.sql +19 -0
- package/migrations/010_migration_ledger.sql +30 -0
- package/migrations/011_anon_write_revoke.sql +12 -0
- package/package.json +6 -5
- package/src/helpers.ts +1 -1
- package/src/index.ts +1 -1
- package/src/schema/spine.ts +14 -15
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- core archetype tables (public schema)
|
|
3
|
+
-- Shared base entities that project-specific tables extend via FK
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
|
|
6
|
+
-- ---------------------------------------------------------------------------
|
|
7
|
+
-- 1. people — any human entity (staff, customer, supplier, professional)
|
|
8
|
+
-- ---------------------------------------------------------------------------
|
|
9
|
+
CREATE TABLE IF NOT EXISTS public.people (
|
|
10
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
11
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
12
|
+
kind text NOT NULL,
|
|
13
|
+
name text NOT NULL,
|
|
14
|
+
email text,
|
|
15
|
+
phone text,
|
|
16
|
+
document_number text,
|
|
17
|
+
avatar_url text,
|
|
18
|
+
date_of_birth date,
|
|
19
|
+
address text,
|
|
20
|
+
city text,
|
|
21
|
+
state text,
|
|
22
|
+
country text DEFAULT 'BR',
|
|
23
|
+
postal_code text,
|
|
24
|
+
tags text[] DEFAULT '{}',
|
|
25
|
+
is_active boolean DEFAULT true,
|
|
26
|
+
notes text,
|
|
27
|
+
metadata jsonb DEFAULT '{}',
|
|
28
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
29
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS people_tenant_kind ON public.people(tenant_id, kind);
|
|
33
|
+
DROP TRIGGER IF EXISTS people_updated_at ON public.people;
|
|
34
|
+
CREATE TRIGGER people_updated_at BEFORE UPDATE ON public.people
|
|
35
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
36
|
+
|
|
37
|
+
-- ---------------------------------------------------------------------------
|
|
38
|
+
-- 2. categories — generic taxonomy (species, menu categories, regions)
|
|
39
|
+
-- ---------------------------------------------------------------------------
|
|
40
|
+
CREATE TABLE IF NOT EXISTS public.categories (
|
|
41
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
42
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
43
|
+
kind text NOT NULL,
|
|
44
|
+
name text NOT NULL,
|
|
45
|
+
slug text,
|
|
46
|
+
parent_id uuid REFERENCES public.categories(id) ON DELETE SET NULL,
|
|
47
|
+
icon text,
|
|
48
|
+
color text,
|
|
49
|
+
sort_order integer DEFAULT 0,
|
|
50
|
+
is_active boolean DEFAULT true,
|
|
51
|
+
metadata jsonb DEFAULT '{}',
|
|
52
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
53
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS categories_tenant_kind ON public.categories(tenant_id, kind);
|
|
57
|
+
DROP TRIGGER IF EXISTS categories_updated_at ON public.categories;
|
|
58
|
+
CREATE TRIGGER categories_updated_at BEFORE UPDATE ON public.categories
|
|
59
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
60
|
+
|
|
61
|
+
-- ---------------------------------------------------------------------------
|
|
62
|
+
-- 3. products — physical/trackable items (menu items, equipment, ingredients)
|
|
63
|
+
-- ---------------------------------------------------------------------------
|
|
64
|
+
CREATE TABLE IF NOT EXISTS public.products (
|
|
65
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
66
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
67
|
+
category_id uuid REFERENCES public.categories(id) ON DELETE SET NULL,
|
|
68
|
+
name text NOT NULL,
|
|
69
|
+
description text,
|
|
70
|
+
sku text,
|
|
71
|
+
price numeric,
|
|
72
|
+
cost numeric,
|
|
73
|
+
currency text DEFAULT 'BRL',
|
|
74
|
+
unit text,
|
|
75
|
+
image_url text,
|
|
76
|
+
stock numeric,
|
|
77
|
+
min_stock numeric,
|
|
78
|
+
status text DEFAULT 'active',
|
|
79
|
+
is_active boolean DEFAULT true,
|
|
80
|
+
tags text[] DEFAULT '{}',
|
|
81
|
+
metadata jsonb DEFAULT '{}',
|
|
82
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
83
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS products_tenant ON public.products(tenant_id);
|
|
87
|
+
DROP TRIGGER IF EXISTS products_updated_at ON public.products;
|
|
88
|
+
CREATE TRIGGER products_updated_at BEFORE UPDATE ON public.products
|
|
89
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
90
|
+
|
|
91
|
+
-- ---------------------------------------------------------------------------
|
|
92
|
+
-- 4. services — intangible offerings (exam types, consultations)
|
|
93
|
+
-- ---------------------------------------------------------------------------
|
|
94
|
+
CREATE TABLE IF NOT EXISTS public.services (
|
|
95
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
96
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
97
|
+
category_id uuid REFERENCES public.categories(id) ON DELETE SET NULL,
|
|
98
|
+
name text NOT NULL,
|
|
99
|
+
description text,
|
|
100
|
+
price numeric,
|
|
101
|
+
cost numeric,
|
|
102
|
+
currency text DEFAULT 'BRL',
|
|
103
|
+
duration_minutes integer,
|
|
104
|
+
image_url text,
|
|
105
|
+
status text DEFAULT 'active',
|
|
106
|
+
is_active boolean DEFAULT true,
|
|
107
|
+
tags text[] DEFAULT '{}',
|
|
108
|
+
metadata jsonb DEFAULT '{}',
|
|
109
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
110
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE INDEX IF NOT EXISTS services_tenant ON public.services(tenant_id);
|
|
114
|
+
DROP TRIGGER IF EXISTS services_updated_at ON public.services;
|
|
115
|
+
CREATE TRIGGER services_updated_at BEFORE UPDATE ON public.services
|
|
116
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
117
|
+
|
|
118
|
+
-- ---------------------------------------------------------------------------
|
|
119
|
+
-- 5. orders — business orders/jobs
|
|
120
|
+
-- ---------------------------------------------------------------------------
|
|
121
|
+
CREATE TABLE IF NOT EXISTS public.orders (
|
|
122
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
123
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
124
|
+
kind text NOT NULL,
|
|
125
|
+
reference_number text,
|
|
126
|
+
status text DEFAULT 'draft',
|
|
127
|
+
party_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
128
|
+
assignee_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
129
|
+
location_id uuid REFERENCES public.locations(id) ON DELETE SET NULL,
|
|
130
|
+
subtotal numeric DEFAULT 0,
|
|
131
|
+
discount numeric DEFAULT 0,
|
|
132
|
+
tax numeric DEFAULT 0,
|
|
133
|
+
total numeric DEFAULT 0,
|
|
134
|
+
currency text DEFAULT 'BRL',
|
|
135
|
+
due_at timestamptz,
|
|
136
|
+
completed_at timestamptz,
|
|
137
|
+
notes text,
|
|
138
|
+
tags text[] DEFAULT '{}',
|
|
139
|
+
metadata jsonb DEFAULT '{}',
|
|
140
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
141
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
CREATE INDEX IF NOT EXISTS orders_tenant_kind ON public.orders(tenant_id, kind);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS orders_status ON public.orders(tenant_id, status);
|
|
146
|
+
DROP TRIGGER IF EXISTS orders_updated_at ON public.orders;
|
|
147
|
+
CREATE TRIGGER orders_updated_at BEFORE UPDATE ON public.orders
|
|
148
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
149
|
+
|
|
150
|
+
-- ---------------------------------------------------------------------------
|
|
151
|
+
-- 6. order_items — line items for orders
|
|
152
|
+
-- ---------------------------------------------------------------------------
|
|
153
|
+
CREATE TABLE IF NOT EXISTS public.order_items (
|
|
154
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
155
|
+
order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE,
|
|
156
|
+
product_id uuid REFERENCES public.products(id) ON DELETE SET NULL,
|
|
157
|
+
service_id uuid REFERENCES public.services(id) ON DELETE SET NULL,
|
|
158
|
+
name text NOT NULL,
|
|
159
|
+
description text,
|
|
160
|
+
quantity numeric DEFAULT 1,
|
|
161
|
+
unit_price numeric DEFAULT 0,
|
|
162
|
+
discount numeric DEFAULT 0,
|
|
163
|
+
total numeric DEFAULT 0,
|
|
164
|
+
sort_order integer DEFAULT 0,
|
|
165
|
+
metadata jsonb DEFAULT '{}',
|
|
166
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CREATE INDEX IF NOT EXISTS order_items_order ON public.order_items(order_id);
|
|
170
|
+
|
|
171
|
+
-- ---------------------------------------------------------------------------
|
|
172
|
+
-- 7. transactions — financial movements (payments, refunds, expenses)
|
|
173
|
+
-- ---------------------------------------------------------------------------
|
|
174
|
+
CREATE TABLE IF NOT EXISTS public.transactions (
|
|
175
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
176
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
177
|
+
kind text NOT NULL,
|
|
178
|
+
order_id uuid REFERENCES public.orders(id) ON DELETE SET NULL,
|
|
179
|
+
party_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
180
|
+
amount numeric NOT NULL,
|
|
181
|
+
currency text DEFAULT 'BRL',
|
|
182
|
+
payment_method text,
|
|
183
|
+
reference text,
|
|
184
|
+
status text DEFAULT 'completed',
|
|
185
|
+
transacted_at timestamptz DEFAULT now(),
|
|
186
|
+
notes text,
|
|
187
|
+
metadata jsonb DEFAULT '{}',
|
|
188
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
189
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
CREATE INDEX IF NOT EXISTS transactions_tenant_kind ON public.transactions(tenant_id, kind);
|
|
193
|
+
DROP TRIGGER IF EXISTS transactions_updated_at ON public.transactions;
|
|
194
|
+
CREATE TRIGGER transactions_updated_at BEFORE UPDATE ON public.transactions
|
|
195
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
196
|
+
|
|
197
|
+
-- ---------------------------------------------------------------------------
|
|
198
|
+
-- 8. appointments — reservations/appointments
|
|
199
|
+
-- ---------------------------------------------------------------------------
|
|
200
|
+
CREATE TABLE IF NOT EXISTS public.appointments (
|
|
201
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
202
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
203
|
+
kind text NOT NULL,
|
|
204
|
+
party_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
205
|
+
assignee_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
206
|
+
location_id uuid REFERENCES public.locations(id) ON DELETE SET NULL,
|
|
207
|
+
order_id uuid REFERENCES public.orders(id) ON DELETE SET NULL,
|
|
208
|
+
starts_at timestamptz NOT NULL,
|
|
209
|
+
ends_at timestamptz,
|
|
210
|
+
status text DEFAULT 'pending',
|
|
211
|
+
notes text,
|
|
212
|
+
metadata jsonb DEFAULT '{}',
|
|
213
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
214
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
CREATE INDEX IF NOT EXISTS appointments_tenant_kind ON public.appointments(tenant_id, kind);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS appointments_starts ON public.appointments(tenant_id, starts_at);
|
|
219
|
+
DROP TRIGGER IF EXISTS appointments_updated_at ON public.appointments;
|
|
220
|
+
CREATE TRIGGER appointments_updated_at BEFORE UPDATE ON public.appointments
|
|
221
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
222
|
+
|
|
223
|
+
-- ---------------------------------------------------------------------------
|
|
224
|
+
-- 9. appointment_items — services within an appointment
|
|
225
|
+
-- (booking_id column name preserved for parity with converted pools —
|
|
226
|
+
-- the in-place converter renames the table, not the FK column)
|
|
227
|
+
-- ---------------------------------------------------------------------------
|
|
228
|
+
CREATE TABLE IF NOT EXISTS public.appointment_items (
|
|
229
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
230
|
+
booking_id uuid NOT NULL REFERENCES public.appointments(id) ON DELETE CASCADE,
|
|
231
|
+
service_id uuid REFERENCES public.services(id) ON DELETE SET NULL,
|
|
232
|
+
assignee_id uuid REFERENCES public.people(id) ON DELETE SET NULL,
|
|
233
|
+
name text NOT NULL,
|
|
234
|
+
duration_minutes integer,
|
|
235
|
+
price numeric DEFAULT 0,
|
|
236
|
+
sort_order integer DEFAULT 0,
|
|
237
|
+
notes text,
|
|
238
|
+
metadata jsonb DEFAULT '{}',
|
|
239
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
CREATE INDEX IF NOT EXISTS appointment_items_booking ON public.appointment_items(booking_id);
|
|
243
|
+
|
|
244
|
+
-- ---------------------------------------------------------------------------
|
|
245
|
+
-- 10. schedules — recurring availability/shifts
|
|
246
|
+
-- ---------------------------------------------------------------------------
|
|
247
|
+
CREATE TABLE IF NOT EXISTS public.schedules (
|
|
248
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
249
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
250
|
+
kind text NOT NULL,
|
|
251
|
+
assignee_id uuid REFERENCES public.people(id) ON DELETE CASCADE,
|
|
252
|
+
location_id uuid REFERENCES public.locations(id) ON DELETE SET NULL,
|
|
253
|
+
day_of_week smallint,
|
|
254
|
+
specific_date date,
|
|
255
|
+
starts_at time NOT NULL,
|
|
256
|
+
ends_at time NOT NULL,
|
|
257
|
+
is_active boolean DEFAULT true,
|
|
258
|
+
metadata jsonb DEFAULT '{}',
|
|
259
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
260
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
CREATE INDEX IF NOT EXISTS schedules_tenant_kind ON public.schedules(tenant_id, kind);
|
|
264
|
+
CREATE INDEX IF NOT EXISTS schedules_assignee ON public.schedules(assignee_id, day_of_week);
|
|
265
|
+
DROP TRIGGER IF EXISTS schedules_updated_at ON public.schedules;
|
|
266
|
+
CREATE TRIGGER schedules_updated_at BEFORE UPDATE ON public.schedules
|
|
267
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
268
|
+
|
|
269
|
+
-- ============================================================================
|
|
270
|
+
-- RLS policies — tenant isolation for all archetype tables
|
|
271
|
+
-- ============================================================================
|
|
272
|
+
|
|
273
|
+
DO $$
|
|
274
|
+
DECLARE
|
|
275
|
+
tbl text;
|
|
276
|
+
BEGIN
|
|
277
|
+
FOREACH tbl IN ARRAY ARRAY[
|
|
278
|
+
'people', 'categories', 'products', 'services',
|
|
279
|
+
'orders', 'transactions', 'appointments', 'schedules'
|
|
280
|
+
]
|
|
281
|
+
LOOP
|
|
282
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', tbl);
|
|
283
|
+
-- Drop first so a replay on an already-provisioned pool no-ops cleanly.
|
|
284
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_select" ON public.%I', tbl, tbl);
|
|
285
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_insert" ON public.%I', tbl, tbl);
|
|
286
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_update" ON public.%I', tbl, tbl);
|
|
287
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_delete" ON public.%I', tbl, tbl);
|
|
288
|
+
EXECUTE format(
|
|
289
|
+
'CREATE POLICY "%s_select" ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid()))',
|
|
290
|
+
tbl, tbl
|
|
291
|
+
);
|
|
292
|
+
EXECUTE format(
|
|
293
|
+
'CREATE POLICY "%s_insert" ON public.%I FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid()))',
|
|
294
|
+
tbl, tbl
|
|
295
|
+
);
|
|
296
|
+
EXECUTE format(
|
|
297
|
+
'CREATE POLICY "%s_update" ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid()))',
|
|
298
|
+
tbl, tbl
|
|
299
|
+
);
|
|
300
|
+
EXECUTE format(
|
|
301
|
+
'CREATE POLICY "%s_delete" ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid()))',
|
|
302
|
+
tbl, tbl
|
|
303
|
+
);
|
|
304
|
+
END LOOP;
|
|
305
|
+
|
|
306
|
+
-- Child tables (order_items, appointment_items) — RLS via parent FK
|
|
307
|
+
FOREACH tbl IN ARRAY ARRAY['order_items', 'appointment_items']
|
|
308
|
+
LOOP
|
|
309
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', tbl);
|
|
310
|
+
END LOOP;
|
|
311
|
+
END $$;
|
|
312
|
+
|
|
313
|
+
-- order_items: access via parent order's tenant
|
|
314
|
+
DROP POLICY IF EXISTS "order_items_select" ON public.order_items;
|
|
315
|
+
CREATE POLICY "order_items_select" ON public.order_items FOR SELECT TO authenticated
|
|
316
|
+
USING (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
317
|
+
DROP POLICY IF EXISTS "order_items_insert" ON public.order_items;
|
|
318
|
+
CREATE POLICY "order_items_insert" ON public.order_items FOR INSERT TO authenticated
|
|
319
|
+
WITH CHECK (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
320
|
+
DROP POLICY IF EXISTS "order_items_update" ON public.order_items;
|
|
321
|
+
CREATE POLICY "order_items_update" ON public.order_items FOR UPDATE TO authenticated
|
|
322
|
+
USING (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
323
|
+
DROP POLICY IF EXISTS "order_items_delete" ON public.order_items;
|
|
324
|
+
CREATE POLICY "order_items_delete" ON public.order_items FOR DELETE TO authenticated
|
|
325
|
+
USING (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
326
|
+
|
|
327
|
+
-- appointment_items: access via parent appointment's tenant
|
|
328
|
+
DROP POLICY IF EXISTS "appointment_items_select" ON public.appointment_items;
|
|
329
|
+
CREATE POLICY "appointment_items_select" ON public.appointment_items FOR SELECT TO authenticated
|
|
330
|
+
USING (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
331
|
+
DROP POLICY IF EXISTS "appointment_items_insert" ON public.appointment_items;
|
|
332
|
+
CREATE POLICY "appointment_items_insert" ON public.appointment_items FOR INSERT TO authenticated
|
|
333
|
+
WITH CHECK (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
334
|
+
DROP POLICY IF EXISTS "appointment_items_update" ON public.appointment_items;
|
|
335
|
+
CREATE POLICY "appointment_items_update" ON public.appointment_items FOR UPDATE TO authenticated
|
|
336
|
+
USING (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
337
|
+
DROP POLICY IF EXISTS "appointment_items_delete" ON public.appointment_items;
|
|
338
|
+
CREATE POLICY "appointment_items_delete" ON public.appointment_items FOR DELETE TO authenticated
|
|
339
|
+
USING (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid())));
|
|
340
|
+
|
|
341
|
+
-- Grant access to archetype tables
|
|
342
|
+
GRANT ALL ON ALL TABLES IN SCHEMA public TO authenticated, service_role;
|
|
343
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Bring public.locations in line with other archetype tables
|
|
2
|
+
ALTER TABLE public.locations
|
|
3
|
+
ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT 'branch',
|
|
4
|
+
ADD COLUMN IF NOT EXISTS email text,
|
|
5
|
+
ADD COLUMN IF NOT EXISTS postal_code text,
|
|
6
|
+
ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}',
|
|
7
|
+
ADD COLUMN IF NOT EXISTS notes text,
|
|
8
|
+
ADD COLUMN IF NOT EXISTS metadata jsonb DEFAULT '{}';
|
|
9
|
+
|
|
10
|
+
-- Migrate settings → metadata for existing rows (if settings column still exists)
|
|
11
|
+
DO $$ BEGIN
|
|
12
|
+
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'locations' AND column_name = 'settings') THEN
|
|
13
|
+
UPDATE public.locations SET metadata = settings WHERE metadata = '{}' AND settings != '{}';
|
|
14
|
+
ALTER TABLE public.locations DROP COLUMN settings;
|
|
15
|
+
END IF;
|
|
16
|
+
END $$;
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS locations_tenant_kind ON public.locations(tenant_id, kind);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- Fix archetype RLS policies to use SECURITY DEFINER function
|
|
2
|
+
-- The raw subquery on tenant_members fails due to recursive RLS
|
|
3
|
+
|
|
4
|
+
-- Ensure the SECURITY DEFINER helper exists (bypasses RLS)
|
|
5
|
+
CREATE OR REPLACE FUNCTION public.user_tenant_ids()
|
|
6
|
+
RETURNS SETOF uuid
|
|
7
|
+
LANGUAGE sql STABLE SECURITY DEFINER AS $$
|
|
8
|
+
SELECT tenant_id FROM public.tenant_members WHERE user_id = auth.uid();
|
|
9
|
+
$$;
|
|
10
|
+
|
|
11
|
+
-- Drop and recreate all archetype table policies using the SECURITY DEFINER function
|
|
12
|
+
DO $$
|
|
13
|
+
DECLARE
|
|
14
|
+
tbl text;
|
|
15
|
+
BEGIN
|
|
16
|
+
FOREACH tbl IN ARRAY ARRAY[
|
|
17
|
+
'people', 'categories', 'products', 'services',
|
|
18
|
+
'orders', 'transactions', 'appointments', 'schedules'
|
|
19
|
+
]
|
|
20
|
+
LOOP
|
|
21
|
+
-- Drop old policies
|
|
22
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_select" ON public.%I', tbl, tbl);
|
|
23
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_insert" ON public.%I', tbl, tbl);
|
|
24
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_update" ON public.%I', tbl, tbl);
|
|
25
|
+
EXECUTE format('DROP POLICY IF EXISTS "%s_delete" ON public.%I', tbl, tbl);
|
|
26
|
+
|
|
27
|
+
-- Create new policies using SECURITY DEFINER function
|
|
28
|
+
EXECUTE format(
|
|
29
|
+
'CREATE POLICY "%s_select" ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))',
|
|
30
|
+
tbl, tbl
|
|
31
|
+
);
|
|
32
|
+
EXECUTE format(
|
|
33
|
+
'CREATE POLICY "%s_insert" ON public.%I FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()))',
|
|
34
|
+
tbl, tbl
|
|
35
|
+
);
|
|
36
|
+
EXECUTE format(
|
|
37
|
+
'CREATE POLICY "%s_update" ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))',
|
|
38
|
+
tbl, tbl
|
|
39
|
+
);
|
|
40
|
+
EXECUTE format(
|
|
41
|
+
'CREATE POLICY "%s_delete" ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))',
|
|
42
|
+
tbl, tbl
|
|
43
|
+
);
|
|
44
|
+
END LOOP;
|
|
45
|
+
END $$;
|
|
46
|
+
|
|
47
|
+
-- Fix child table policies too
|
|
48
|
+
DROP POLICY IF EXISTS "order_items_select" ON public.order_items;
|
|
49
|
+
DROP POLICY IF EXISTS "order_items_insert" ON public.order_items;
|
|
50
|
+
DROP POLICY IF EXISTS "order_items_update" ON public.order_items;
|
|
51
|
+
DROP POLICY IF EXISTS "order_items_delete" ON public.order_items;
|
|
52
|
+
|
|
53
|
+
CREATE POLICY "order_items_select" ON public.order_items FOR SELECT TO authenticated
|
|
54
|
+
USING (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
55
|
+
CREATE POLICY "order_items_insert" ON public.order_items FOR INSERT TO authenticated
|
|
56
|
+
WITH CHECK (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
57
|
+
CREATE POLICY "order_items_update" ON public.order_items FOR UPDATE TO authenticated
|
|
58
|
+
USING (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
59
|
+
CREATE POLICY "order_items_delete" ON public.order_items FOR DELETE TO authenticated
|
|
60
|
+
USING (order_id IN (SELECT id FROM public.orders WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
61
|
+
|
|
62
|
+
DROP POLICY IF EXISTS "appointment_items_select" ON public.appointment_items;
|
|
63
|
+
DROP POLICY IF EXISTS "appointment_items_insert" ON public.appointment_items;
|
|
64
|
+
DROP POLICY IF EXISTS "appointment_items_update" ON public.appointment_items;
|
|
65
|
+
DROP POLICY IF EXISTS "appointment_items_delete" ON public.appointment_items;
|
|
66
|
+
|
|
67
|
+
CREATE POLICY "appointment_items_select" ON public.appointment_items FOR SELECT TO authenticated
|
|
68
|
+
USING (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
69
|
+
CREATE POLICY "appointment_items_insert" ON public.appointment_items FOR INSERT TO authenticated
|
|
70
|
+
WITH CHECK (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
71
|
+
CREATE POLICY "appointment_items_update" ON public.appointment_items FOR UPDATE TO authenticated
|
|
72
|
+
USING (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
73
|
+
CREATE POLICY "appointment_items_delete" ON public.appointment_items FOR DELETE TO authenticated
|
|
74
|
+
USING (booking_id IN (SELECT id FROM public.appointments WHERE tenant_id IN (SELECT public.user_tenant_ids())));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Grant access to archetype tables created in migration 004
|
|
2
|
+
-- The original GRANT ALL ON ALL TABLES only covered tables that existed at that time
|
|
3
|
+
GRANT ALL ON ALL TABLES IN SCHEMA public TO authenticated, service_role;
|
|
4
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Ensure all roles have access to the public schema
|
|
2
|
+
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
|
|
3
|
+
GRANT ALL ON ALL TABLES IN SCHEMA public TO authenticated, service_role;
|
|
4
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon;
|
|
5
|
+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated, service_role;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 009: anon hardening — close 008's blanket grants for the anon role.
|
|
3
|
+
--
|
|
4
|
+
-- 008_grants.sql grants SELECT on all tables and EXECUTE on all functions to
|
|
5
|
+
-- anon. RLS (policies are TO authenticated) already returns zero rows for
|
|
6
|
+
-- table reads, but EXECUTE on SECURITY DEFINER functions like
|
|
7
|
+
-- create_tenant_with_owner is a real abuse surface on projects that serve
|
|
8
|
+
-- public (anon-key) pages. Revoke everything; anon-facing surfaces must grant
|
|
9
|
+
-- themselves explicitly (e.g. plugin-agenda's public booking RPCs/views).
|
|
10
|
+
--
|
|
11
|
+
-- Apply AFTER 008 and BEFORE plugin migrations that grant anon access.
|
|
12
|
+
-- ============================================================================
|
|
13
|
+
|
|
14
|
+
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA public FROM anon;
|
|
15
|
+
|
|
16
|
+
REVOKE SELECT ON ALL TABLES IN SCHEMA public FROM anon;
|
|
17
|
+
|
|
18
|
+
-- Future objects: don't default-grant anon anything.
|
|
19
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE EXECUTE ON FUNCTIONS FROM anon;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 010_migration_ledger.sql — per-pool applied-migration ledger.
|
|
3
|
+
--
|
|
4
|
+
-- The Runner v2 executor records every applied SQL file here (plugin_id +
|
|
5
|
+
-- file_name + checksum) so re-applies are detected and checksum drift on an
|
|
6
|
+
-- already-applied file is a HARD STOP.
|
|
7
|
+
--
|
|
8
|
+
-- service_role only: no anon/authenticated grants, RLS enabled with NO
|
|
9
|
+
-- policies, so PostgREST (anon/authenticated) is deny-all. Only the service
|
|
10
|
+
-- role (which bypasses RLS) — i.e. the runner — can read/write it.
|
|
11
|
+
-- ============================================================================
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS public.fayz_migration_ledger (
|
|
14
|
+
id bigserial PRIMARY KEY,
|
|
15
|
+
plugin_id text NOT NULL,
|
|
16
|
+
file_name text NOT NULL,
|
|
17
|
+
checksum text NOT NULL,
|
|
18
|
+
plugin_version text,
|
|
19
|
+
applied_at timestamptz NOT NULL DEFAULT now(),
|
|
20
|
+
applied_by text,
|
|
21
|
+
UNIQUE(plugin_id, file_name)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
ALTER TABLE public.fayz_migration_ledger ENABLE ROW LEVEL SECURITY;
|
|
25
|
+
|
|
26
|
+
-- Deny-all through PostgREST: revoke any inherited grants, grant to nobody but
|
|
27
|
+
-- service_role (which bypasses RLS anyway). No policies = no anon/authenticated access.
|
|
28
|
+
REVOKE ALL ON public.fayz_migration_ledger FROM anon, authenticated;
|
|
29
|
+
GRANT ALL ON public.fayz_migration_ledger TO service_role;
|
|
30
|
+
GRANT USAGE, SELECT ON SEQUENCE public.fayz_migration_ledger_id_seq TO service_role;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- 009 revoked anon SELECT/EXECUTE but Supabase default privileges also hand
|
|
2
|
+
-- anon INSERT/UPDATE/DELETE/... on every table created by postgres. RLS blocks
|
|
3
|
+
-- those writes today; make the grant layer agree and stop future tables from
|
|
4
|
+
-- being born writable-by-anon.
|
|
5
|
+
|
|
6
|
+
REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER
|
|
7
|
+
ON ALL TABLES IN SCHEMA public FROM anon;
|
|
8
|
+
|
|
9
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
|
10
|
+
REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER ON TABLES FROM anon;
|
|
11
|
+
|
|
12
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE SELECT ON TABLES FROM anon;
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fayz-ai/db",
|
|
3
|
-
"
|
|
3
|
+
"fayz": {
|
|
4
|
+
"status": "beta"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.8.0",
|
|
4
7
|
"description": "Fayz SDK database layer — Drizzle schema primitives, spine references, and migration helpers shared across plugins.",
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "./dist/index.cjs",
|
|
@@ -16,7 +19,8 @@
|
|
|
16
19
|
},
|
|
17
20
|
"files": [
|
|
18
21
|
"dist",
|
|
19
|
-
"src"
|
|
22
|
+
"src",
|
|
23
|
+
"migrations"
|
|
20
24
|
],
|
|
21
25
|
"dependencies": {
|
|
22
26
|
"drizzle-orm": "^0.36.4"
|
|
@@ -25,9 +29,6 @@
|
|
|
25
29
|
"tsup": "^8.2.0",
|
|
26
30
|
"typescript": "^5.5.0"
|
|
27
31
|
},
|
|
28
|
-
"publishConfig": {
|
|
29
|
-
"access": "public"
|
|
30
|
-
},
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"keywords": [
|
|
33
34
|
"fayz",
|
package/src/helpers.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { tenants } from './schema/spine'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Canonical tenant-scoping column: `tenant_id uuid NOT NULL REFERENCES
|
|
6
|
-
*
|
|
6
|
+
* public.tenants(id) ON DELETE CASCADE`. Every Ring-1 plugin table uses this
|
|
7
7
|
* so tenancy is identical everywhere (and RLS can assume the column exists).
|
|
8
8
|
*/
|
|
9
9
|
export const tenantId = () =>
|
package/src/index.ts
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
// when an app composes its own tables with @fayz-ai/db spine refs + plugin schema).
|
|
9
9
|
export * from 'drizzle-orm/pg-core'
|
|
10
10
|
|
|
11
|
-
export {
|
|
11
|
+
export { tenants, people, orders, appointments, products, orderItems } from './schema/spine'
|
|
12
12
|
export { tenantId, timestamps, createdAt } from './helpers'
|
package/src/schema/spine.ts
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pgTable, uuid } from 'drizzle-orm/pg-core'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Ring 0 — the
|
|
4
|
+
* Ring 0 — the core spine, declared as Drizzle *references* only.
|
|
5
5
|
*
|
|
6
|
-
* These tables are owned by the platform (@fayz/saas
|
|
7
|
-
* every provisioned
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* These tables are owned by the platform (@fayz-ai/saas core) and already exist
|
|
7
|
+
* in every provisioned pool, directly in the `public` schema (industry-pool
|
|
8
|
+
* model — no saas_core schema). We declare a minimal shape here purely so plugin
|
|
9
|
+
* tables can express real foreign keys in TypeScript. They land in the Drizzle
|
|
10
|
+
* *baseline* snapshot (never re-created), so only the `id` FK target is needed —
|
|
11
|
+
* the live columns are authoritative.
|
|
11
12
|
*/
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
export const tenants = saasCore.table('tenants', {
|
|
13
|
+
export const tenants = pgTable('tenants', {
|
|
15
14
|
id: uuid('id').primaryKey(),
|
|
16
15
|
})
|
|
17
16
|
|
|
18
|
-
export const
|
|
17
|
+
export const people = pgTable('people', {
|
|
19
18
|
id: uuid('id').primaryKey(),
|
|
20
19
|
})
|
|
21
20
|
|
|
22
|
-
export const orders =
|
|
21
|
+
export const orders = pgTable('orders', {
|
|
23
22
|
id: uuid('id').primaryKey(),
|
|
24
23
|
})
|
|
25
24
|
|
|
26
|
-
export const
|
|
25
|
+
export const appointments = pgTable('appointments', {
|
|
27
26
|
id: uuid('id').primaryKey(),
|
|
28
27
|
})
|
|
29
28
|
|
|
30
|
-
export const products =
|
|
29
|
+
export const products = pgTable('products', {
|
|
31
30
|
id: uuid('id').primaryKey(),
|
|
32
31
|
})
|
|
33
32
|
|
|
34
|
-
export const orderItems =
|
|
33
|
+
export const orderItems = pgTable('order_items', {
|
|
35
34
|
id: uuid('id').primaryKey(),
|
|
36
35
|
})
|