@rapidd/build 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidd/build",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Dynamic code generator that transforms Prisma schemas into Express.js CRUD APIs with PostgreSQL RLS-to-JavaScript translation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -150,7 +150,7 @@ function generateAllModels(models, modelDir, modelJsPath) {
150
150
 
151
151
  // Copy rapidd.js to output if it exists
152
152
  const sourceRapiddJs = path.join(process.cwd(), 'rapidd', 'rapidd.js');
153
- const outputRapiddDir = path.dirname(modelDir.replace(/src[\/\\]Model$/, 'rapidd'));
153
+ const outputRapiddDir = modelDir.replace(/src[\/\\]Model$/, 'rapidd');
154
154
  const outputRapiddJs = path.join(outputRapiddDir, 'rapidd.js');
155
155
 
156
156
  if (fs.existsSync(sourceRapiddJs)) {
@@ -34,10 +34,10 @@ function detectUserTable(models, userTableOption) {
34
34
  /**
35
35
  * Extract RLS policies from PostgreSQL
36
36
  * @param {string} databaseUrl - PostgreSQL connection URL
37
- * @param {Array} modelNames - Array of model names
38
- * @returns {Object} - RLS policies for each table
37
+ * @param {Object} models - Models object with dbName mapping
38
+ * @returns {Object} - RLS policies for each model
39
39
  */
40
- async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
40
+ async function extractPostgreSQLPolicies(databaseUrl, models) {
41
41
  const client = new Client({ connectionString: databaseUrl });
42
42
 
43
43
  try {
@@ -45,8 +45,11 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
45
45
 
46
46
  const policies = {};
47
47
 
48
- // Initialize all models with empty policies
49
- for (const modelName of modelNames) {
48
+ // Create mapping from database table name to model name
49
+ const tableToModelMap = {};
50
+ for (const [modelName, modelData] of Object.entries(models)) {
51
+ const dbName = modelData.dbName || modelName.toLowerCase();
52
+ tableToModelMap[dbName] = modelName;
50
53
  policies[modelName] = [];
51
54
  }
52
55
 
@@ -65,11 +68,13 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
65
68
  ORDER BY tablename, policyname
66
69
  `);
67
70
 
68
- // Group policies by table
71
+ // Group policies by model (using table to model mapping)
69
72
  for (const row of result.rows) {
70
73
  const tableName = row.tablename;
71
- if (policies[tableName] !== undefined) {
72
- policies[tableName].push({
74
+ const modelName = tableToModelMap[tableName];
75
+
76
+ if (modelName && policies[modelName] !== undefined) {
77
+ policies[modelName].push({
73
78
  name: row.policyname,
74
79
  permissive: row.permissive === 'PERMISSIVE',
75
80
  roles: row.roles,
@@ -290,7 +295,7 @@ async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTa
290
295
 
291
296
  console.log('Extracting RLS policies from database...');
292
297
  try {
293
- policies = await extractPostgreSQLPolicies(databaseUrl, modelNames);
298
+ policies = await extractPostgreSQLPolicies(databaseUrl, models);
294
299
  const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
295
300
  console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
296
301
  } catch (error) {
@@ -32,7 +32,7 @@ function detectUserTable(models, userTableOption) {
32
32
  /**
33
33
  * Extract RLS policies from PostgreSQL
34
34
  */
35
- async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
35
+ async function extractPostgreSQLPolicies(databaseUrl, models) {
36
36
  const client = new Client({ connectionString: databaseUrl });
37
37
 
38
38
  try {
@@ -40,8 +40,11 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
40
40
 
41
41
  const policies = {};
42
42
 
43
- // Initialize all models with empty policies
44
- for (const modelName of modelNames) {
43
+ // Create mapping from database table name to model name
44
+ const tableToModelMap = {};
45
+ for (const [modelName, modelData] of Object.entries(models)) {
46
+ const dbName = modelData.dbName || modelName.toLowerCase();
47
+ tableToModelMap[dbName] = modelName;
45
48
  policies[modelName] = [];
46
49
  }
47
50
 
@@ -60,11 +63,13 @@ async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
60
63
  ORDER BY tablename, policyname
61
64
  `);
62
65
 
63
- // Group policies by table
66
+ // Group policies by model (using table to model mapping)
64
67
  for (const row of result.rows) {
65
68
  const tableName = row.tablename;
66
- if (policies[tableName] !== undefined) {
67
- policies[tableName].push({
69
+ const modelName = tableToModelMap[tableName];
70
+
71
+ if (modelName && policies[modelName] !== undefined) {
72
+ policies[modelName].push({
68
73
  name: row.policyname,
69
74
  permissive: row.permissive === 'PERMISSIVE',
70
75
  roles: row.roles,
@@ -165,9 +170,12 @@ function generateFunction(policies, expressionField, converter, modelName) {
165
170
  if (expr) {
166
171
  try {
167
172
  const jsExpr = converter.convertToJavaScript(expr, 'data', 'user', modelName);
173
+ console.log(`✓ Policy '${policy.name}': ${expr.substring(0, 50)}... -> ${jsExpr.substring(0, 80)}`);
168
174
  conditions.push(jsExpr);
169
175
  } catch (e) {
170
- conditions.push(`true /* Error: ${e.message} */`);
176
+ console.warn(`⚠ Failed to convert RLS policy '${policy.name}' for ${modelName}: ${e.message}`);
177
+ console.warn(` SQL: ${expr}`);
178
+ conditions.push(`true /* TODO: Manual conversion needed for policy '${policy.name}' */`);
171
179
  }
172
180
  }
173
181
  }
@@ -176,6 +184,11 @@ function generateFunction(policies, expressionField, converter, modelName) {
176
184
  return 'return true;';
177
185
  }
178
186
 
187
+ // If any condition is 'true', the entire expression is true
188
+ if (conditions.some(c => c === 'true' || c.startsWith('true /*'))) {
189
+ return 'return true;';
190
+ }
191
+
179
192
  // Policies are OR'd together (any policy allows)
180
193
  return `return ${conditions.join(' || ')};`;
181
194
  }
@@ -208,7 +221,9 @@ function generateFilter(policies, expressionField, converter, modelName) {
208
221
  hasDataFilter: prismaFilter !== '{}'
209
222
  });
210
223
  } catch (e) {
211
- // On error, skip filter
224
+ console.warn(`⚠ Failed to convert RLS filter policy '${policy.name}' for ${modelName}: ${e.message}`);
225
+ console.warn(` SQL: ${expr}`);
226
+ // On error, skip filter (fail-safe - no access)
212
227
  }
213
228
  }
214
229
  }
@@ -341,7 +356,7 @@ ${Object.entries(functionAnalysis.userContextRequirements)
341
356
 
342
357
  // Step 2: Extract policies
343
358
  try {
344
- policies = await extractPostgreSQLPolicies(databaseUrl, modelNames);
359
+ policies = await extractPostgreSQLPolicies(databaseUrl, models);
345
360
  const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
346
361
  console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
347
362
  } catch (error) {
@@ -180,8 +180,8 @@ class DeepSQLAnalyzer {
180
180
  * Extract direct field comparisons
181
181
  */
182
182
  extractDirectComparisons(sql, analysis) {
183
- // Pattern: field = 'value'
184
- const stringPattern = /(\w+)\s*=\s*'([^']+)'/gi;
183
+ // Pattern: field = 'value' (with or without quotes on field name)
184
+ const stringPattern = /(?:"?(\w+)"?)\s*=\s*'([^']+)'/gi;
185
185
  let match;
186
186
  while ((match = stringPattern.exec(sql)) !== null) {
187
187
  const field = match[1];
@@ -200,8 +200,8 @@ class DeepSQLAnalyzer {
200
200
  });
201
201
  }
202
202
 
203
- // Pattern: field = number
204
- const numberPattern = /(\w+)\s*=\s*(\d+)(?!\s*\))/gi;
203
+ // Pattern: field = number (with or without quotes on field name)
204
+ const numberPattern = /(?:"?(\w+)"?)\s*=\s*(\d+)(?!\s*\))/gi;
205
205
  while ((match = numberPattern.exec(sql)) !== null) {
206
206
  const field = match[1];
207
207
  const value = match[2];
@@ -219,8 +219,22 @@ class DeepSQLAnalyzer {
219
219
  });
220
220
  }
221
221
 
222
+ // Pattern: field = true/false (with or without quotes on field name)
223
+ const booleanPattern = /(?:"?(\w+)"?)\s*=\s*(true|false)/gi;
224
+ while ((match = booleanPattern.exec(sql)) !== null) {
225
+ const field = match[1];
226
+ const value = match[2].toLowerCase();
227
+
228
+ analysis.filters.push({
229
+ type: 'equal',
230
+ field: field,
231
+ value: value,
232
+ prisma: `{ ${field}: ${value} }`
233
+ });
234
+ }
235
+
222
236
  // Pattern: field IS NULL
223
- const isNullPattern = /(\w+)\s+IS\s+NULL/gi;
237
+ const isNullPattern = /(?:"?(\w+)"?)\s+IS\s+NULL/gi;
224
238
  while ((match = isNullPattern.exec(sql)) !== null) {
225
239
  analysis.filters.push({
226
240
  type: 'is_null',
@@ -244,10 +258,10 @@ class DeepSQLAnalyzer {
244
258
  * Extract function-based comparisons
245
259
  */
246
260
  extractFunctionComparisons(sql, analysis) {
247
- // Pattern: field = function()
261
+ // Pattern: field = function() (with or without quotes on field name)
248
262
  const patterns = [
249
- /(\w+)\s*=\s*([\w.]+)\s*\(\s*\)/gi, // field = function()
250
- /([\w.]+)\s*\(\s*\)\s*=\s*(\w+)/gi // function() = field
263
+ /(?:"?(\w+)"?)\s*=\s*([\w.]+)\s*\(\s*\)/gi, // field = function()
264
+ /([\w.]+)\s*\(\s*\)\s*=\s*(?:"?(\w+)"?)/gi // function() = field
251
265
  ];
252
266
 
253
267
  // Normalize dots in function names for lookup
@@ -13,6 +13,23 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
13
13
 
14
14
  sql = sql.trim();
15
15
 
16
+ // Strip outer parentheses if they wrap the entire expression
17
+ if (sql.startsWith('(') && sql.endsWith(')')) {
18
+ let depth = 0;
19
+ let matchesOuter = true;
20
+ for (let i = 0; i < sql.length; i++) {
21
+ if (sql[i] === '(') depth++;
22
+ if (sql[i] === ')') depth--;
23
+ if (depth === 0 && i < sql.length - 1) {
24
+ matchesOuter = false;
25
+ break;
26
+ }
27
+ }
28
+ if (matchesOuter) {
29
+ sql = sql.substring(1, sql.length - 1).trim();
30
+ }
31
+ }
32
+
16
33
  // Normalize whitespace
17
34
  sql = sql.replace(/\s+/g, ' ').replace(/\n/g, ' ');
18
35
 
@@ -48,8 +65,8 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
48
65
  return `[${arrayValues}].includes(${userVar}?.${userProperty})`;
49
66
  }
50
67
 
51
- // Handle field = function() comparisons
52
- const funcCompareMatch = sql.match(/(\w+)\s*=\s*(\w+)\s*\([^)]*\)/i);
68
+ // Handle field = function() comparisons (with or without quotes)
69
+ const funcCompareMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*(\w+)\s*\([^)]*\)/i);
53
70
  if (funcCompareMatch) {
54
71
  const field = funcCompareMatch[1];
55
72
  const funcName = funcCompareMatch[2];
@@ -60,7 +77,7 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
60
77
  }
61
78
 
62
79
  // Handle field = (current_setting(...))
63
- const currentSettingMatch = sql.match(/(\w+)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
80
+ const currentSettingMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*\(\s*current_setting\s*\(\s*'([^']+)'/i);
64
81
  if (currentSettingMatch) {
65
82
  const field = currentSettingMatch[1];
66
83
  const setting = currentSettingMatch[2];
@@ -70,6 +87,14 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
70
87
  return `${dataVar}?.${field} === ${userVar}?.${userProperty}`;
71
88
  }
72
89
 
90
+ // Handle field = literal value (true, false, numbers, strings)
91
+ const literalMatch = sql.match(/(?:"?(\w+)"?)\s*=\s*(true|false|\d+|'[^']+')/i);
92
+ if (literalMatch) {
93
+ const field = literalMatch[1];
94
+ const value = literalMatch[2];
95
+ return `${dataVar}?.${field} === ${value}`;
96
+ }
97
+
73
98
  // Handle EXISTS subqueries
74
99
  if (sql.includes('EXISTS')) {
75
100
  return handleExistsSubquery(sql, dataVar, userVar);
@@ -80,6 +105,7 @@ function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
80
105
  if (sql.toLowerCase() === 'false') return 'false';
81
106
 
82
107
  // Unhandled pattern
108
+ console.warn(`⚠ Unhandled RLS pattern: ${sql}`);
83
109
  return `true /* Unhandled RLS pattern: ${sql.substring(0, 60)}... */`;
84
110
  }
85
111
 
@@ -46,6 +46,7 @@ function parsePrismaSchema(schemaPath) {
46
46
  for (const { name, body } of modelBlocks) {
47
47
  const fields = parseModelFields(body);
48
48
  const compositeKeyFields = parseCompositeKey(body);
49
+ const dbName = parseMapDirective(body);
49
50
 
50
51
  // Mark composite key fields with isId
51
52
  if (compositeKeyFields) {
@@ -60,7 +61,8 @@ function parsePrismaSchema(schemaPath) {
60
61
  name,
61
62
  fields,
62
63
  relations: parseModelRelations(body),
63
- compositeKey: compositeKeyFields
64
+ compositeKey: compositeKeyFields,
65
+ dbName: dbName || name.toLowerCase() // Default to lowercase model name
64
66
  };
65
67
  }
66
68
 
@@ -94,6 +96,25 @@ function parseCompositeKey(modelBody) {
94
96
  return null;
95
97
  }
96
98
 
99
+ /**
100
+ * Parse @@map directive to get database table name
101
+ * @param {string} modelBody - The content inside model braces
102
+ * @returns {string|null} - Database table name, or null if no @@map directive
103
+ */
104
+ function parseMapDirective(modelBody) {
105
+ const lines = modelBody.split('\n').map(line => line.trim());
106
+
107
+ for (const line of lines) {
108
+ // Match @@map("table_name") or @@map('table_name')
109
+ const match = line.match(/^@@map\(["']([^"']+)["']\)/);
110
+ if (match) {
111
+ return match[1];
112
+ }
113
+ }
114
+
115
+ return null;
116
+ }
117
+
97
118
  /**
98
119
  * Parse model fields from model body
99
120
  * @param {string} modelBody - The content inside model braces
@@ -206,7 +227,8 @@ async function parsePrismaDMMF(prismaClientPath) {
206
227
  name: model.name,
207
228
  fields: {},
208
229
  relations: [],
209
- compositeKey
230
+ compositeKey,
231
+ dbName: model.dbName || model.name.toLowerCase() // Use dbName from DMMF or default to lowercase
210
232
  };
211
233
 
212
234
  for (const field of model.fields) {