@soulbatical/tetra-core 0.1.13 → 0.1.15
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.
Potentially problematic release.
This version of @soulbatical/tetra-core might be problematic. Click here for more details.
- package/dist/generators/rls-auditor.d.ts +39 -0
- package/dist/generators/rls-auditor.d.ts.map +1 -0
- package/dist/generators/rls-auditor.js +505 -0
- package/dist/generators/rls-auditor.js.map +1 -0
- package/dist/generators/rls-checker.d.ts +94 -0
- package/dist/generators/rls-checker.d.ts.map +1 -0
- package/dist/generators/rls-checker.js +215 -0
- package/dist/generators/rls-checker.js.map +1 -0
- package/dist/generators/rls-generator.d.ts +77 -0
- package/dist/generators/rls-generator.d.ts.map +1 -0
- package/dist/generators/rls-generator.js +402 -0
- package/dist/generators/rls-generator.js.map +1 -0
- package/dist/generators/rpc/detail-rpc-generator.d.ts +58 -0
- package/dist/generators/rpc/detail-rpc-generator.d.ts.map +1 -0
- package/dist/generators/rpc/detail-rpc-generator.js +163 -0
- package/dist/generators/rpc/detail-rpc-generator.js.map +1 -0
- package/dist/generators/rpc/index.d.ts +24 -0
- package/dist/generators/rpc/index.d.ts.map +1 -0
- package/dist/generators/rpc/index.js +20 -0
- package/dist/generators/rpc/index.js.map +1 -0
- package/dist/generators/rpc/rpc-generator.d.ts +150 -0
- package/dist/generators/rpc/rpc-generator.d.ts.map +1 -0
- package/dist/generators/rpc/rpc-generator.js +743 -0
- package/dist/generators/rpc/rpc-generator.js.map +1 -0
- package/dist/generators/rpc/templates/array.d.ts +29 -0
- package/dist/generators/rpc/templates/array.d.ts.map +1 -0
- package/dist/generators/rpc/templates/array.js +40 -0
- package/dist/generators/rpc/templates/array.js.map +1 -0
- package/dist/generators/rpc/templates/auth.d.ts +85 -0
- package/dist/generators/rpc/templates/auth.d.ts.map +1 -0
- package/dist/generators/rpc/templates/auth.js +233 -0
- package/dist/generators/rpc/templates/auth.js.map +1 -0
- package/dist/generators/rpc/templates/column.d.ts +39 -0
- package/dist/generators/rpc/templates/column.d.ts.map +1 -0
- package/dist/generators/rpc/templates/column.js +97 -0
- package/dist/generators/rpc/templates/column.js.map +1 -0
- package/dist/generators/rpc/templates/enum.d.ts +33 -0
- package/dist/generators/rpc/templates/enum.d.ts.map +1 -0
- package/dist/generators/rpc/templates/enum.js +93 -0
- package/dist/generators/rpc/templates/enum.js.map +1 -0
- package/dist/generators/rpc/templates/nullable.d.ts +31 -0
- package/dist/generators/rpc/templates/nullable.d.ts.map +1 -0
- package/dist/generators/rpc/templates/nullable.js +50 -0
- package/dist/generators/rpc/templates/nullable.js.map +1 -0
- package/dist/generators/rpc/templates/related.d.ts +47 -0
- package/dist/generators/rpc/templates/related.d.ts.map +1 -0
- package/dist/generators/rpc/templates/related.js +182 -0
- package/dist/generators/rpc/templates/related.js.map +1 -0
- package/dist/generators/rpc/templates/search.d.ts +42 -0
- package/dist/generators/rpc/templates/search.d.ts.map +1 -0
- package/dist/generators/rpc/templates/search.js +81 -0
- package/dist/generators/rpc/templates/search.js.map +1 -0
- package/dist/generators/rpc/templates/time.d.ts +44 -0
- package/dist/generators/rpc/templates/time.d.ts.map +1 -0
- package/dist/generators/rpc/templates/time.js +143 -0
- package/dist/generators/rpc/templates/time.js.map +1 -0
- package/dist/generators/rpc/utils.d.ts +58 -0
- package/dist/generators/rpc/utils.d.ts.map +1 -0
- package/dist/generators/rpc/utils.js +92 -0
- package/dist/generators/rpc/utils.js.map +1 -0
- package/dist/generators/rpc/validator.d.ts +21 -0
- package/dist/generators/rpc/validator.d.ts.map +1 -0
- package/dist/generators/rpc/validator.js +398 -0
- package/dist/generators/rpc/validator.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/shared/auth/index.d.ts +1 -1
- package/dist/shared/auth/index.d.ts.map +1 -1
- package/dist/shared/auth/routes.d.ts +4 -1
- package/dist/shared/auth/routes.d.ts.map +1 -1
- package/dist/shared/auth/routes.js +83 -1
- package/dist/shared/auth/routes.js.map +1 -1
- package/dist/shared/auth/types.d.ts +24 -0
- package/dist/shared/auth/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tetra RLS Generator v1.0
|
|
3
|
+
*
|
|
4
|
+
* Generates Row Level Security policies based on SparkBuddy production patterns.
|
|
5
|
+
* Every Tetra project MUST use these patterns for consistent security.
|
|
6
|
+
*
|
|
7
|
+
* NAMING STANDARD (no exceptions):
|
|
8
|
+
* - Organization column: organization_id
|
|
9
|
+
* - User column: user_id
|
|
10
|
+
* - All columns: snake_case
|
|
11
|
+
* - All roles: authenticated (never public)
|
|
12
|
+
*
|
|
13
|
+
* Table types:
|
|
14
|
+
* admin — Only admin/owner can CRUD. Most common. (default)
|
|
15
|
+
* user — Admins see all, users see only own data (user_id = auth.uid())
|
|
16
|
+
* creator — Creator role can INSERT/SELECT own + shared, admin sees all
|
|
17
|
+
* public — Anyone can SELECT, only admin can mutate
|
|
18
|
+
* public-active — Anyone can SELECT where is_active = true, only admin can mutate
|
|
19
|
+
* service — All client access blocked, only service_role can access
|
|
20
|
+
* superadmin — Only superadmins can manage (e.g., organizations table)
|
|
21
|
+
*
|
|
22
|
+
* Special tables (hardcoded, always the same in every project):
|
|
23
|
+
* organizations → superadmin type, org column = 'id'
|
|
24
|
+
* users_public → custom: self-insert, admin+self select/update
|
|
25
|
+
* organization_members → service type
|
|
26
|
+
* organization_invites → service type
|
|
27
|
+
*
|
|
28
|
+
* Prerequisites:
|
|
29
|
+
* - Auth functions from rls-auth-functions.sql must be installed
|
|
30
|
+
* - organization_members table with (user_id, organization_id, role, status)
|
|
31
|
+
* - users_public table with (id, is_superadmin)
|
|
32
|
+
*/
|
|
33
|
+
// Standard column names — no overrides, no legacy
|
|
34
|
+
const ORG_COL = 'organization_id';
|
|
35
|
+
const USER_COL = 'user_id';
|
|
36
|
+
/**
|
|
37
|
+
* Generate RLS policies for a table.
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* const result = generateRLS({ tableName: 'appointments', tableType: 'admin' });
|
|
41
|
+
* // Apply result.sql as a Supabase migration
|
|
42
|
+
*/
|
|
43
|
+
export function generateRLS(config) {
|
|
44
|
+
const { tableName, tableType, visibilityColumn = 'visibility_level', publicVisibilityValues = ['shared', 'public'], allowAnonInsert = false, extraPolicies = [], } = config;
|
|
45
|
+
// Special tables get hardcoded treatment
|
|
46
|
+
const isSpecialTable = SPECIAL_TABLES[tableName];
|
|
47
|
+
if (isSpecialTable) {
|
|
48
|
+
return generateSpecialTable(tableName, isSpecialTable);
|
|
49
|
+
}
|
|
50
|
+
const policies = [];
|
|
51
|
+
const policyNames = [];
|
|
52
|
+
const prefix = tableName;
|
|
53
|
+
const header = generateHeader(tableName, tableType);
|
|
54
|
+
const drops = generateDrops(tableName, prefix);
|
|
55
|
+
switch (tableType) {
|
|
56
|
+
case 'admin':
|
|
57
|
+
policies.push(...generateAdminPolicies(tableName, prefix));
|
|
58
|
+
break;
|
|
59
|
+
case 'user':
|
|
60
|
+
policies.push(...generateUserPolicies(tableName, prefix));
|
|
61
|
+
break;
|
|
62
|
+
case 'creator':
|
|
63
|
+
policies.push(...generateCreatorPolicies(tableName, prefix, visibilityColumn, publicVisibilityValues));
|
|
64
|
+
break;
|
|
65
|
+
case 'public':
|
|
66
|
+
policies.push(...generatePublicPolicies(tableName, prefix, false, allowAnonInsert));
|
|
67
|
+
break;
|
|
68
|
+
case 'public-active':
|
|
69
|
+
policies.push(...generatePublicPolicies(tableName, prefix, true, allowAnonInsert));
|
|
70
|
+
break;
|
|
71
|
+
case 'service':
|
|
72
|
+
policies.push(...generateServicePolicies(tableName, prefix));
|
|
73
|
+
break;
|
|
74
|
+
case 'superadmin':
|
|
75
|
+
policies.push(...generateSuperadminPolicies(tableName, prefix));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
for (const policy of policies) {
|
|
79
|
+
const match = policy.match(/CREATE POLICY "([^"]+)"/);
|
|
80
|
+
if (match)
|
|
81
|
+
policyNames.push(match[1]);
|
|
82
|
+
}
|
|
83
|
+
if (extraPolicies.length > 0) {
|
|
84
|
+
policies.push('\n-- Custom policies');
|
|
85
|
+
policies.push(...extraPolicies);
|
|
86
|
+
}
|
|
87
|
+
const sql = [header, drops, '\n-- Policies', ...policies].join('\n');
|
|
88
|
+
return { sql, policyCount: policyNames.length + extraPolicies.length, tableType, policyNames };
|
|
89
|
+
}
|
|
90
|
+
// ─── Special Tables ────────────────────────────────────────────
|
|
91
|
+
// These tables ALWAYS have the same RLS in every Tetra project.
|
|
92
|
+
const SPECIAL_TABLES = {
|
|
93
|
+
organizations: 'organizations',
|
|
94
|
+
users_public: 'users_public',
|
|
95
|
+
organization_members: 'organization_members',
|
|
96
|
+
organization_invites: 'organization_invites',
|
|
97
|
+
};
|
|
98
|
+
function generateSpecialTable(tableName, type) {
|
|
99
|
+
const header = generateHeader(tableName, type);
|
|
100
|
+
const drops = generateDrops(tableName, tableName);
|
|
101
|
+
let policies;
|
|
102
|
+
switch (type) {
|
|
103
|
+
case 'organizations':
|
|
104
|
+
policies = [
|
|
105
|
+
`
|
|
106
|
+
-- Members can read their own org
|
|
107
|
+
CREATE POLICY "organizations_select" ON public.organizations
|
|
108
|
+
FOR SELECT TO authenticated
|
|
109
|
+
USING (id IN (SELECT public.auth_user_organizations()));`,
|
|
110
|
+
`
|
|
111
|
+
-- Only superadmins can insert
|
|
112
|
+
CREATE POLICY "organizations_insert" ON public.organizations
|
|
113
|
+
FOR INSERT TO authenticated
|
|
114
|
+
WITH CHECK (public.auth_is_superadmin());`,
|
|
115
|
+
`
|
|
116
|
+
-- Only superadmins can update
|
|
117
|
+
CREATE POLICY "organizations_update" ON public.organizations
|
|
118
|
+
FOR UPDATE TO authenticated
|
|
119
|
+
USING (public.auth_is_superadmin())
|
|
120
|
+
WITH CHECK (public.auth_is_superadmin());`,
|
|
121
|
+
`
|
|
122
|
+
-- Only superadmins can delete
|
|
123
|
+
CREATE POLICY "organizations_delete" ON public.organizations
|
|
124
|
+
FOR DELETE TO authenticated
|
|
125
|
+
USING (public.auth_is_superadmin());`,
|
|
126
|
+
];
|
|
127
|
+
break;
|
|
128
|
+
case 'users_public':
|
|
129
|
+
policies = [
|
|
130
|
+
`
|
|
131
|
+
-- Users can see themselves + admins can see org members
|
|
132
|
+
CREATE POLICY "users_public_select" ON public.users_public
|
|
133
|
+
FOR SELECT TO authenticated
|
|
134
|
+
USING (
|
|
135
|
+
id = auth.uid()
|
|
136
|
+
OR active_organization_id IN (SELECT public.auth_admin_organizations())
|
|
137
|
+
);`,
|
|
138
|
+
`
|
|
139
|
+
-- Users can only insert their own record
|
|
140
|
+
CREATE POLICY "users_public_insert" ON public.users_public
|
|
141
|
+
FOR INSERT TO authenticated
|
|
142
|
+
WITH CHECK (id = auth.uid());`,
|
|
143
|
+
`
|
|
144
|
+
-- Users can update own profile, admins can update org members
|
|
145
|
+
CREATE POLICY "users_public_update" ON public.users_public
|
|
146
|
+
FOR UPDATE TO authenticated
|
|
147
|
+
USING (
|
|
148
|
+
id = auth.uid()
|
|
149
|
+
OR active_organization_id IN (SELECT public.auth_admin_organizations())
|
|
150
|
+
)
|
|
151
|
+
WITH CHECK (
|
|
152
|
+
id = auth.uid()
|
|
153
|
+
OR active_organization_id IN (SELECT public.auth_admin_organizations())
|
|
154
|
+
);`,
|
|
155
|
+
`
|
|
156
|
+
-- Only admins can delete org members
|
|
157
|
+
CREATE POLICY "users_public_delete" ON public.users_public
|
|
158
|
+
FOR DELETE TO authenticated
|
|
159
|
+
USING (active_organization_id IN (SELECT public.auth_admin_organizations()));`,
|
|
160
|
+
];
|
|
161
|
+
break;
|
|
162
|
+
case 'organization_members':
|
|
163
|
+
case 'organization_invites':
|
|
164
|
+
policies = [
|
|
165
|
+
`
|
|
166
|
+
-- Block ALL client access. Only service_role can manage ${tableName}.
|
|
167
|
+
CREATE POLICY "${tableName}_service_only" ON public.${tableName}
|
|
168
|
+
FOR ALL
|
|
169
|
+
USING (false);`,
|
|
170
|
+
];
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
policies = [];
|
|
174
|
+
}
|
|
175
|
+
const policyNames = [];
|
|
176
|
+
for (const p of policies) {
|
|
177
|
+
const match = p.match(/CREATE POLICY "([^"]+)"/);
|
|
178
|
+
if (match)
|
|
179
|
+
policyNames.push(match[1]);
|
|
180
|
+
}
|
|
181
|
+
const sql = [header, drops, '\n-- Policies', ...policies].join('\n');
|
|
182
|
+
return { sql, policyCount: policyNames.length, tableType: 'service', policyNames };
|
|
183
|
+
}
|
|
184
|
+
// ─── Helpers ───────────────────────────────────────────────────
|
|
185
|
+
function generateHeader(tableName, tableType) {
|
|
186
|
+
return `-- ============================================================================
|
|
187
|
+
-- RLS Policies for ${tableName}
|
|
188
|
+
-- ============================================================================
|
|
189
|
+
-- Table type: ${tableType}
|
|
190
|
+
-- Org column: ${ORG_COL} (Tetra standard, no exceptions)
|
|
191
|
+
-- User column: ${USER_COL} (Tetra standard, no exceptions)
|
|
192
|
+
-- Generated by: Tetra RLS Generator v1.0
|
|
193
|
+
-- Generated at: ${new Date().toISOString()}
|
|
194
|
+
-- ============================================================================
|
|
195
|
+
|
|
196
|
+
-- Enable RLS
|
|
197
|
+
ALTER TABLE public.${tableName} ENABLE ROW LEVEL SECURITY;
|
|
198
|
+
ALTER TABLE public.${tableName} FORCE ROW LEVEL SECURITY;
|
|
199
|
+
|
|
200
|
+
-- Drop existing policies (idempotent)`;
|
|
201
|
+
}
|
|
202
|
+
function generateDrops(tableName, prefix) {
|
|
203
|
+
const names = [
|
|
204
|
+
`${prefix}_select`, `${prefix}_insert`, `${prefix}_update`, `${prefix}_delete`,
|
|
205
|
+
`${prefix}_public_select`, `${prefix}_active_select`,
|
|
206
|
+
`${prefix}_anon_insert`, `${prefix}_service_only`,
|
|
207
|
+
];
|
|
208
|
+
return names.map(p => `DROP POLICY IF EXISTS "${p}" ON public.${tableName};`).join('\n');
|
|
209
|
+
}
|
|
210
|
+
// ─── Admin Policies ────────────────────────────────────────────
|
|
211
|
+
function generateAdminPolicies(table, prefix) {
|
|
212
|
+
return [
|
|
213
|
+
`
|
|
214
|
+
CREATE POLICY "${prefix}_select" ON public.${table}
|
|
215
|
+
FOR SELECT TO authenticated
|
|
216
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
217
|
+
`
|
|
218
|
+
CREATE POLICY "${prefix}_insert" ON public.${table}
|
|
219
|
+
FOR INSERT TO authenticated
|
|
220
|
+
WITH CHECK (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
221
|
+
`
|
|
222
|
+
CREATE POLICY "${prefix}_update" ON public.${table}
|
|
223
|
+
FOR UPDATE TO authenticated
|
|
224
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()))
|
|
225
|
+
WITH CHECK (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
226
|
+
`
|
|
227
|
+
CREATE POLICY "${prefix}_delete" ON public.${table}
|
|
228
|
+
FOR DELETE TO authenticated
|
|
229
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
// ─── User Policies ─────────────────────────────────────────────
|
|
233
|
+
function generateUserPolicies(table, prefix) {
|
|
234
|
+
return [
|
|
235
|
+
`
|
|
236
|
+
-- Admins see all org data, users see only their own records
|
|
237
|
+
CREATE POLICY "${prefix}_select" ON public.${table}
|
|
238
|
+
FOR SELECT TO authenticated
|
|
239
|
+
USING (
|
|
240
|
+
${ORG_COL} IN (SELECT public.auth_admin_organizations())
|
|
241
|
+
OR (${ORG_COL} IN (SELECT public.auth_user_organizations()) AND ${USER_COL} = auth.uid())
|
|
242
|
+
);`,
|
|
243
|
+
`
|
|
244
|
+
-- Users can insert for their own org, tied to their user ID
|
|
245
|
+
CREATE POLICY "${prefix}_insert" ON public.${table}
|
|
246
|
+
FOR INSERT TO authenticated
|
|
247
|
+
WITH CHECK (
|
|
248
|
+
${ORG_COL} IN (SELECT public.auth_user_organizations())
|
|
249
|
+
AND ${USER_COL} = auth.uid()
|
|
250
|
+
);`,
|
|
251
|
+
`
|
|
252
|
+
-- Admins can update all org records, users only their own
|
|
253
|
+
CREATE POLICY "${prefix}_update" ON public.${table}
|
|
254
|
+
FOR UPDATE TO authenticated
|
|
255
|
+
USING (
|
|
256
|
+
${ORG_COL} IN (SELECT public.auth_admin_organizations())
|
|
257
|
+
OR (${ORG_COL} IN (SELECT public.auth_user_organizations()) AND ${USER_COL} = auth.uid())
|
|
258
|
+
)
|
|
259
|
+
WITH CHECK (
|
|
260
|
+
${ORG_COL} IN (SELECT public.auth_admin_organizations())
|
|
261
|
+
OR (${ORG_COL} IN (SELECT public.auth_user_organizations()) AND ${USER_COL} = auth.uid())
|
|
262
|
+
);`,
|
|
263
|
+
`
|
|
264
|
+
-- Only admins can delete
|
|
265
|
+
CREATE POLICY "${prefix}_delete" ON public.${table}
|
|
266
|
+
FOR DELETE TO authenticated
|
|
267
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
// ─── Creator Policies ──────────────────────────────────────────
|
|
271
|
+
function generateCreatorPolicies(table, prefix, visCol, publicValues) {
|
|
272
|
+
const valuesList = publicValues.map(v => `'${v}'`).join(', ');
|
|
273
|
+
return [
|
|
274
|
+
`
|
|
275
|
+
-- Admins see all, creators see own org content if shared/public
|
|
276
|
+
CREATE POLICY "${prefix}_select" ON public.${table}
|
|
277
|
+
FOR SELECT TO authenticated
|
|
278
|
+
USING (
|
|
279
|
+
${ORG_COL} IN (SELECT public.auth_admin_organizations())
|
|
280
|
+
OR (${ORG_COL} IN (SELECT public.auth_creator_organizations()) AND ${visCol} IN (${valuesList}))
|
|
281
|
+
);`,
|
|
282
|
+
`
|
|
283
|
+
-- Creators can insert in their creator orgs
|
|
284
|
+
CREATE POLICY "${prefix}_insert" ON public.${table}
|
|
285
|
+
FOR INSERT TO authenticated
|
|
286
|
+
WITH CHECK (${ORG_COL} IN (SELECT public.auth_creator_organizations()));`,
|
|
287
|
+
`
|
|
288
|
+
-- Only admins can update
|
|
289
|
+
CREATE POLICY "${prefix}_update" ON public.${table}
|
|
290
|
+
FOR UPDATE TO authenticated
|
|
291
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()))
|
|
292
|
+
WITH CHECK (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
293
|
+
`
|
|
294
|
+
-- Only admins can delete
|
|
295
|
+
CREATE POLICY "${prefix}_delete" ON public.${table}
|
|
296
|
+
FOR DELETE TO authenticated
|
|
297
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
// ─── Public Policies ───────────────────────────────────────────
|
|
301
|
+
function generatePublicPolicies(table, prefix, activeOnly, allowAnonInsert) {
|
|
302
|
+
const selectPolicy = activeOnly
|
|
303
|
+
? `
|
|
304
|
+
-- Anyone can read active records
|
|
305
|
+
CREATE POLICY "${prefix}_active_select" ON public.${table}
|
|
306
|
+
FOR SELECT
|
|
307
|
+
USING (is_active = true);`
|
|
308
|
+
: `
|
|
309
|
+
-- Anyone can read
|
|
310
|
+
CREATE POLICY "${prefix}_public_select" ON public.${table}
|
|
311
|
+
FOR SELECT
|
|
312
|
+
USING (true);`;
|
|
313
|
+
const policies = [
|
|
314
|
+
selectPolicy,
|
|
315
|
+
`
|
|
316
|
+
-- Only admins can insert
|
|
317
|
+
CREATE POLICY "${prefix}_insert" ON public.${table}
|
|
318
|
+
FOR INSERT TO authenticated
|
|
319
|
+
WITH CHECK (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
320
|
+
`
|
|
321
|
+
-- Only admins can update
|
|
322
|
+
CREATE POLICY "${prefix}_update" ON public.${table}
|
|
323
|
+
FOR UPDATE TO authenticated
|
|
324
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()))
|
|
325
|
+
WITH CHECK (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
326
|
+
`
|
|
327
|
+
-- Only admins can delete
|
|
328
|
+
CREATE POLICY "${prefix}_delete" ON public.${table}
|
|
329
|
+
FOR DELETE TO authenticated
|
|
330
|
+
USING (${ORG_COL} IN (SELECT public.auth_admin_organizations()));`,
|
|
331
|
+
];
|
|
332
|
+
if (allowAnonInsert) {
|
|
333
|
+
policies.push(`
|
|
334
|
+
-- Anonymous users can insert (signups, contact forms)
|
|
335
|
+
CREATE POLICY "${prefix}_anon_insert" ON public.${table}
|
|
336
|
+
FOR INSERT TO anon
|
|
337
|
+
WITH CHECK (true);`);
|
|
338
|
+
}
|
|
339
|
+
return policies;
|
|
340
|
+
}
|
|
341
|
+
// ─── Service Policies ──────────────────────────────────────────
|
|
342
|
+
function generateServicePolicies(table, prefix) {
|
|
343
|
+
return [
|
|
344
|
+
`
|
|
345
|
+
-- Block ALL client access. Only service_role (which bypasses RLS) can access.
|
|
346
|
+
CREATE POLICY "${prefix}_service_only" ON public.${table}
|
|
347
|
+
FOR ALL
|
|
348
|
+
USING (false);`,
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
// ─── Superadmin Policies ───────────────────────────────────────
|
|
352
|
+
function generateSuperadminPolicies(table, prefix) {
|
|
353
|
+
return [
|
|
354
|
+
`
|
|
355
|
+
-- Members can read their own org records
|
|
356
|
+
CREATE POLICY "${prefix}_select" ON public.${table}
|
|
357
|
+
FOR SELECT TO authenticated
|
|
358
|
+
USING (${ORG_COL} IN (SELECT public.auth_user_organizations()));`,
|
|
359
|
+
`
|
|
360
|
+
-- Only superadmins can insert
|
|
361
|
+
CREATE POLICY "${prefix}_insert" ON public.${table}
|
|
362
|
+
FOR INSERT TO authenticated
|
|
363
|
+
WITH CHECK (public.auth_is_superadmin());`,
|
|
364
|
+
`
|
|
365
|
+
-- Only superadmins can update
|
|
366
|
+
CREATE POLICY "${prefix}_update" ON public.${table}
|
|
367
|
+
FOR UPDATE TO authenticated
|
|
368
|
+
USING (public.auth_is_superadmin())
|
|
369
|
+
WITH CHECK (public.auth_is_superadmin());`,
|
|
370
|
+
`
|
|
371
|
+
-- Only superadmins can delete
|
|
372
|
+
CREATE POLICY "${prefix}_delete" ON public.${table}
|
|
373
|
+
FOR DELETE TO authenticated
|
|
374
|
+
USING (public.auth_is_superadmin());`,
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
// ─── Batch Generator ───────────────────────────────────────────
|
|
378
|
+
/**
|
|
379
|
+
* Generate RLS for multiple tables at once.
|
|
380
|
+
*
|
|
381
|
+
* Usage:
|
|
382
|
+
* const sql = generateRLSBatch([
|
|
383
|
+
* { tableName: 'appointments', tableType: 'admin' },
|
|
384
|
+
* { tableName: 'organizations', tableType: 'superadmin' },
|
|
385
|
+
* { tableName: 'organization_invites', tableType: 'service' },
|
|
386
|
+
* ]);
|
|
387
|
+
*/
|
|
388
|
+
export function generateRLSBatch(configs) {
|
|
389
|
+
const results = configs.map(c => generateRLS(c));
|
|
390
|
+
const totalPolicies = results.reduce((sum, r) => sum + r.policyCount, 0);
|
|
391
|
+
const header = `-- ============================================================================
|
|
392
|
+
-- Tetra RLS Migration (Batch)
|
|
393
|
+
-- ============================================================================
|
|
394
|
+
-- Tables: ${configs.length}
|
|
395
|
+
-- Policies: ${totalPolicies}
|
|
396
|
+
-- Generated by: Tetra RLS Generator v1.0
|
|
397
|
+
-- Generated at: ${new Date().toISOString()}
|
|
398
|
+
-- ============================================================================
|
|
399
|
+
`;
|
|
400
|
+
return header + results.map(r => r.sql).join('\n\n');
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=rls-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls-generator.js","sourceRoot":"","sources":["../../src/generators/rls-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail RPC Generator
|
|
3
|
+
* Auto-generates SQL RPC functions for fetching single records with nested includes
|
|
4
|
+
*
|
|
5
|
+
* This generator creates get_<table>_detail(p_id uuid) RPC functions that:
|
|
6
|
+
* - Efficiently join nested relations using LATERAL joins
|
|
7
|
+
* - Apply computed fields at each nesting level
|
|
8
|
+
* - Return single JSONB object with all nested data
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const generator = new DetailRPCGenerator(productionsFeatureConfig);
|
|
12
|
+
* const detailSQL = generator.generate();
|
|
13
|
+
* generator.writeMigration();
|
|
14
|
+
*/
|
|
15
|
+
import { FeatureConfig } from '../../shared/types/feature-config.js';
|
|
16
|
+
export interface DetailRPCGeneratorOptions {
|
|
17
|
+
/** Custom table alias map */
|
|
18
|
+
aliasMap?: Record<string, string>;
|
|
19
|
+
/** Base fields to include in detail response (overrides auto-detection) */
|
|
20
|
+
baseFields?: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare class DetailRPCGenerator {
|
|
23
|
+
private config;
|
|
24
|
+
private tableName;
|
|
25
|
+
private tableAlias;
|
|
26
|
+
private aliasMap?;
|
|
27
|
+
private baseFieldsOverride?;
|
|
28
|
+
private readonly GENERATOR_VERSION;
|
|
29
|
+
constructor(config: FeatureConfig<any>, options?: DetailRPCGeneratorOptions);
|
|
30
|
+
/**
|
|
31
|
+
* Generate the detail RPC SQL
|
|
32
|
+
*/
|
|
33
|
+
generate(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Generate base table fields for SELECT
|
|
36
|
+
*/
|
|
37
|
+
private generateBaseFields;
|
|
38
|
+
/**
|
|
39
|
+
* Generate subquery for an include
|
|
40
|
+
*/
|
|
41
|
+
private generateIncludeSubquery;
|
|
42
|
+
/**
|
|
43
|
+
* Get JOIN condition based on foreign key
|
|
44
|
+
*/
|
|
45
|
+
private getJoinCondition;
|
|
46
|
+
/**
|
|
47
|
+
* Extract column name from foreign key name
|
|
48
|
+
* Example: 'productionitems_user_id_fkey' -> 'user_id'
|
|
49
|
+
*/
|
|
50
|
+
private extractColumnFromForeignKey;
|
|
51
|
+
/**
|
|
52
|
+
* Write migration file
|
|
53
|
+
*/
|
|
54
|
+
writeMigration(options?: {
|
|
55
|
+
outputDir?: string;
|
|
56
|
+
}): string;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=detail-rpc-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detail-rpc-generator.d.ts","sourceRoot":"","sources":["../../../src/generators/rpc/detail-rpc-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAIrE,MAAM,WAAW,yBAAyB;IACxC,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,CAAyB;IAC1C,OAAO,CAAC,kBAAkB,CAAC,CAAW;IACtC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAE/B,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,yBAAyB;IAQ3E;;OAEG;IACH,QAAQ,IAAI,MAAM;IAiClB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkB1B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IA+C/B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAgBxB;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IASnC;;OAEG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM;CAkBzD"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail RPC Generator
|
|
3
|
+
* Auto-generates SQL RPC functions for fetching single records with nested includes
|
|
4
|
+
*
|
|
5
|
+
* This generator creates get_<table>_detail(p_id uuid) RPC functions that:
|
|
6
|
+
* - Efficiently join nested relations using LATERAL joins
|
|
7
|
+
* - Apply computed fields at each nesting level
|
|
8
|
+
* - Return single JSONB object with all nested data
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const generator = new DetailRPCGenerator(productionsFeatureConfig);
|
|
12
|
+
* const detailSQL = generator.generate();
|
|
13
|
+
* generator.writeMigration();
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { generateTimestamp, getTableAlias } from './utils.js';
|
|
18
|
+
export class DetailRPCGenerator {
|
|
19
|
+
config;
|
|
20
|
+
tableName;
|
|
21
|
+
tableAlias;
|
|
22
|
+
aliasMap;
|
|
23
|
+
baseFieldsOverride;
|
|
24
|
+
GENERATOR_VERSION = '1.0';
|
|
25
|
+
constructor(config, options) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.tableName = config.tableName;
|
|
28
|
+
this.aliasMap = options?.aliasMap;
|
|
29
|
+
this.tableAlias = getTableAlias(this.tableName, this.aliasMap);
|
|
30
|
+
this.baseFieldsOverride = options?.baseFields;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate the detail RPC SQL
|
|
34
|
+
*/
|
|
35
|
+
generate() {
|
|
36
|
+
const rpcName = `get_${this.tableName}_detail`;
|
|
37
|
+
const includeMapping = this.config.includeMapping || {};
|
|
38
|
+
// Build SELECT for base table fields
|
|
39
|
+
const baseFields = this.generateBaseFields();
|
|
40
|
+
// Build nested includes
|
|
41
|
+
const nestedIncludes = Object.entries(includeMapping)
|
|
42
|
+
.map(([key, config]) => this.generateIncludeSubquery(key, config, this.tableAlias))
|
|
43
|
+
.join(',\n ');
|
|
44
|
+
const sql = `-- ============================================
|
|
45
|
+
-- Detail RPC for ${this.tableName}
|
|
46
|
+
-- Generated by DetailRPCGenerator v${this.GENERATOR_VERSION}
|
|
47
|
+
-- ============================================
|
|
48
|
+
|
|
49
|
+
CREATE OR REPLACE FUNCTION ${rpcName}(p_id uuid)
|
|
50
|
+
RETURNS jsonb AS $$
|
|
51
|
+
SELECT jsonb_build_object(
|
|
52
|
+
${baseFields}${nestedIncludes ? ',\n ' + nestedIncludes : ''}
|
|
53
|
+
)
|
|
54
|
+
FROM ${this.tableName} ${this.tableAlias}
|
|
55
|
+
WHERE ${this.tableAlias}.id = p_id;
|
|
56
|
+
$$ LANGUAGE sql STABLE;
|
|
57
|
+
|
|
58
|
+
-- Usage:
|
|
59
|
+
-- SELECT * FROM ${rpcName}('uuid-here');
|
|
60
|
+
`;
|
|
61
|
+
return sql;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate base table fields for SELECT
|
|
65
|
+
*/
|
|
66
|
+
generateBaseFields() {
|
|
67
|
+
if (this.baseFieldsOverride) {
|
|
68
|
+
return this.baseFieldsOverride
|
|
69
|
+
.map(f => `'${f}', ${this.tableAlias}.${f}`)
|
|
70
|
+
.join(',\n ');
|
|
71
|
+
}
|
|
72
|
+
// Default base fields - projects should override via options.baseFields
|
|
73
|
+
return `'id', ${this.tableAlias}.id,
|
|
74
|
+
'created_at', ${this.tableAlias}.created_at,
|
|
75
|
+
'title', ${this.tableAlias}.title,
|
|
76
|
+
'status', ${this.tableAlias}.status,
|
|
77
|
+
'notes', ${this.tableAlias}.notes,
|
|
78
|
+
'user_id', ${this.tableAlias}.user_id,
|
|
79
|
+
'organization_id', ${this.tableAlias}.organization_id,
|
|
80
|
+
'current_phase', ${this.tableAlias}.current_phase`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Generate subquery for an include
|
|
84
|
+
*/
|
|
85
|
+
generateIncludeSubquery(includeKey, includeConfig, parentAlias, depth = 0) {
|
|
86
|
+
const indent = ' '.repeat(depth + 2);
|
|
87
|
+
const childTableName = includeConfig.tableName || includeKey;
|
|
88
|
+
const childAlias = `${getTableAlias(childTableName, this.aliasMap)}${depth}`;
|
|
89
|
+
// Determine join condition
|
|
90
|
+
const joinCondition = this.getJoinCondition(includeConfig, parentAlias, childAlias);
|
|
91
|
+
// Build fields for this level
|
|
92
|
+
const fields = includeConfig.fields
|
|
93
|
+
.map(field => `'${field}', ${childAlias}.${field}`)
|
|
94
|
+
.join(',\n' + indent + ' ');
|
|
95
|
+
// Build computed fields for this level
|
|
96
|
+
const computedFields = includeConfig.computedFields
|
|
97
|
+
? Object.entries(includeConfig.computedFields)
|
|
98
|
+
.map(([key, expr]) => `'${key}', ${expr.replace(/\b(\w+)\b/g, `${childAlias}.$1`)}`)
|
|
99
|
+
.join(',\n' + indent + ' ')
|
|
100
|
+
: '';
|
|
101
|
+
// Build nested includes for this level
|
|
102
|
+
const nestedIncludes = includeConfig.nested
|
|
103
|
+
? Object.entries(includeConfig.nested)
|
|
104
|
+
.map(([nestedKey, nestedConfig]) => this.generateIncludeSubquery(nestedKey, nestedConfig, childAlias, depth + 1))
|
|
105
|
+
.join(',\n' + indent + ' ')
|
|
106
|
+
: '';
|
|
107
|
+
const allFields = [fields, computedFields, nestedIncludes]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(',\n' + indent + ' ');
|
|
110
|
+
return `'${includeKey}', (
|
|
111
|
+
${indent} SELECT jsonb_agg(jsonb_build_object(
|
|
112
|
+
${indent} ${allFields}
|
|
113
|
+
${indent} ))
|
|
114
|
+
${indent} FROM ${childTableName} ${childAlias}
|
|
115
|
+
${indent} ${joinCondition}
|
|
116
|
+
${indent})`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get JOIN condition based on foreign key
|
|
120
|
+
*/
|
|
121
|
+
getJoinCondition(includeConfig, parentAlias, childAlias) {
|
|
122
|
+
if (includeConfig.isReverseRelation) {
|
|
123
|
+
// Reverse relation: child points to parent
|
|
124
|
+
const foreignKeyColumn = includeConfig.column || this.extractColumnFromForeignKey(includeConfig.foreignKey);
|
|
125
|
+
return `WHERE ${childAlias}.${foreignKeyColumn} = ${parentAlias}.id`;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Forward relation: parent points to child
|
|
129
|
+
const foreignKeyColumn = includeConfig.column || this.extractColumnFromForeignKey(includeConfig.foreignKey);
|
|
130
|
+
return `WHERE ${childAlias}.id = ${parentAlias}.${foreignKeyColumn}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract column name from foreign key name
|
|
135
|
+
* Example: 'productionitems_user_id_fkey' -> 'user_id'
|
|
136
|
+
*/
|
|
137
|
+
extractColumnFromForeignKey(foreignKey) {
|
|
138
|
+
if (!foreignKey)
|
|
139
|
+
return 'id';
|
|
140
|
+
// Pattern: tablename_columnname_fkey
|
|
141
|
+
const parts = foreignKey.split('_');
|
|
142
|
+
// Remove first part (table name) and last part ('fkey')
|
|
143
|
+
return parts.slice(1, -1).join('_');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Write migration file
|
|
147
|
+
*/
|
|
148
|
+
writeMigration(options) {
|
|
149
|
+
const timestamp = generateTimestamp();
|
|
150
|
+
const fileName = `${timestamp}_create_get_${this.tableName}_detail.sql`;
|
|
151
|
+
const migrationsDir = options?.outputDir || path.join(process.cwd(), 'supabase', 'migrations');
|
|
152
|
+
const filePath = path.join(migrationsDir, fileName);
|
|
153
|
+
const sql = this.generate();
|
|
154
|
+
// Ensure migrations directory exists
|
|
155
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
156
|
+
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
fs.writeFileSync(filePath, sql);
|
|
159
|
+
console.log(`Created migration: ${fileName}`);
|
|
160
|
+
return filePath;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=detail-rpc-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detail-rpc-generator.js","sourceRoot":"","sources":["../../../src/generators/rpc/detail-rpc-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Generator — Auto-generates SQL RPC functions from FeatureConfig
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { RPCGenerator, DetailRPCGenerator, validateConfig } from '@soulbatical/tetra-core';
|
|
7
|
+
*
|
|
8
|
+
* const generator = new RPCGenerator(ordersFeatureConfig, {
|
|
9
|
+
* aliasMap: { orders: 'o', orderitems: 'oi' },
|
|
10
|
+
* });
|
|
11
|
+
* const { resultsSQL, countsSQL } = generator.generate();
|
|
12
|
+
* generator.writeMigrations({ force: true });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export { RPCGenerator } from './rpc-generator.js';
|
|
16
|
+
export type { GeneratedSQL, FilterDefinition, RPCGeneratorOptions } from './rpc-generator.js';
|
|
17
|
+
export { DetailRPCGenerator } from './detail-rpc-generator.js';
|
|
18
|
+
export type { DetailRPCGeneratorOptions } from './detail-rpc-generator.js';
|
|
19
|
+
export { validateConfig } from './validator.js';
|
|
20
|
+
export type { ValidationResult, ValidationError } from './validator.js';
|
|
21
|
+
export { generateAuthCheck, generateAuthWhereClause, generateAuthDeclarations } from './templates/auth.js';
|
|
22
|
+
export type { AccessLevel, CreatorVisibilityConfig } from './templates/auth.js';
|
|
23
|
+
export { generateTimestamp, getTableAlias, escapeIdentifier, capitalize, kebabToCamelCase, snakeToCamelCase, filterNameToCountsKey, indent, } from './utils.js';
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|