@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,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Generator
|
|
3
|
+
* Auto-generates SQL RPC functions from FeatureConfig
|
|
4
|
+
*
|
|
5
|
+
* This is the core of the SQL Generator system. It takes a FeatureConfig
|
|
6
|
+
* and generates two SQL migration files:
|
|
7
|
+
* 1. get_<table>_results.sql - Results function (paginated, with includes)
|
|
8
|
+
* 2. get_<table>_counts.sql - Counts function (filter breakdowns)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const generator = new RPCGenerator(ordersFeatureConfig);
|
|
12
|
+
* const { resultsSQL, countsSQL } = generator.generate();
|
|
13
|
+
* generator.writeMigrations();
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { generateTimestamp, getTableAlias, escapeIdentifier } from './utils.js';
|
|
18
|
+
import { validateConfig } from './validator.js';
|
|
19
|
+
// Import filter templates
|
|
20
|
+
import { generateEnumWhere, generateEnumCounts } from './templates/enum.js';
|
|
21
|
+
import { generateColumnWhere, generateColumnCounts } from './templates/column.js';
|
|
22
|
+
import { generateNullableWhere, generateNullableCounts } from './templates/nullable.js';
|
|
23
|
+
import { generateRelatedWhere, generateRelatedCounts } from './templates/related.js';
|
|
24
|
+
import { generateTimeWhere, generateTimeCounts } from './templates/time.js';
|
|
25
|
+
import { generateSearchWhere, generateSearchCounts } from './templates/search.js';
|
|
26
|
+
import { generateArrayWhere, generateArrayCounts } from './templates/array.js';
|
|
27
|
+
// Security template (v3.9 → v3.15: Service role + anon blocking + session_user fix)
|
|
28
|
+
import { generateAuthCheck, generateAuthWhereClause, generateAuthDeclarations } from './templates/auth.js';
|
|
29
|
+
export class RPCGenerator {
|
|
30
|
+
config;
|
|
31
|
+
tableName;
|
|
32
|
+
tableAlias;
|
|
33
|
+
aliasMap;
|
|
34
|
+
userIdColumn;
|
|
35
|
+
searchJoins;
|
|
36
|
+
GENERATOR_VERSION = '3.17'; // v3.17: Document public. prefix requirement for customWhereClause/sqlComputedFields (January 16, 2026)
|
|
37
|
+
ENABLE_RPC_INCLUDES = true; // Enabled - LATERAL JOINs for selective includes (pagination limits dataset!)
|
|
38
|
+
constructor(config, options) {
|
|
39
|
+
// Validate config first
|
|
40
|
+
const validation = validateConfig(config);
|
|
41
|
+
if (!validation.valid) {
|
|
42
|
+
throw new Error(`Invalid feature config:\n${this.formatValidationErrors(validation)}`);
|
|
43
|
+
}
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.tableName = config.tableName;
|
|
46
|
+
this.aliasMap = options?.aliasMap;
|
|
47
|
+
this.tableAlias = getTableAlias(this.tableName, this.aliasMap);
|
|
48
|
+
this.userIdColumn = options?.userIdColumn || 'user_id';
|
|
49
|
+
this.searchJoins = options?.searchJoins || {
|
|
50
|
+
user: (alias) => `LEFT JOIN public.users_public u ON u.id = ${alias}.${this.userIdColumn}`,
|
|
51
|
+
order: (alias) => `LEFT JOIN public.orders o ON o.id = ${alias}.order_id`,
|
|
52
|
+
};
|
|
53
|
+
// Log warnings
|
|
54
|
+
if (validation.warnings.length > 0) {
|
|
55
|
+
console.warn('Warning: Validation warnings:');
|
|
56
|
+
validation.warnings.forEach(w => {
|
|
57
|
+
console.warn(` ${w.field}: ${w.message}`);
|
|
58
|
+
if (w.suggestion)
|
|
59
|
+
console.warn(` Suggestion: ${w.suggestion}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Format validation errors for display
|
|
65
|
+
*/
|
|
66
|
+
formatValidationErrors(validation) {
|
|
67
|
+
const errors = validation.errors.map(e => ` - ${e.field}: ${e.message}${e.suggestion ? `\n Suggestion: ${e.suggestion}` : ''}`);
|
|
68
|
+
return errors.join('\n');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse filters from config
|
|
72
|
+
*/
|
|
73
|
+
parseFilters() {
|
|
74
|
+
if (!this.config.filters)
|
|
75
|
+
return [];
|
|
76
|
+
return this.config.filters.map(filter => ({
|
|
77
|
+
...filter,
|
|
78
|
+
paramName: `p_${filter.rpcParam || filter.name}`,
|
|
79
|
+
paramType: this.getParamType(filter)
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get SQL parameter type for filter
|
|
84
|
+
*/
|
|
85
|
+
getParamType(filter) {
|
|
86
|
+
switch (filter.type) {
|
|
87
|
+
case 'multiselect':
|
|
88
|
+
return 'text[]'; // Array for multiselect
|
|
89
|
+
case 'daterange':
|
|
90
|
+
return 'daterange'; // PostgreSQL daterange type
|
|
91
|
+
case 'array':
|
|
92
|
+
// Support custom array type via sqlGeneration.arrayType (e.g., 'text[]' for varchar columns)
|
|
93
|
+
return filter.sqlGeneration?.arrayType || 'uuid[]'; // Default: UUID[] for by-ids navigation
|
|
94
|
+
default:
|
|
95
|
+
return 'text'; // Most filters use text
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate filter function signature
|
|
100
|
+
*/
|
|
101
|
+
generateFilterSignature() {
|
|
102
|
+
const params = [
|
|
103
|
+
`p_org_id uuid`
|
|
104
|
+
];
|
|
105
|
+
// Add all filter parameters
|
|
106
|
+
this.parseFilters().forEach(filter => {
|
|
107
|
+
params.push(`${filter.paramName} ${filter.paramType} DEFAULT NULL`);
|
|
108
|
+
});
|
|
109
|
+
// Add sort parameter
|
|
110
|
+
params.push(`p_sort_by text DEFAULT 'date-desc'`);
|
|
111
|
+
// Use filterRpcName if specified, otherwise generate from tableName
|
|
112
|
+
if (this.config.filterRpcName) {
|
|
113
|
+
return `${this.config.filterRpcName}(\n ${params.join(',\n ')}\n)`;
|
|
114
|
+
}
|
|
115
|
+
return `filter_${this.config.tableName}_simple(\n ${params.join(',\n ')}\n)`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generate org WHERE clause (includes system content where organization_id IS NULL)
|
|
119
|
+
*/
|
|
120
|
+
generateOrgWhereClause(alias) {
|
|
121
|
+
const f = `${alias}.${this.config.organizationIdField}`;
|
|
122
|
+
return `(p_org_id IS NULL OR ${f} = p_org_id OR ${f} IS NULL)`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generate WHERE clause for a filter
|
|
126
|
+
*/
|
|
127
|
+
generateWhereClause(filter) {
|
|
128
|
+
// Check for custom WHERE clause
|
|
129
|
+
if (filter.sqlGeneration?.customWhereClause) {
|
|
130
|
+
return `\n -- ${filter.name} (custom WHERE clause)\n AND ${filter.sqlGeneration.customWhereClause}`;
|
|
131
|
+
}
|
|
132
|
+
// Use template based on filter type
|
|
133
|
+
switch (filter.type) {
|
|
134
|
+
case 'enum':
|
|
135
|
+
return generateEnumWhere(filter, this.tableName, this.aliasMap);
|
|
136
|
+
case 'column':
|
|
137
|
+
return generateColumnWhere(filter, this.tableName, this.aliasMap);
|
|
138
|
+
case 'nullable':
|
|
139
|
+
return generateNullableWhere(filter, this.tableName, this.aliasMap);
|
|
140
|
+
case 'related':
|
|
141
|
+
return generateRelatedWhere(filter, this.tableName, this.aliasMap);
|
|
142
|
+
case 'time':
|
|
143
|
+
return generateTimeWhere(filter, this.tableName, this.aliasMap);
|
|
144
|
+
case 'search':
|
|
145
|
+
return generateSearchWhere(filter, this.tableName, this.aliasMap);
|
|
146
|
+
case 'array':
|
|
147
|
+
return generateArrayWhere(filter, this.tableName, this.aliasMap);
|
|
148
|
+
default:
|
|
149
|
+
throw new Error(`Unsupported filter type: ${filter.type}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Generate all WHERE clauses
|
|
154
|
+
*/
|
|
155
|
+
generateWhereClauses() {
|
|
156
|
+
const filters = this.parseFilters();
|
|
157
|
+
if (filters.length === 0) {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
return filters.map(filter => this.generateWhereClause(filter)).join('\n');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Generate ORDER BY clause
|
|
164
|
+
*/
|
|
165
|
+
generateOrderBy() {
|
|
166
|
+
const alias = this.tableAlias;
|
|
167
|
+
const defaultSortField = this.config.defaultSortField || 'created_at';
|
|
168
|
+
const allowedSortFields = this.config.allowedSortFields || [defaultSortField];
|
|
169
|
+
// Use sortFields config to find the field for 'name' alias
|
|
170
|
+
let nameField = null;
|
|
171
|
+
let hasNameSort = false;
|
|
172
|
+
if (this.config.sortFields) {
|
|
173
|
+
// Find the sortField with alias: 'name'
|
|
174
|
+
const nameSortConfig = this.config.sortFields.find(sf => sf.alias === 'name');
|
|
175
|
+
if (nameSortConfig) {
|
|
176
|
+
nameField = nameSortConfig.field;
|
|
177
|
+
hasNameSort = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// If no sortFields config, fall back to old logic (auto-detect name field)
|
|
181
|
+
if (!this.config.sortFields) {
|
|
182
|
+
const detectedNameField = allowedSortFields.find(f => ['firstname', 'lastname', 'name', 'title', 'original_filename', 'filename'].includes(f.toLowerCase()));
|
|
183
|
+
if (detectedNameField) {
|
|
184
|
+
nameField = detectedNameField;
|
|
185
|
+
hasNameSort = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Build CASE statements for each sort option
|
|
189
|
+
let orderBy = `
|
|
190
|
+
ORDER BY
|
|
191
|
+
CASE WHEN p_sort_by = 'date-desc' THEN ${alias}.${escapeIdentifier(defaultSortField)} END DESC NULLS LAST,
|
|
192
|
+
CASE WHEN p_sort_by = 'date-asc' THEN ${alias}.${escapeIdentifier(defaultSortField)} END ASC NULLS LAST,`;
|
|
193
|
+
// Only add name sorting if we have a valid name field
|
|
194
|
+
if (hasNameSort && nameField) {
|
|
195
|
+
const nameSortExpr = `LOWER(${alias}.${escapeIdentifier(nameField)})`;
|
|
196
|
+
orderBy += `
|
|
197
|
+
CASE WHEN p_sort_by = 'name-asc' THEN ${nameSortExpr} END ASC NULLS LAST,
|
|
198
|
+
CASE WHEN p_sort_by = 'name-desc' THEN ${nameSortExpr} END DESC NULLS LAST,`;
|
|
199
|
+
}
|
|
200
|
+
orderBy += `
|
|
201
|
+
${alias}.${escapeIdentifier(defaultSortField)} DESC -- Fallback`;
|
|
202
|
+
return orderBy;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Detect required JOINs from searchFields
|
|
206
|
+
* v3.11: Add public. prefix for Postgres v17 compatibility
|
|
207
|
+
* v3.14: Also check filter-level searchFields (filter.sqlGeneration.searchFields)
|
|
208
|
+
*/
|
|
209
|
+
detectJoins() {
|
|
210
|
+
// Collect searchFields from both root config and filter configs
|
|
211
|
+
const allSearchFields = [...(this.config.searchFields || [])];
|
|
212
|
+
// v3.14: Also check filter-level searchFields
|
|
213
|
+
if (this.config.filters) {
|
|
214
|
+
this.config.filters.forEach(filter => {
|
|
215
|
+
if (filter.type === 'search' && filter.sqlGeneration?.searchFields) {
|
|
216
|
+
allSearchFields.push(...filter.sqlGeneration.searchFields);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const joinSet = new Set();
|
|
221
|
+
allSearchFields.forEach(field => {
|
|
222
|
+
if (field.includes('.')) {
|
|
223
|
+
const [relation] = field.split('.');
|
|
224
|
+
const joinFn = this.searchJoins[relation];
|
|
225
|
+
if (joinFn) {
|
|
226
|
+
joinSet.add(joinFn(this.tableAlias));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return joinSet.size > 0 ? '\n ' + Array.from(joinSet).join('\n ') : '';
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Generate complete filter function SQL
|
|
234
|
+
*/
|
|
235
|
+
generateFilterFunction() {
|
|
236
|
+
const signature = this.generateFilterSignature();
|
|
237
|
+
const whereClauses = this.generateWhereClauses();
|
|
238
|
+
const orderBy = this.generateOrderBy();
|
|
239
|
+
const alias = this.tableAlias;
|
|
240
|
+
const joins = this.detectJoins();
|
|
241
|
+
// v3.11: Add public. prefix for Postgres v17 compatibility (empty search_path)
|
|
242
|
+
return `-- ============================================================================
|
|
243
|
+
-- Filter function for ${this.tableName}
|
|
244
|
+
-- ============================================================================
|
|
245
|
+
-- DO NOT EDIT MANUALLY! This file is auto-generated.
|
|
246
|
+
-- Changes will be OVERWRITTEN on next generation.
|
|
247
|
+
--
|
|
248
|
+
-- To modify this function:
|
|
249
|
+
-- 1. Edit config: /backend/src/features/${this.tableName}/config/${this.tableName}.config.ts
|
|
250
|
+
-- 2. Regenerate: npm run generate:rpc ${this.tableName}
|
|
251
|
+
-- 3. Apply: Use MCP apply_migration or supabase db push
|
|
252
|
+
--
|
|
253
|
+
-- Generator: SQL Generator v${this.GENERATOR_VERSION}
|
|
254
|
+
-- Generated: ${new Date().toISOString()}
|
|
255
|
+
-- ============================================================================
|
|
256
|
+
|
|
257
|
+
DROP FUNCTION IF EXISTS public.${signature.split('(')[0]};
|
|
258
|
+
|
|
259
|
+
CREATE OR REPLACE FUNCTION public.${signature}
|
|
260
|
+
RETURNS uuid[]
|
|
261
|
+
LANGUAGE plpgsql
|
|
262
|
+
STABLE
|
|
263
|
+
AS $$
|
|
264
|
+
BEGIN
|
|
265
|
+
RETURN ARRAY(
|
|
266
|
+
SELECT ${alias}.id
|
|
267
|
+
FROM public.${this.tableName} ${alias}${joins}
|
|
268
|
+
WHERE ${this.generateOrgWhereClause(alias)}${whereClauses}
|
|
269
|
+
${orderBy}
|
|
270
|
+
);
|
|
271
|
+
END;
|
|
272
|
+
$$;
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Generate counts query for a filter
|
|
277
|
+
*/
|
|
278
|
+
generateCountsQuery(filter) {
|
|
279
|
+
// Check for custom counts query
|
|
280
|
+
if (filter.sqlGeneration?.customCountsQuery) {
|
|
281
|
+
return filter.sqlGeneration.customCountsQuery;
|
|
282
|
+
}
|
|
283
|
+
// Search and array filters don't have counts
|
|
284
|
+
if (filter.type === 'search') {
|
|
285
|
+
return generateSearchCounts(filter);
|
|
286
|
+
}
|
|
287
|
+
if (filter.type === 'array') {
|
|
288
|
+
// Array filters (like 'ids') don't generate counts
|
|
289
|
+
return generateArrayCounts(filter) || '';
|
|
290
|
+
}
|
|
291
|
+
// Use template based on filter type
|
|
292
|
+
switch (filter.type) {
|
|
293
|
+
case 'enum':
|
|
294
|
+
return generateEnumCounts(filter);
|
|
295
|
+
case 'column':
|
|
296
|
+
return generateColumnCounts(filter);
|
|
297
|
+
case 'nullable':
|
|
298
|
+
return generateNullableCounts(filter);
|
|
299
|
+
case 'related':
|
|
300
|
+
return generateRelatedCounts(filter, this.tableName, this.aliasMap);
|
|
301
|
+
case 'time':
|
|
302
|
+
return generateTimeCounts(filter);
|
|
303
|
+
default:
|
|
304
|
+
return ''; // No counts for unsupported types
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Generate all counts queries
|
|
309
|
+
*/
|
|
310
|
+
generateCountsQueries() {
|
|
311
|
+
const filters = this.parseFilters();
|
|
312
|
+
const countsQueries = filters
|
|
313
|
+
.map(filter => this.generateCountsQuery(filter))
|
|
314
|
+
.filter(query => query !== '') // Remove empty queries (search filters)
|
|
315
|
+
.join(',\n');
|
|
316
|
+
return countsQueries;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Generate counts function signature
|
|
320
|
+
*/
|
|
321
|
+
generateCountsSignature() {
|
|
322
|
+
const params = [
|
|
323
|
+
`p_org_id uuid DEFAULT NULL` // Optional for system-wide operations
|
|
324
|
+
];
|
|
325
|
+
// Add all filter parameters (same as filter/results function)
|
|
326
|
+
this.parseFilters().forEach(filter => {
|
|
327
|
+
params.push(`${filter.paramName} ${filter.paramType} DEFAULT NULL`);
|
|
328
|
+
});
|
|
329
|
+
// NOTE: p_sort_by is NOT added to counts - counts don't need sorting!
|
|
330
|
+
// Only results function has p_sort_by parameter
|
|
331
|
+
// Use countsRpcName if specified, otherwise generate from tableName
|
|
332
|
+
const funcName = this.config.countsRpcName || `get_${this.config.tableName}_counts`;
|
|
333
|
+
return `${funcName}(\n ${params.join(',\n ')}\n)`;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Generate complete counts function SQL
|
|
337
|
+
*/
|
|
338
|
+
generateCountsFunction() {
|
|
339
|
+
const signature = this.generateCountsSignature();
|
|
340
|
+
const whereClauses = this.generateWhereClauses();
|
|
341
|
+
const countsQueries = this.generateCountsQueries();
|
|
342
|
+
const alias = this.tableAlias;
|
|
343
|
+
const joins = this.detectJoins();
|
|
344
|
+
// v3.9 → v3.10: Generate auth check and WHERE clause based on accessLevel config
|
|
345
|
+
const accessLevel = (this.config.accessLevel || 'admin');
|
|
346
|
+
const authCheck = generateAuthCheck(accessLevel);
|
|
347
|
+
// v3.10: Generate auth WHERE clause for creator visibility filtering
|
|
348
|
+
// v3.11: Pass organizationIdField from config
|
|
349
|
+
const creatorVisibility = this.config.creatorVisibility;
|
|
350
|
+
const organizationIdField = this.config.organizationIdField || 'organization_id';
|
|
351
|
+
const authWhereClause = generateAuthWhereClause(accessLevel, alias, creatorVisibility, organizationIdField);
|
|
352
|
+
const securityNote = accessLevel === 'public' || accessLevel === 'system'
|
|
353
|
+
? `-- Access Level: ${accessLevel} (no auth check)`
|
|
354
|
+
: accessLevel === 'creator' && creatorVisibility
|
|
355
|
+
? `-- Access Level: ${accessLevel} (auth check + visibility: ${creatorVisibility.column})`
|
|
356
|
+
: `-- Access Level: ${accessLevel} (auth check enabled)`;
|
|
357
|
+
// Include joined table columns in CTE if we have joins
|
|
358
|
+
const selectClause = joins.includes('orders')
|
|
359
|
+
? `${alias}.*, o.status as order_status, o.source as order_source`
|
|
360
|
+
: `${alias}.*`;
|
|
361
|
+
return `-- ============================================================================
|
|
362
|
+
-- Counts function for ${this.tableName}
|
|
363
|
+
-- ============================================================================
|
|
364
|
+
-- DO NOT EDIT MANUALLY! This file is auto-generated.
|
|
365
|
+
-- Changes will be OVERWRITTEN on next generation.
|
|
366
|
+
--
|
|
367
|
+
-- To modify this function:
|
|
368
|
+
-- 1. Edit config: /backend/src/features/${this.tableName}/config/${this.tableName}.config.ts
|
|
369
|
+
-- 2. Regenerate: npm run generate:rpc ${this.tableName}
|
|
370
|
+
-- 3. Apply: Use MCP apply_migration or supabase db push
|
|
371
|
+
--
|
|
372
|
+
-- Generator: SQL Generator v${this.GENERATOR_VERSION}
|
|
373
|
+
-- Generated: ${new Date().toISOString()}
|
|
374
|
+
-- ${securityNote}
|
|
375
|
+
-- ============================================================================
|
|
376
|
+
|
|
377
|
+
DROP FUNCTION IF EXISTS public.${signature.split('(')[0]}_simple;
|
|
378
|
+
DROP FUNCTION IF EXISTS public.${signature.split('(')[0]};
|
|
379
|
+
|
|
380
|
+
CREATE OR REPLACE FUNCTION public.${signature}
|
|
381
|
+
RETURNS jsonb
|
|
382
|
+
LANGUAGE plpgsql
|
|
383
|
+
STABLE
|
|
384
|
+
SECURITY DEFINER
|
|
385
|
+
SET search_path = ''
|
|
386
|
+
AS $$
|
|
387
|
+
DECLARE
|
|
388
|
+
result jsonb;${generateAuthDeclarations(accessLevel)}
|
|
389
|
+
BEGIN
|
|
390
|
+
${authCheck}
|
|
391
|
+
-- Create filtered CTE for consistent counts
|
|
392
|
+
-- v3.11: Add public. prefix for Postgres v17 compatibility
|
|
393
|
+
WITH filtered_items AS (
|
|
394
|
+
SELECT ${selectClause}
|
|
395
|
+
FROM public.${this.tableName} ${alias}${joins}
|
|
396
|
+
WHERE ${this.generateOrgWhereClause(alias)}${whereClauses}
|
|
397
|
+
${authWhereClause}
|
|
398
|
+
)
|
|
399
|
+
SELECT jsonb_build_object(
|
|
400
|
+
'total', (SELECT COUNT(*)::int FROM filtered_items),${countsQueries}
|
|
401
|
+
) INTO result;
|
|
402
|
+
|
|
403
|
+
RETURN result;
|
|
404
|
+
END;
|
|
405
|
+
$$;
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Generate JSONB aggregates for includes (v3.5)
|
|
410
|
+
* Returns LATERAL JOIN subqueries that aggregate related records as JSONB arrays
|
|
411
|
+
*/
|
|
412
|
+
generateIncludeJoins() {
|
|
413
|
+
if (!this.ENABLE_RPC_INCLUDES) {
|
|
414
|
+
return '';
|
|
415
|
+
}
|
|
416
|
+
if (!this.config.includeMapping || Object.keys(this.config.includeMapping).length === 0) {
|
|
417
|
+
return '';
|
|
418
|
+
}
|
|
419
|
+
const joins = [];
|
|
420
|
+
const alias = this.tableAlias;
|
|
421
|
+
for (const [includeName, includeConfig] of Object.entries(this.config.includeMapping)) {
|
|
422
|
+
const includeAlias = includeName.substring(0, 3); // e.g., 'orders' → 'ord'
|
|
423
|
+
const fields = includeConfig.fields.map(f => `'${f}', ${includeAlias}.${f}`).join(', ');
|
|
424
|
+
// Check if this is a many-to-many relationship via junction table
|
|
425
|
+
// v3.11: Add public. prefix for Postgres v17 compatibility
|
|
426
|
+
if (includeConfig.junctionTable && includeConfig.junctionForeignKey && includeConfig.junctionTargetKey) {
|
|
427
|
+
// M:N relationship via junction table
|
|
428
|
+
const junctionAlias = 'j_' + includeAlias;
|
|
429
|
+
joins.push(`
|
|
430
|
+
LEFT JOIN LATERAL (
|
|
431
|
+
SELECT jsonb_agg(to_jsonb(${includeAlias}.*)) as ${includeName}_data
|
|
432
|
+
FROM public.${includeConfig.junctionTable} ${junctionAlias}
|
|
433
|
+
JOIN public.${includeConfig.tableName} ${includeAlias} ON ${includeAlias}.id = ${junctionAlias}.${includeConfig.junctionTargetKey}
|
|
434
|
+
WHERE ${junctionAlias}.${includeConfig.junctionForeignKey} = ${alias}.id
|
|
435
|
+
) ${includeName}_agg ON true`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
// Get foreign key column - use explicit column if provided, otherwise auto-detect
|
|
439
|
+
const foreignKeyColumn = includeConfig.column || this.getForeignKeyColumn(includeConfig.foreignKey || '');
|
|
440
|
+
// Detect join direction: is this table referencing us (1:N) or are we referencing it (N:1)?
|
|
441
|
+
const isOneToMany = (includeConfig.foreignKey || '').includes(`${includeConfig.tableName}_`);
|
|
442
|
+
if (isOneToMany) {
|
|
443
|
+
// 1:N relationship
|
|
444
|
+
// Check for nested includes (v3.8, enhanced v3.9)
|
|
445
|
+
if (includeConfig.nested && Object.keys(includeConfig.nested).length > 0) {
|
|
446
|
+
// Build nested JOINs and fields
|
|
447
|
+
const nestedJoins = [];
|
|
448
|
+
const nestedFields = [];
|
|
449
|
+
let aliasCounter = 0;
|
|
450
|
+
for (const [nestedName, nestedConfig] of Object.entries(includeConfig.nested)) {
|
|
451
|
+
const nestedAlias = `n${aliasCounter++}`;
|
|
452
|
+
const nestedFKColumn = nestedConfig.column || this.getForeignKeyColumn(nestedConfig.foreignKey || '');
|
|
453
|
+
// v3.9: Use outputName if specified, otherwise use nestedName
|
|
454
|
+
const outputName = nestedConfig.outputName || nestedName;
|
|
455
|
+
// Add LEFT JOIN for nested relation (v3.11: public. prefix)
|
|
456
|
+
nestedJoins.push(`LEFT JOIN public.${nestedConfig.tableName} ${nestedAlias} ON ${nestedAlias}.id = ${includeAlias}.${nestedFKColumn}`);
|
|
457
|
+
// v3.9: Check for deepNested
|
|
458
|
+
const deepNested = nestedConfig.deepNested;
|
|
459
|
+
if (deepNested && Object.keys(deepNested).length > 0) {
|
|
460
|
+
// Build deep nested JOINs and CASE expression
|
|
461
|
+
const deepNestedParts = [];
|
|
462
|
+
for (const [deepName, deepConfig] of Object.entries(deepNested)) {
|
|
463
|
+
const deepAlias = `d${aliasCounter++}`;
|
|
464
|
+
const deepOutputName = deepConfig.outputName || deepName;
|
|
465
|
+
// Add deep JOIN (v3.11: public. prefix)
|
|
466
|
+
nestedJoins.push(`LEFT JOIN public.${deepConfig.tableName} ${deepAlias} ON ${deepAlias}.id = ${nestedAlias}.${deepConfig.joinColumn}`);
|
|
467
|
+
// Build deep nested object
|
|
468
|
+
const deepFields = deepConfig.fields.map((f) => `'${f}', ${deepAlias}.${f}`).join(', ');
|
|
469
|
+
deepNestedParts.push(`'${deepOutputName}', CASE WHEN ${deepAlias}.id IS NOT NULL THEN jsonb_build_object(${deepFields}) ELSE NULL END`);
|
|
470
|
+
}
|
|
471
|
+
// Build the nested object with deep nested data
|
|
472
|
+
const nestedFieldsList = nestedConfig.fields.map(f => `'${f}', ${nestedAlias}.${f}`).join(', ');
|
|
473
|
+
nestedFields.push(`'${outputName}', CASE WHEN ${nestedAlias}.id IS NOT NULL THEN jsonb_build_object(${nestedFieldsList}, ${deepNestedParts.join(', ')}) ELSE NULL END`);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
// Simple nested object (no deep nesting)
|
|
477
|
+
const nestedFieldsList = nestedConfig.fields.map(f => `'${f}', ${nestedAlias}.${f}`).join(', ');
|
|
478
|
+
nestedFields.push(`'${outputName}', CASE WHEN ${nestedAlias}.id IS NOT NULL THEN jsonb_build_object(${nestedFieldsList}) ELSE NULL END`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Combine base fields with nested fields
|
|
482
|
+
const allFields = `${fields}, ${nestedFields.join(', ')}`;
|
|
483
|
+
// v3.12: Use orderBy from config if specified
|
|
484
|
+
const orderByClause = includeConfig.orderBy
|
|
485
|
+
? ` ORDER BY ${includeAlias}.${includeConfig.orderBy}`
|
|
486
|
+
: '';
|
|
487
|
+
// v3.11: Add public. prefix for Postgres v17 compatibility
|
|
488
|
+
joins.push(`
|
|
489
|
+
LEFT JOIN LATERAL (
|
|
490
|
+
SELECT COALESCE(jsonb_agg(jsonb_build_object(${allFields})${orderByClause}), '[]'::jsonb) as ${includeName}_data
|
|
491
|
+
FROM public.${includeConfig.tableName} ${includeAlias}
|
|
492
|
+
${nestedJoins.join('\n ')}
|
|
493
|
+
WHERE ${includeAlias}.${foreignKeyColumn} = ${alias}.id
|
|
494
|
+
) ${includeName}_agg ON true`);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Simple 1:N without nesting (v3.11: public. prefix)
|
|
498
|
+
joins.push(`
|
|
499
|
+
LEFT JOIN LATERAL (
|
|
500
|
+
SELECT COALESCE(jsonb_agg(jsonb_build_object(${fields})), '[]'::jsonb) as ${includeName}_data
|
|
501
|
+
FROM public.${includeConfig.tableName} ${includeAlias}
|
|
502
|
+
WHERE ${includeAlias}.${foreignKeyColumn} = ${alias}.id
|
|
503
|
+
) ${includeName}_agg ON true`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// N:1 relationship
|
|
508
|
+
// v3.11: Add public. prefix for Postgres v17 compatibility
|
|
509
|
+
joins.push(`
|
|
510
|
+
LEFT JOIN LATERAL (
|
|
511
|
+
SELECT to_jsonb(${includeAlias}.*) as ${includeName}_data
|
|
512
|
+
FROM public.${includeConfig.tableName} ${includeAlias}
|
|
513
|
+
WHERE ${includeAlias}.id = ${alias}.${foreignKeyColumn}
|
|
514
|
+
) ${includeName}_agg ON true`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return joins.join('');
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Extract column name from foreign key constraint name
|
|
521
|
+
* Example: 'orders_user_id_fkey' → 'user_id'
|
|
522
|
+
* 'users_public_active_organization_id_fkey' → 'active_organization_id'
|
|
523
|
+
*/
|
|
524
|
+
getForeignKeyColumn(foreignKeyName) {
|
|
525
|
+
// Remove _fkey suffix first
|
|
526
|
+
let column = foreignKeyName.replace(/_fkey$/, '');
|
|
527
|
+
// Try removing related table name prefix
|
|
528
|
+
for (const [_includeName, includeConfig] of Object.entries(this.config.includeMapping || {})) {
|
|
529
|
+
const tablePrefix = includeConfig.tableName + '_';
|
|
530
|
+
if (column.startsWith(tablePrefix)) {
|
|
531
|
+
column = column.substring(tablePrefix.length);
|
|
532
|
+
return column;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Try removing our table name prefix
|
|
536
|
+
const ourTablePrefix = this.config.tableName + '_';
|
|
537
|
+
if (column.startsWith(ourTablePrefix)) {
|
|
538
|
+
column = column.substring(ourTablePrefix.length);
|
|
539
|
+
}
|
|
540
|
+
return column;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Generate SELECT columns with includes as JSONB fields (v3.5)
|
|
544
|
+
*/
|
|
545
|
+
generateIncludesCases() {
|
|
546
|
+
if (!this.ENABLE_RPC_INCLUDES) {
|
|
547
|
+
return '';
|
|
548
|
+
}
|
|
549
|
+
if (!this.config.includeMapping || Object.keys(this.config.includeMapping).length === 0) {
|
|
550
|
+
return '';
|
|
551
|
+
}
|
|
552
|
+
const cases = [];
|
|
553
|
+
for (const includeName of Object.keys(this.config.includeMapping)) {
|
|
554
|
+
cases.push(`${includeName}_agg.${includeName}_data as ${includeName}`);
|
|
555
|
+
}
|
|
556
|
+
return cases.join(',\n ');
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Generate all table columns for SELECT clause
|
|
560
|
+
*/
|
|
561
|
+
generateTableColumns() {
|
|
562
|
+
return `${this.tableAlias}.*`;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Generate SQL computed fields (v3.8)
|
|
566
|
+
* Returns computed field expressions for SELECT clause
|
|
567
|
+
*/
|
|
568
|
+
generateSQLComputedFields() {
|
|
569
|
+
if (!this.config.sqlComputedFields || this.config.sqlComputedFields.length === 0) {
|
|
570
|
+
return '';
|
|
571
|
+
}
|
|
572
|
+
const fields = this.config.sqlComputedFields.map(field => {
|
|
573
|
+
// Replace {alias} placeholder with actual table alias
|
|
574
|
+
const sql = field.sql.replace(/\{alias\}/g, this.tableAlias);
|
|
575
|
+
return `${sql} as ${field.name}`;
|
|
576
|
+
});
|
|
577
|
+
return fields.join(',\n ');
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Generate results function signature
|
|
581
|
+
*/
|
|
582
|
+
generateResultsSignature() {
|
|
583
|
+
const params = [
|
|
584
|
+
`p_org_id uuid DEFAULT NULL` // Optional for system-wide cron jobs
|
|
585
|
+
];
|
|
586
|
+
// Add all filter parameters
|
|
587
|
+
this.parseFilters().forEach(filter => {
|
|
588
|
+
params.push(`${filter.paramName} ${filter.paramType} DEFAULT NULL`);
|
|
589
|
+
});
|
|
590
|
+
// Add pagination parameters
|
|
591
|
+
params.push(`p_limit int DEFAULT 20`);
|
|
592
|
+
params.push(`p_offset int DEFAULT 0`);
|
|
593
|
+
params.push(`p_sort_by text DEFAULT 'date-desc'`);
|
|
594
|
+
const funcName = this.config.resultsRpcName || `get_${this.config.tableName}_results`;
|
|
595
|
+
return `${funcName}(\n ${params.join(',\n ')}\n)`;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Generate complete results function SQL (NEW - replaces filter + .in pattern!)
|
|
599
|
+
*/
|
|
600
|
+
generateResultsFunction() {
|
|
601
|
+
const signature = this.generateResultsSignature();
|
|
602
|
+
const whereClauses = this.generateWhereClauses();
|
|
603
|
+
const orderBy = this.generateOrderBy();
|
|
604
|
+
const alias = this.tableAlias;
|
|
605
|
+
const searchJoins = this.detectJoins(); // JOINs for search
|
|
606
|
+
const includeJoins = this.generateIncludeJoins(); // JOINs for includes
|
|
607
|
+
const allJoins = searchJoins + includeJoins;
|
|
608
|
+
const tableColumns = this.generateTableColumns();
|
|
609
|
+
const includesCases = this.generateIncludesCases();
|
|
610
|
+
const computedFields = this.generateSQLComputedFields(); // v3.8: SQL computed fields
|
|
611
|
+
// v3.9 → v3.10: Generate auth check and WHERE clause based on accessLevel config
|
|
612
|
+
const accessLevel = (this.config.accessLevel || 'admin');
|
|
613
|
+
const authCheck = generateAuthCheck(accessLevel);
|
|
614
|
+
// v3.10: Generate auth WHERE clause for creator visibility filtering
|
|
615
|
+
const creatorVisibility = this.config.creatorVisibility;
|
|
616
|
+
const organizationIdField = this.config.organizationIdField || 'organization_id';
|
|
617
|
+
const authWhereClause = generateAuthWhereClause(accessLevel, alias, creatorVisibility, organizationIdField);
|
|
618
|
+
const securityNote = accessLevel === 'public' || accessLevel === 'system'
|
|
619
|
+
? `-- Access Level: ${accessLevel} (no auth check)`
|
|
620
|
+
: accessLevel === 'creator' && creatorVisibility
|
|
621
|
+
? `-- Access Level: ${accessLevel} (auth check + visibility: ${creatorVisibility.column})`
|
|
622
|
+
: `-- Access Level: ${accessLevel} (auth check enabled)`;
|
|
623
|
+
// Build SELECT clause with all parts
|
|
624
|
+
const selectParts = [tableColumns];
|
|
625
|
+
if (computedFields)
|
|
626
|
+
selectParts.push(computedFields);
|
|
627
|
+
if (includesCases)
|
|
628
|
+
selectParts.push(includesCases);
|
|
629
|
+
selectParts.push('COUNT(*) OVER() as total_count');
|
|
630
|
+
const selectClause = selectParts.join(',\n ');
|
|
631
|
+
return `-- ============================================================================
|
|
632
|
+
-- Results function for ${this.tableName}
|
|
633
|
+
-- ============================================================================
|
|
634
|
+
-- DO NOT EDIT MANUALLY! This file is auto-generated.
|
|
635
|
+
-- Changes will be OVERWRITTEN on next generation.
|
|
636
|
+
--
|
|
637
|
+
-- To modify this function:
|
|
638
|
+
-- 1. Edit config: /backend/src/features/${this.tableName}/config/${this.tableName}.config.ts
|
|
639
|
+
-- 2. Regenerate: npm run generate:rpc ${this.tableName}
|
|
640
|
+
-- 3. Apply: Use MCP apply_migration or supabase db push
|
|
641
|
+
--
|
|
642
|
+
-- Generator: SQL Generator v${this.GENERATOR_VERSION} (Window Function Pattern with JSONB Includes)
|
|
643
|
+
-- Generated: ${new Date().toISOString()}
|
|
644
|
+
--
|
|
645
|
+
-- Pattern: get_${this.tableName}_results() -> Returns full records + includes + total_count
|
|
646
|
+
-- ${securityNote}
|
|
647
|
+
-- ============================================================================
|
|
648
|
+
|
|
649
|
+
DROP FUNCTION IF EXISTS public.${signature.split('(')[0]};
|
|
650
|
+
|
|
651
|
+
CREATE OR REPLACE FUNCTION public.${signature}
|
|
652
|
+
RETURNS TABLE (
|
|
653
|
+
data jsonb,
|
|
654
|
+
total_count bigint
|
|
655
|
+
)
|
|
656
|
+
LANGUAGE plpgsql
|
|
657
|
+
STABLE
|
|
658
|
+
SECURITY DEFINER
|
|
659
|
+
SET search_path = ''
|
|
660
|
+
AS $$${accessLevel !== 'public' && accessLevel !== 'system' ? `
|
|
661
|
+
DECLARE${generateAuthDeclarations(accessLevel)}` : ''}
|
|
662
|
+
BEGIN
|
|
663
|
+
${authCheck}
|
|
664
|
+
RETURN QUERY
|
|
665
|
+
-- v3.11: Add public. prefix for Postgres v17 compatibility
|
|
666
|
+
WITH results AS (
|
|
667
|
+
SELECT
|
|
668
|
+
${selectClause}
|
|
669
|
+
FROM public.${this.tableName} ${alias}${allJoins}
|
|
670
|
+
WHERE ${this.generateOrgWhereClause(alias)}${whereClauses}
|
|
671
|
+
${authWhereClause}
|
|
672
|
+
${orderBy}
|
|
673
|
+
LIMIT p_limit
|
|
674
|
+
OFFSET p_offset
|
|
675
|
+
)
|
|
676
|
+
SELECT
|
|
677
|
+
to_jsonb(results.*) - 'total_count' as data,
|
|
678
|
+
results.total_count
|
|
679
|
+
FROM results;
|
|
680
|
+
END;
|
|
681
|
+
$$;
|
|
682
|
+
`;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Generate both results and counts functions (NEW API)
|
|
686
|
+
*/
|
|
687
|
+
generate() {
|
|
688
|
+
return {
|
|
689
|
+
resultsSQL: this.generateResultsFunction(),
|
|
690
|
+
countsSQL: this.generateCountsFunction(),
|
|
691
|
+
filterSQL: this.generateFilterFunction() // For backwards compatibility
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Write migrations to files (NEW - generates results + counts)
|
|
696
|
+
*/
|
|
697
|
+
writeMigrations(options = {}) {
|
|
698
|
+
const { resultsSQL, countsSQL, filterSQL } = this.generate();
|
|
699
|
+
// Determine output directory
|
|
700
|
+
const outputDir = options.outputDir || path.join(process.cwd(), 'supabase', 'migrations');
|
|
701
|
+
// Ensure output directory exists
|
|
702
|
+
if (!fs.existsSync(outputDir)) {
|
|
703
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
704
|
+
}
|
|
705
|
+
// Generate timestamps (1 second apart to ensure unique)
|
|
706
|
+
const resultsTimestamp = generateTimestamp(0);
|
|
707
|
+
const countsTimestamp = generateTimestamp(1);
|
|
708
|
+
// Generate filenames (use config names if provided, otherwise fallback to tableName)
|
|
709
|
+
const resultsRpcName = this.config.resultsRpcName || `get_${this.config.tableName}_results`;
|
|
710
|
+
const countsRpcName = this.config.countsRpcName || `get_${this.config.tableName}_counts`;
|
|
711
|
+
const resultsFile = path.join(outputDir, `${resultsTimestamp}_${resultsRpcName}.sql`);
|
|
712
|
+
const countsFile = path.join(outputDir, `${countsTimestamp}_${countsRpcName}.sql`);
|
|
713
|
+
// Check if files exist (unless force is true)
|
|
714
|
+
if (!options.force) {
|
|
715
|
+
if (fs.existsSync(resultsFile)) {
|
|
716
|
+
throw new Error(`Results migration already exists: ${resultsFile}\nUse --force to overwrite`);
|
|
717
|
+
}
|
|
718
|
+
if (fs.existsSync(countsFile)) {
|
|
719
|
+
throw new Error(`Counts migration already exists: ${countsFile}\nUse --force to overwrite`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Write files
|
|
723
|
+
fs.writeFileSync(resultsFile, resultsSQL, 'utf-8');
|
|
724
|
+
fs.writeFileSync(countsFile, countsSQL, 'utf-8');
|
|
725
|
+
const result = {
|
|
726
|
+
resultsFile,
|
|
727
|
+
countsFile
|
|
728
|
+
};
|
|
729
|
+
// Optionally write legacy filter function for backwards compatibility
|
|
730
|
+
if (options.legacy && filterSQL) {
|
|
731
|
+
const filterTimestamp = generateTimestamp(2);
|
|
732
|
+
const filterRpcName = `filter_${this.config.tableName}_simple`;
|
|
733
|
+
const filterFile = path.join(outputDir, `${filterTimestamp}_${filterRpcName}.sql`);
|
|
734
|
+
if (!options.force && fs.existsSync(filterFile)) {
|
|
735
|
+
throw new Error(`Filter migration already exists: ${filterFile}\nUse --force to overwrite`);
|
|
736
|
+
}
|
|
737
|
+
fs.writeFileSync(filterFile, filterSQL, 'utf-8');
|
|
738
|
+
result.filterFile = filterFile;
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
//# sourceMappingURL=rpc-generator.js.map
|