@rapidd/build 1.1.0 → 1.1.1

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/README.md CHANGED
@@ -5,7 +5,7 @@ Dynamic code generator that transforms Prisma schemas into complete Express.js C
5
5
  ## Features
6
6
 
7
7
  - 🚀 **Automatic CRUD API Generation** - Creates Express.js routes from Prisma models
8
- - 🔒 **RLS Translation** - Converts PostgreSQL Row-Level Security policies to JavaScript/Prisma filters (ACL)
8
+ - 🔒 **RLS Translation** - Converts PostgreSQL Row-Level Security policies to JavaScript/Prisma filters (ACL: Access Control Layer)
9
9
  - 🎯 **Dynamic & Schema-Aware** - Zero hardcoding, adapts to any database structure
10
10
  - 🔗 **Relationship Handling** - Supports 1:1, 1:n, n:m including junction tables
11
11
  - 👥 **Role-Based Access Control** - Properly handles role checks in filters
@@ -50,7 +50,7 @@ npx rapidd build --user-table accounts
50
50
  - `-s, --schema <path>` - Prisma schema file (default: `./prisma/schema.prisma`)
51
51
  - `-m, --model <name>` - Generate/update only specific model (e.g., "account", "user")
52
52
  - `--only <component>` - Generate only specific component: "model", "route", "acl", or "relationship"
53
- - `--user-table <name>` - User table name for RLS (default: auto-detected)
53
+ - `--user-table <name>` - User table name for ACL (default: auto-detected)
54
54
 
55
55
  ## Selective Generation
56
56
 
@@ -122,9 +122,6 @@ CREATE POLICY user_policy ON posts
122
122
 
123
123
  **Generated JavaScript:**
