@rapidd/build 1.0.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.
@@ -0,0 +1,381 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { Client } = require('pg');
4
+ const { createConverter } = require('../parsers/autoRLSConverter');
5
+ const { createEnhancedConverter } = require('../parsers/enhancedRLSConverter');
6
+ const { analyzeFunctions, generateMappingConfig } = require('../parsers/functionAnalyzer');
7
+
8
+ /**
9
+ * Auto-detect user table name (case-insensitive search for user/users)
10
+ */
11
+ function detectUserTable(models, userTableOption) {
12
+ if (userTableOption) {
13
+ return userTableOption;
14
+ }
15
+
16
+ const modelNames = Object.keys(models);
17
+ const userTables = modelNames.filter(name =>
18
+ name.toLowerCase() === 'user' || name.toLowerCase() === 'users'
19
+ );
20
+
21
+ if (userTables.length === 0) {
22
+ throw new Error('No user table found (user/users). Please specify --user-table option.');
23
+ }
24
+
25
+ if (userTables.length > 1) {
26
+ throw new Error(`Multiple user tables found: ${userTables.join(', ')}. Please specify --user-table option.`);
27
+ }
28
+
29
+ return userTables[0];
30
+ }
31
+
32
+ /**
33
+ * Extract RLS policies from PostgreSQL
34
+ */
35
+ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
36
+ const client = new Client({ connectionString: databaseUrl });
37
+
38
+ try {
39
+ await client.connect();
40
+
41
+ const policies = {};
42
+
43
+ // Initialize all models with empty policies
44
+ for (const modelName of modelNames) {
45
+ policies[modelName] = [];
46
+ }
47
+
48
+ // Query all RLS policies from pg_policies
49
+ const result = await client.query(`
50
+ SELECT
51
+ tablename,
52
+ policyname,
53
+ permissive,
54
+ roles,
55
+ cmd,
56
+ qual,
57
+ with_check
58
+ FROM pg_policies
59
+ WHERE schemaname = 'public'
60
+ ORDER BY tablename, policyname
61
+ `);
62
+
63
+ // Group policies by table
64
+ for (const row of result.rows) {
65
+ const tableName = row.tablename;
66
+ if (policies[tableName] !== undefined) {
67
+ policies[tableName].push({
68
+ name: row.policyname,
69
+ permissive: row.permissive === 'PERMISSIVE',
70
+ roles: row.roles,
71
+ command: row.cmd,
72
+ using: row.qual,
73
+ withCheck: row.with_check
74
+ });
75
+ }
76
+ }
77
+
78
+ await client.end();
79
+ return policies;
80
+
81
+ } catch (error) {
82
+ try {
83
+ await client.end();
84
+ } catch (e) {}
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Generate RLS functions for a single model from PostgreSQL policies
91
+ */
92
+ function generateModelRLS(modelName, policies, converter) {
93
+ const hasPolicies = policies && policies.length > 0;
94
+
95
+ if (!hasPolicies) {
96
+ // No RLS policies - generate permissive access
97
+ return ` ${modelName}: {
98
+ canCreate: (user) => true,
99
+ hasAccess: (data, user) => true,
100
+ getAccessFilter: (user) => ({}),
101
+ getUpdateFilter: (user) => ({}),
102
+ getDeleteFilter: (user) => ({}),
103
+ getOmitFields: (user) => []
104
+ }`;
105
+ }
106
+
107
+ // Find policies by command type
108
+ const selectPolicies = policies.filter(p => p.command === 'SELECT' || p.command === 'ALL');
109
+ const insertPolicies = policies.filter(p => p.command === 'INSERT' || p.command === 'ALL');
110
+ const updatePolicies = policies.filter(p => p.command === 'UPDATE' || p.command === 'ALL');
111
+ const deletePolicies = policies.filter(p => p.command === 'DELETE' || p.command === 'ALL');
112
+
113
+ // Generate each function
114
+ const canCreateCode = generateFunction(insertPolicies, 'withCheck', converter, modelName);
115
+ const hasAccessCode = generateFunction(selectPolicies, 'using', converter, modelName);
116
+ const accessFilterCode = generateFilter(selectPolicies, 'using', converter, modelName);
117
+ let updateFilterCode = generateFilter(updatePolicies, 'using', converter, modelName);
118
+ let deleteFilterCode = generateFilter(deletePolicies, 'using', converter, modelName);
119
+
120
+ // If update/delete filters are empty/false but access filter is not, copy access filter
121
+ if ((updateFilterCode === 'return false;' || updateFilterCode === 'return {};') &&
122
+ accessFilterCode !== 'return false;' && accessFilterCode !== 'return {};') {
123
+ updateFilterCode = accessFilterCode;
124
+ }
125
+ if ((deleteFilterCode === 'return false;' || deleteFilterCode === 'return {};') &&
126
+ accessFilterCode !== 'return false;' && accessFilterCode !== 'return {};') {
127
+ deleteFilterCode = accessFilterCode;
128
+ }
129
+
130
+ return ` ${modelName}: {
131
+ canCreate: (user) => {
132
+ ${canCreateCode}
133
+ },
134
+ hasAccess: (data, user) => {
135
+ ${hasAccessCode}
136
+ },
137
+ getAccessFilter: (user) => {
138
+ ${accessFilterCode}
139
+ },
140
+ getUpdateFilter: (user) => {
141
+ ${updateFilterCode}
142
+ },
143
+ getDeleteFilter: (user) => {
144
+ ${deleteFilterCode}
145
+ },
146
+ getOmitFields: (user) => []
147
+ }`;
148
+ }
149
+
150
+ /**
151
+ * Generate JavaScript function from policies
152
+ */
153
+ function generateFunction(policies, expressionField, converter, modelName) {
154
+ if (policies.length === 0) {
155
+ return 'return true;';
156
+ }
157
+
158
+ const conditions = [];
159
+
160
+ for (const policy of policies) {
161
+ const expr = expressionField === 'withCheck'
162
+ ? (policy.withCheck || policy.using)
163
+ : policy[expressionField];
164
+
165
+ if (expr) {
166
+ try {
167
+ const jsExpr = converter.convertToJavaScript(expr, 'data', 'user', modelName);
168
+ conditions.push(jsExpr);
169
+ } catch (e) {
170
+ conditions.push(`true /* Error: ${e.message} */`);
171
+ }
172
+ }
173
+ }
174
+
175
+ if (conditions.length === 0) {
176
+ return 'return true;';
177
+ }
178
+
179
+ // Policies are OR'd together (any policy allows)
180
+ return `return ${conditions.join(' || ')};`;
181
+ }
182
+
183
+ /**
184
+ * Generate Prisma filter function
185
+ */
186
+ function generateFilter(policies, expressionField, converter, modelName) {
187
+ if (policies.length === 0) {
188
+ return 'return false;';
189
+ }
190
+
191
+ const filtersWithRoles = [];
192
+
193
+ for (const policy of policies) {
194
+ const expr = policy[expressionField];
195
+ if (expr) {
196
+ try {
197
+ const prismaFilter = converter.convertToPrismaFilter(expr, 'user', modelName);
198
+ const analysis = converter.analyzer ? converter.analyzer.analyzeSQLForFilters(expr) : null;
199
+
200
+ // Track role conditions and data filters separately
201
+ const roleConditions = analysis?.conditions?.filter(c =>
202
+ c.type === 'role_any' || c.type === 'role_equal'
203
+ ) || [];
204
+
205
+ filtersWithRoles.push({
206
+ filter: prismaFilter,
207
+ roleConditions,
208
+ hasDataFilter: prismaFilter !== '{}'
209
+ });
210
+ } catch (e) {
211
+ // On error, skip filter
212
+ }
213
+ }
214
+ }
215
+
216
+ if (filtersWithRoles.length === 0) {
217
+ return 'return false;';
218
+ }
219
+
220
+ // Build conditional filter logic
221
+ return buildConditionalFilter(filtersWithRoles);
222
+ }
223
+
224
+ /**
225
+ * Build conditional filter with role checks
226
+ */
227
+ function buildConditionalFilter(filtersWithRoles) {
228
+ const roleOnlyFilters = [];
229
+ const dataFilters = [];
230
+
231
+ for (const item of filtersWithRoles) {
232
+ if (item.roleConditions.length > 0 && !item.hasDataFilter) {
233
+ // Pure role check - return {} if role matches
234
+ roleOnlyFilters.push(...item.roleConditions);
235
+ } else if (item.roleConditions.length > 0 && item.hasDataFilter) {
236
+ // Has both role and data filter - already handled with if statement
237
+ if (item.filter.includes('if (')) {
238
+ return item.filter;
239
+ }
240
+ dataFilters.push(item.filter);
241
+ } else if (item.hasDataFilter) {
242
+ // Data filter only
243
+ dataFilters.push(item.filter);
244
+ }
245
+ }
246
+
247
+ // Generate conditional code
248
+ const conditions = [];
249
+
250
+ // Collect all roles that grant full access
251
+ const rolesWithFullAccess = new Set();
252
+ for (const roleCond of roleOnlyFilters) {
253
+ if (roleCond.type === 'role_any') {
254
+ roleCond.roles.forEach(r => rolesWithFullAccess.add(r));
255
+ } else if (roleCond.type === 'role_equal') {
256
+ rolesWithFullAccess.add(roleCond.role);
257
+ }
258
+ }
259
+
260
+ // Add single consolidated role check if needed
261
+ if (rolesWithFullAccess.size > 0) {
262
+ const roleArray = Array.from(rolesWithFullAccess);
263
+ if (roleArray.length === 1) {
264
+ conditions.push(`if (user?.role === '${roleArray[0]}') { return {}; }`);
265
+ } else {
266
+ conditions.push(`if ([${roleArray.map(r => `'${r}'`).join(', ')}].includes(user?.role)) { return {}; }`);
267
+ }
268
+ }
269
+
270
+ // Deduplicate data filters
271
+ const uniqueDataFilters = [...new Set(dataFilters)];
272
+
273
+ // Add final return with data filters
274
+ if (uniqueDataFilters.length === 0) {
275
+ conditions.push('return false;');
276
+ } else if (uniqueDataFilters.length === 1) {
277
+ conditions.push(`return ${uniqueDataFilters[0]};`);
278
+ } else {
279
+ conditions.push(`return { OR: [${uniqueDataFilters.join(', ')}] };`);
280
+ }
281
+
282
+ return conditions.join(' ');
283
+ }
284
+
285
+ /**
286
+ * Generate complete rls.js file
287
+ */
288
+ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption, relationships = {}) {
289
+ const userTable = detectUserTable(models, userTableOption);
290
+ const modelNames = Object.keys(models);
291
+
292
+ let policies = {};
293
+ const timestamp = new Date().toISOString();
294
+
295
+ let rlsCode = `const rls = {\n model: {},\n lastUpdateDate: '${timestamp}'\n};\n\n`;
296
+
297
+ // Create enhanced converter with analyzed functions, models, and relationships
298
+ let converter = createEnhancedConverter({}, {}, models, relationships);
299
+
300
+ if (isPostgreSQL && databaseUrl) {
301
+ console.log('PostgreSQL detected - analyzing database...');
302
+
303
+ // Step 1: Analyze functions
304
+ try {
305
+ const functionAnalysis = await analyzeFunctions(databaseUrl);
306
+ console.log(`✓ Analyzed ${Object.keys(functionAnalysis.functionMappings).length} PostgreSQL functions`);
307
+
308
+ // Create enhanced converter with analyzed mappings, models, and relationships
309
+ converter = createEnhancedConverter(
310
+ functionAnalysis.functionMappings,
311
+ functionAnalysis.sessionVariables,
312
+ models,
313
+ relationships
314
+ );
315
+
316
+ // Save function analysis for debugging
317
+ const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
318
+ const mappingConfig = generateMappingConfig(functionAnalysis);
319
+ fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
320
+ console.log(`✓ Function mappings saved to ${configPath}`);
321
+
322
+ // Also add user context requirements as a comment in rls.js
323
+ if (Object.keys(functionAnalysis.userContextRequirements).length > 0) {
324
+ rlsCode = `/**
325
+ * User Context Requirements:
326
+ * The user object should contain:
327
+ ${Object.entries(functionAnalysis.userContextRequirements)
328
+ .map(([field, req]) => ` * - ${field}: ${typeof req === 'object' ? req.description : 'required'}`)
329
+ .join('\n')}
330
+ */
331
+
332
+ ` + rlsCode;
333
+ }
334
+ } catch (error) {
335
+ console.warn(`⚠ Could not analyze functions: ${error.message}`);
336
+ }
337
+
338
+ // Step 2: Extract policies
339
+ try {
340
+ policies = await extractPostgreSQLPolicies(databaseUrl, modelNames);
341
+ const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
342
+ console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
343
+ } catch (error) {
344
+ console.warn(`⚠ Failed to extract PostgreSQL RLS: ${error.message}`);
345
+ console.log('Generating permissive RLS for all models...');
346
+ for (const modelName of modelNames) {
347
+ policies[modelName] = [];
348
+ }
349
+ }
350
+ } else {
351
+ if (!isPostgreSQL) {
352
+ console.log('Non-PostgreSQL database detected - RLS not supported');
353
+ }
354
+ console.log('Generating permissive RLS for all models...');
355
+ for (const modelName of modelNames) {
356
+ policies[modelName] = [];
357
+ }
358
+ }
359
+
360
+ // Generate RLS for each model
361
+ rlsCode += 'rls.model = {\n';
362
+ const modelRLSCode = modelNames.map(modelName => {
363
+ return generateModelRLS(modelName, policies[modelName], converter);
364
+ });
365
+ rlsCode += modelRLSCode.join(',\n');
366
+ rlsCode += '\n};\n\n';
367
+ rlsCode += 'module.exports = rls;\n';
368
+
369
+ // Ensure output directory exists
370
+ const outputDir = path.dirname(outputPath);
371
+ if (!fs.existsSync(outputDir)) {
372
+ fs.mkdirSync(outputDir, { recursive: true });
373
+ }
374
+
375
+ fs.writeFileSync(outputPath, rlsCode);
376
+ console.log('✓ Generated rls.js with dynamic function mappings');
377
+ }
378
+
379
+ module.exports = {
380
+ generateRLS
381
+ };
@@ -0,0 +1,127 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Generate Express route for a single model
6
+ * @param {string} modelName - Name of the model
7
+ * @returns {string} - Generated route code
8
+ */
9
+ function generateRouteFile(modelName) {
10
+ const modelNameLower = modelName.toLowerCase();
11
+ const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
12
+
13
+ return `const router = require('express').Router();
14
+ const {Api, ErrorResponse} = require('../../../src/Api');
15
+ const {${className}, QueryBuilder, prisma} = require('../../../src/Model/${className}');
16
+
17
+ router.all('*', async (req, res, next) => {
18
+ if(req.session && req.user){
19
+ req.${className} = new ${className}({'user': req.user});
20
+ next();
21
+ }
22
+ else{
23
+ res.status(401).send({'status_code': res.statusCode, 'message': "No valid session"});
24
+ }
25
+ });
26
+
27
+ // GET ALL
28
+ router.get('/', async function(req, res) {
29
+ let response, status_code = 200;
30
+ try {
31
+ const { q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc" } = req.query;
32
+
33
+ const _data = req.${className}.getMany(q, include, limit, offset, sortBy, sortOrder);
34
+ const _count = req.${className}.count(q);
35
+ const [data, count] = await Promise.all([_data, _count]);
36
+
37
+ response = Api.getListResponseBody(data, {'take': req.${className}.take(Number(limit)), 'skip': req.${className}.skip(Number(offset)), 'total': count});
38
+ }
39
+ catch(error){
40
+ response = QueryBuilder.errorHandler(error);
41
+ status_code = response.status_code;
42
+ }
43
+ res.status(status_code).send(response);
44
+ });
45
+
46
+ // GET BY ID
47
+ router.get('/:id', async function(req, res) {
48
+ let response, status_code = 200;
49
+ try{
50
+ const { include = ""} = req.query;
51
+ response = await req.${className}.get(req.params.id, include);
52
+ }
53
+ catch(error){
54
+ response = QueryBuilder.errorHandler(error);
55
+ status_code = response.status_code;
56
+ }
57
+ res.status(status_code).send(response);
58
+ });
59
+
60
+ // CREATE
61
+ router.post('/', async function(req, res) {
62
+ let response, status_code = 201, payload = req.body;
63
+ try{
64
+ response = await req.${className}.create(payload);
65
+ }
66
+ catch(error){
67
+ response = QueryBuilder.errorHandler(error, payload);
68
+ status_code = response.status_code;
69
+ }
70
+ res.status(status_code).send(response);
71
+ });
72
+
73
+ // UPDATE
74
+ router.patch('/:id', async function(req, res) {
75
+ let response, status_code = 200, payload = req.body;
76
+ try{
77
+ response = await req.${className}.update(req.params.id, payload);
78
+ }
79
+ catch(error){
80
+ response = QueryBuilder.errorHandler(error, payload);
81
+ status_code = response.status_code;
82
+ }
83
+ res.status(status_code).send(response);
84
+ });
85
+
86
+ // DELETE
87
+ router.delete('/:id', async (req, res)=>{
88
+ let response, status_code = 200;
89
+ try{
90
+ await req.${className}.delete(req.params.id);
91
+ response = {'status_code': status_code, 'message': "${className} successfully deleted"}
92
+ }
93
+ catch(error){
94
+ response = QueryBuilder.errorHandler(error);
95
+ status_code = response.status_code;
96
+ }
97
+ res.status(status_code).send(response);
98
+ });
99
+
100
+ module.exports = router;
101
+ `;
102
+ }
103
+
104
+ /**
105
+ * Generate all route files
106
+ * @param {Object} models - Models object from parser
107
+ * @param {string} routesDir - Directory to output route files
108
+ */
109
+ function generateAllRoutes(models, routesDir) {
110
+ // Create routes directory if it doesn't exist
111
+ if (!fs.existsSync(routesDir)) {
112
+ fs.mkdirSync(routesDir, { recursive: true });
113
+ }
114
+
115
+ // Generate individual route files
116
+ for (const modelName of Object.keys(models)) {
117
+ const routeCode = generateRouteFile(modelName);
118
+ const routePath = path.join(routesDir, `${modelName.toLowerCase()}.js`);
119
+ fs.writeFileSync(routePath, routeCode);
120
+ console.log(`Generated route: ${modelName.toLowerCase()}.js`);
121
+ }
122
+ }
123
+
124
+ module.exports = {
125
+ generateAllRoutes,
126
+ generateRouteFile
127
+ };