124
124
  ```javascript
125
- hasAccess: (data, user) => {
126
- return data?.author_id === user?.id || ['admin', 'moderator'].includes(user?.role);
127
- },
128
125
  getAccessFilter: (user) => {
129
126
  if (['admin', 'moderator'].includes(user?.role)) return {};
130
127
  return { author_id: user?.id };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidd/build",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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": {
@@ -74,7 +74,7 @@ class Model {
74
74
  'where': this.filter(q)
75
75
  })
76
76
  ]);
77
- return {data, total};
77
+ return {data, meta: {take, skip, total}};
78
78
  }
79
79
  /**
80
80
  * @param {number} id
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { Client } = require('pg');
4
- const { createConverter } = require('../parsers/autoRLSConverter');
5
4
  const { createEnhancedConverter } = require('../parsers/enhancedRLSConverter');
6
5
  const { analyzeFunctions, generateMappingConfig } = require('../parsers/functionAnalyzer');
7
6
 
@@ -48,9 +48,15 @@ function generateRelationships(models, outputPath) {
48
48
  // Find the foreign key field name
49
49
  const foreignKeyField = findForeignKeyField(relation, modelInfo, relatedModel);
50
50
 
51
+ if (!foreignKeyField) {
52
+ // Skip this relationship if we can't find the FK field
53
+ // This usually means the FK is on the other side of the relation
54
+ continue;
55
+ }
56
+
51
57
  relationships[modelName][relation.name] = {
52
58
  'object': relation.type,
53
- 'field': foreignKeyField || `${relation.type}_id` // Use actual FK or fallback to convention
59
+ 'field': foreignKeyField
54
60
  };
55
61
  }
56
62
  }
@@ -74,39 +80,32 @@ function generateRelationships(models, outputPath) {
74
80
  * @returns {string|null} - Foreign key field name
75
81
  */
76
82
  function findForeignKeyField(relation, currentModel, relatedModel) {
77
- // If relation has relationFromFields, use it
78
- if (relation.relationFromFields && relation.relationFromFields.length > 0) {
79
- return relation.relationFromFields[0];
80
- }
83
+ // IMPORTANT: The foreign key field is where the @relation(fields: [...]) is defined
81
84
 
82
- // For array relations (one-to-many from parent), look for the FK in the related model
85
+ // For array relations (one-to-many), the FK is in the related model (child)
83
86
  if (relation.isArray) {
84
- // Find which field in the related model points back to current model
85
- for (const [fieldName, fieldInfo] of Object.entries(relatedModel.fields)) {
86
- if (fieldInfo.relationName === relation.relationName &&
87
- fieldInfo.relationFromFields &&
88
- fieldInfo.relationFromFields.length > 0) {
89
- // This is the FK field in the related model
90
- return fieldInfo.relationFromFields[0];
87
+ // Find the corresponding relation in the related model that points back
88
+ for (const relField of Object.values(relatedModel.fields)) {
89
+ if (relField.kind === 'object' &&
90
+ relField.relationName === relation.relationName &&
91
+ relField.relationFromFields &&
92
+ relField.relationFromFields.length > 0) {
93
+ // This is the FK field in the child (related) model
94
+ return relField.relationFromFields[0];
91
95
  }
92
96
  }
93
-
94
- // Fallback: convention-based
95
- return `${relation.type}_id`;
97
+ // Fallback
98
+ return null;
96
99
  }
97
100
 
98
- // For singular relations (many-to-one), find the FK in current model
99
- for (const [fieldName, fieldInfo] of Object.entries(currentModel.fields)) {
100
- if (fieldInfo.relationName === relation.relationName &&
101
- fieldInfo.relationToFields &&
102
- fieldInfo.relationToFields.length > 0) {
103
- // Found the matching relation field, return its FK
104
- return fieldName;
105
- }
101
+ // For singular relations (many-to-one or one-to-one), check if THIS relation has fields defined
102
+ if (relation.relationFromFields && relation.relationFromFields.length > 0) {
103
+ // The FK is in the current model
104
+ return relation.relationFromFields[0];
106
105
  }
107
106
 
108
- // Final fallback
109
- return `${relation.type}_id`;
107
+ // If no fields on this side, the FK must be on the other side (shouldn't use this relation for filtering)
108
+ return null;
110
109
  }
111
110
 
112
111
  /**
@@ -27,7 +27,7 @@ router.get('/', async function(req, res) {
27
27
  try {
28
28
  const { q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc" } = req.query;
29
29
  const results = await req.${className}.getMany(q, include, limit, offset, sortBy, sortOrder);
30
- return res.sendList(results.data, {'take': req.${className}.take(Number(limit)), 'skip': req.${className}.skip(Number(offset)), 'total': results.total});
30
+ return res.sendList(results.data, results.meta);
31
31
  }
32
32
  catch(error){
33
33
  const response = QueryBuilder.errorHandler(error);
@@ -248,8 +248,18 @@ async function parsePrismaDMMF(prismaClientPath) {
248
248
  name: field.name,
249
249
  type: field.type,
250
250
  isArray: field.isList,
251
- optional: !field.isRequired
251
+ optional: !field.isRequired,
252
+ relationName: field.relationName,
253
+ relationFromFields: field.relationFromFields || [],
254
+ relationToFields: field.relationToFields || [],
255
+ kind: field.kind
252
256
  });
257
+
258
+ // Also add these to the field object for consistency
259
+ models[model.name].fields[field.name].relationName = field.relationName;
260
+ models[model.name].fields[field.name].relationFromFields = field.relationFromFields || [];
261
+ models[model.name].fields[field.name].relationToFields = field.relationToFields || [];
262
+ models[model.name].fields[field.name].kind = field.kind;
253
263
  }
254
264
  }
255
265
  }
@@ -1,305 +0,0 @@
1
- /**
2
- * Advanced PostgreSQL RLS to JavaScript/Prisma Converter
3
- * Handles real production PostgreSQL RLS patterns with custom functions
4
- */
5
-
6
- /**
7
- * Main converter function - PostgreSQL RLS to JavaScript
8
- */
9
- function convertToJavaScript(sql, dataVar = 'data', userVar = 'user') {
10
- if (!sql || sql.trim() === '') {
11
- return 'true';
12
- }
13
-
14
- sql = sql.trim();
15
-
16
- // Remove extra whitespace and newlines for consistent parsing
17
- sql = sql.replace(/\s+/g, ' ').replace(/\n/g, ' ');
18
-
19
- // Handle CASE WHEN expressions
20
- if (sql.toUpperCase().startsWith('CASE')) {
21
- return convertCaseWhen(sql, dataVar, userVar);
22
- }
23
-
24
- // Handle combined conditions with OR
25
- const orMatch = sql.match(/\((.*?)\)\s+OR\s+\((.*?)\)/i);
26
- if (orMatch) {
27
- const left = convertToJavaScript(orMatch[1], dataVar, userVar);
28
- const right = convertToJavaScript(orMatch[2], dataVar, userVar);
29
- return `(${left} || ${right})`;
30
- }
31
-
32
- // Handle role checks: get_current_user_role() = ANY (ARRAY[...])
33
- if (sql.includes('get_current_user_role')) {
34
- return convertRoleCheck(sql, userVar);
35
- }
36
-
37
- // Handle field comparisons with custom functions
38
- const customFuncPatterns = [
39
- { pattern: /teacher_id\s*=\s*get_current_teacher_id\(\)/i, replacement: `${dataVar}?.teacher_id === ${userVar}?.teacher_id` },
40
- { pattern: /student_id\s*=\s*get_current_student_id\(\)/i, replacement: `${dataVar}?.student_id === ${userVar}?.student_id` },
41
- { pattern: /user_id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'[^)]*\)[^)]*\)/i, replacement: `${dataVar}?.user_id === ${userVar}?.id` },
42
- { pattern: /id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'[^)]*\)[^)]*\)/i, replacement: `${dataVar}?.id === ${userVar}?.id` },
43
- { pattern: /(\w+)\s*=\s*get_current_teacher_id\(\)/i, replacement: (m) => `${dataVar}?.${m[1]} === ${userVar}?.teacher_id` },
44
- { pattern: /(\w+)\s*=\s*get_current_student_id\(\)/i, replacement: (m) => `${dataVar}?.${m[1]} === ${userVar}?.student_id` }
45
- ];
46
-
47
- for (const { pattern, replacement } of customFuncPatterns) {
48
- if (pattern.test(sql)) {
49
- if (typeof replacement === 'function') {
50
- const match = sql.match(pattern);
51
- return replacement(match);
52
- }
53
- return replacement;
54
- }
55
- }
56
-
57
- // Handle EXISTS subqueries
58
- if (sql.includes('EXISTS')) {
59
- return convertExistsSubquery(sql, dataVar, userVar);
60
- }
61
-
62
- // Fallback for unhandled patterns
63
- return `true /* Complex RLS expression: ${sql.substring(0, 80)}... */`;
64
- }
65
-
66
- /**
67
- * Convert role check expressions
68
- */
69
- function convertRoleCheck(sql, userVar) {
70
- // Extract roles from ARRAY[...] pattern
71
- const arrayMatch = sql.match(/ARRAY\s*\[([^\]]+)\]/i);
72
- if (arrayMatch) {
73
- const roles = arrayMatch[1]
74
- .split(',')
75
- .map(r => r.trim())
76
- .map(r => r.replace(/::[^,\]]+/g, '')) // Remove type casts (including "character varying")
77
- .map(r => r.replace(/^'|'$/g, '')) // Remove quotes
78
- .map(r => `'${r}'`)
79
- .join(', ');
80
- return `[${roles}].includes(${userVar}?.role)`;
81
- }
82
-
83
- // Simple role equality
84
- const simpleMatch = sql.match(/get_current_user_role\(\)[^=]*=\s*'([^']+)'/i);
85
- if (simpleMatch) {
86
- return `${userVar}?.role === '${simpleMatch[1]}'`;
87
- }
88
-
89
- return `true /* Unparseable role check */`;
90
- }
91
-
92
- /**
93
- * Convert CASE WHEN expressions
94
- */
95
- function convertCaseWhen(sql, dataVar, userVar) {
96
- // Extract the CASE expression and branches
97
- const caseMatch = sql.match(/CASE\s+(\S+(?:\([^)]*\))?)\s+((?:WHEN[\s\S]+?)+)\s*(?:ELSE\s+([\s\S]+?))?\s*END/i);
98
-
99
- if (!caseMatch) {
100
- return `false /* Unparseable CASE expression */`;
101
- }
102
-
103
- const caseExpr = caseMatch[1];
104
- const whenClauses = caseMatch[2];
105
- const elseClause = caseMatch[3];
106
-
107
- // Determine what's being tested
108
- let testExpr = '';
109
- if (caseExpr.includes('get_current_user_role')) {
110
- testExpr = `${userVar}?.role`;
111
- } else if (caseExpr.includes('get_current_teacher_id')) {
112
- testExpr = `${userVar}?.teacher_id`;
113
- } else if (caseExpr.includes('get_current_student_id')) {
114
- testExpr = `${userVar}?.student_id`;
115
- } else {
116
- return `false /* Unsupported CASE expression: ${caseExpr} */`;
117
- }
118
-
119
- // Parse WHEN branches
120
- const whenPattern = /WHEN\s+'?([^':\s]+)'?(?:::\w+)?\s+THEN\s+((?:(?!WHEN\s+|ELSE\s+|END).)+)/gi;
121
- const conditions = [];
122
-
123
- let match;
124
- while ((match = whenPattern.exec(whenClauses)) !== null) {
125
- const value = match[1];
126
- const thenExpr = match[2].trim();
127
-
128
- if (thenExpr.toLowerCase() === 'true') {
129
- conditions.push(`${testExpr} === '${value}'`);
130
- } else if (thenExpr.toLowerCase() === 'false') {
131
- // Skip false conditions
132
- } else {
133
- // Complex THEN expression
134
- const convertedThen = convertThenExpression(thenExpr, dataVar, userVar);
135
- if (convertedThen !== 'false') {
136
- conditions.push(`(${testExpr} === '${value}' && ${convertedThen})`);
137
- }
138
- }
139
- }
140
-
141
- // Handle ELSE clause
142
- if (elseClause && elseClause.trim().toLowerCase() === 'true') {
143
- conditions.push('true');
144
- }
145
-
146
- return conditions.length > 0 ? conditions.join(' || ') : 'false';
147
- }
148
-
149
- /**
150
- * Convert THEN expressions in CASE WHEN
151
- */
152
- function convertThenExpression(expr, dataVar, userVar) {
153
- expr = expr.trim();
154
-
155
- // Remove parentheses
156
- if (expr.startsWith('(') && expr.endsWith(')')) {
157
- expr = expr.substring(1, expr.length - 1).trim();
158
- }
159
-
160
- // Handle EXISTS
161
- if (expr.includes('EXISTS')) {
162
- return convertExistsSubquery(expr, dataVar, userVar);
163
- }
164
-
165
- // Handle field = function() patterns
166
- const patterns = [
167
- { regex: /teacher_id\s*=\s*get_current_teacher_id\(\)/i, js: `${dataVar}?.teacher_id === ${userVar}?.teacher_id` },
168
- { regex: /student_id\s*=\s*get_current_student_id\(\)/i, js: `${dataVar}?.student_id === ${userVar}?.student_id` },
169
- { regex: /id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'[^)]*\)[^)]*\)/i, js: `${dataVar}?.id === ${userVar}?.id` },
170
- { regex: /(\w+)\s*=\s*get_current_(\w+)_id\(\)/i, handler: (m) => `${dataVar}?.${m[1]} === ${userVar}?.${m[2]}_id` }
171
- ];
172
-
173
- for (const pattern of patterns) {
174
- if (pattern.regex.test(expr)) {
175
- if (pattern.handler) {
176
- const match = expr.match(pattern.regex);
177
- return pattern.handler(match);
178
- }
179
- return pattern.js;
180
- }
181
- }
182
-
183
- return 'true';
184
- }
185
-
186
- /**
187
- * Convert EXISTS subqueries to JavaScript
188
- * These typically check relationships between tables
189
- */
190
- function convertExistsSubquery(sql, dataVar, userVar) {
191
- // Common patterns in EXISTS subqueries
192
-
193
- // Pattern 1: Check if student_tariff belongs to current student
194
- if (sql.includes('student_tariff') && sql.includes('get_current_student_id')) {
195
- return `true /* TODO: Check if ${dataVar}?.student_tariff.student_id === ${userVar}?.student_id */`;
196
- }
197
-
198
- // Pattern 2: Check if teacher has access
199
- if (sql.includes('get_current_teacher_id')) {
200
- return `true /* TODO: Check if ${dataVar} is accessible to teacher ${userVar}?.teacher_id */`;
201
- }
202
-
203
- // Pattern 3: Check user address relationships
204
- if (sql.includes('contact_address_id') || sql.includes('billing_address_id')) {
205
- return `true /* TODO: Check if address belongs to user via contact_address_id or billing_address_id */`;
206
- }
207
-
208
- // Pattern 4: Generic student access check
209
- if (sql.includes('student') && sql.includes('get_current_student_id')) {
210
- return `true /* TODO: Check student relationship */`;
211
- }
212
-
213
- // Default EXISTS handling
214
- return `true /* EXISTS subquery requires manual implementation */`;
215
- }
216
-
217
- /**
218
- * Convert to Prisma filter
219
- */
220
- function convertToPrismaFilter(sql, userVar = 'user') {
221
- if (!sql || sql.trim() === '') {
222
- return '{}';
223
- }
224
-
225
- sql = sql.trim().replace(/\s+/g, ' ').replace(/\n/g, ' ');
226
-
227
- // Role-based filters can't be directly applied in Prisma WHERE clause
228
- if (sql.includes('get_current_user_role')) {
229
- return '{}'; // Role checks are done in hasAccess, not in filter
230
- }
231
-
232
- // Handle CASE WHEN - extract filterable conditions
233
- if (sql.toUpperCase().startsWith('CASE')) {
234
- return convertCaseWhenToPrisma(sql, userVar);
235
- }
236
-
237
- // Handle simple field comparisons
238
- const patterns = [
239
- { regex: /teacher_id\s*=\s*get_current_teacher_id\(\)/i, filter: `{ teacher_id: ${userVar}?.teacher_id }` },
240
- { regex: /student_id\s*=\s*get_current_student_id\(\)/i, filter: `{ student_id: ${userVar}?.student_id }` },
241
- { regex: /user_id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i, filter: `{ user_id: ${userVar}?.id }` },
242
- { regex: /^id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i, filter: `{ id: ${userVar}?.id }` }
243
- ];
244
-
245
- for (const pattern of patterns) {
246
- if (pattern.regex.test(sql)) {
247
- return pattern.filter;
248
- }
249
- }
250
-
251
- // Handle OR conditions
252
- const orMatch = sql.match(/\((.*?)\)\s+OR\s+\((.*?)\)/i);
253
- if (orMatch) {
254
- const left = convertToPrismaFilter(orMatch[1], userVar);
255
- const right = convertToPrismaFilter(orMatch[2], userVar);
256
- if (left !== '{}' && right !== '{}') {
257
- return `{ OR: [${left}, ${right}] }`;
258
- } else if (left !== '{}') {
259
- return left;
260
- } else if (right !== '{}') {
261
- return right;
262
- }
263
- }
264
-
265
- return '{}';
266
- }
267
-
268
- /**
269
- * Convert CASE WHEN to Prisma filter
270
- */
271
- function convertCaseWhenToPrisma(sql, userVar) {
272
- // Extract field-based conditions that can be filtered in Prisma
273
- const filters = [];
274
-
275
- // Look for teacher_id = get_current_teacher_id()
276
- if (sql.includes('teacher_id') && sql.includes('get_current_teacher_id')) {
277
- filters.push(`{ teacher_id: ${userVar}?.teacher_id }`);
278
- }
279
-
280
- // Look for student_id = get_current_student_id()
281
- if (sql.includes('student_id') && sql.includes('get_current_student_id')) {
282
- filters.push(`{ student_id: ${userVar}?.student_id }`);
283
- }
284
-
285
- // Look for id = current_setting('app.current_user_id')
286
- if (sql.match(/id\s*=\s*\(\s*current_setting\s*\(\s*'app\.current_user_id'/i)) {
287
- filters.push(`{ id: ${userVar}?.id }`);
288
- }
289
-
290
- if (filters.length === 0) {
291
- return '{}';
292
- }
293
-
294
- if (filters.length === 1) {
295
- return filters[0];
296
- }
297
-
298
- // Multiple conditions are OR'd in CASE WHEN
299
- return `{ OR: [${filters.join(', ')}] }`;
300
- }
301
-
302
- module.exports = {
303
- convertToJavaScript,
304
- convertToPrismaFilter
305
- };