@rapidd/build 1.0.2 → 1.0.4
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 +1 -1
- package/src/commands/build.js +102 -15
- package/src/generators/modelGenerator.js +2 -13
- package/src/generators/rlsGenerator.js +14 -9
- package/src/generators/rlsGeneratorV2.js +24 -9
- package/src/parsers/deepSQLAnalyzer.js +22 -8
- package/src/parsers/dynamicRLSConverter.js +29 -3
- package/src/parsers/prismaParser.js +24 -2
package/package.json
CHANGED
package/src/commands/build.js
CHANGED
|
@@ -12,7 +12,8 @@ const { generateAllRoutes } = require('../generators/routeGenerator');
|
|
|
12
12
|
*/
|
|
13
13
|
function generateBaseModelFile(modelJsPath) {
|
|
14
14
|
const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
|
|
15
|
-
const {
|
|
15
|
+
const {rls} = require('../../rapidd/rapidd');
|
|
16
|
+
const {ErrorResponse} = require('./Api');
|
|
16
17
|
|
|
17
18
|
class Model {
|
|
18
19
|
/**
|
|
@@ -21,17 +22,23 @@ class Model {
|
|
|
21
22
|
*/
|
|
22
23
|
constructor(name, options){
|
|
23
24
|
this.modelName = name;
|
|
25
|
+
this.queryBuilder = new QueryBuilder(name);
|
|
26
|
+
this.rls = rls.model[name] || {};
|
|
24
27
|
this.options = options || {}
|
|
25
28
|
this.user = this.options.user || {'id': 1, 'role': 'application'};
|
|
26
29
|
this.user_id = this.user ? this.user.id : null;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
_select = (fields) => this.
|
|
30
|
-
_filter = (q) => this.
|
|
31
|
-
_include = (include) => this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
_select = (fields) => this.queryBuilder.select(fields);
|
|
33
|
+
_filter = (q) => this.queryBuilder.filter(q);
|
|
34
|
+
_include = (include) => this.queryBuilder.include(include, this.user);
|
|
35
|
+
// RLS METHODS
|
|
36
|
+
_canCreate = () => this.rls.canCreate?.(this.user) || false;
|
|
37
|
+
_hasAccess = (data) => this.rls.hasAccess?.(data, this.user) || false;
|
|
38
|
+
_getAccessFilter = () => this.rls.getAccessFilter?.(this.user);
|
|
39
|
+
_getUpdateFilter = () => this.rls.getUpdateFilter?.(this.user);
|
|
40
|
+
_getDeleteFilter = () => this.rls.getDeleteFilter?.(this.user);
|
|
41
|
+
_omit = () => this.queryBuilder.omit(this.user);
|
|
35
42
|
|
|
36
43
|
/**
|
|
37
44
|
*
|
|
@@ -50,8 +57,7 @@ class Model {
|
|
|
50
57
|
sortBy = sortBy.trim();
|
|
51
58
|
sortOrder = sortOrder.trim();
|
|
52
59
|
if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
|
|
53
|
-
|
|
54
|
-
throw new ErrorResponse(message, 400);
|
|
60
|
+
throw new ErrorResponse(400, "invalid_sort_field", {sortBy, modelName: this.constructor.name});
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
// Query the database using Prisma with filters, pagination, and limits
|
|
@@ -114,8 +120,13 @@ class Model {
|
|
|
114
120
|
* @returns {Promise<Object>}
|
|
115
121
|
*/
|
|
116
122
|
_create = async (data, options = {}) => {
|
|
123
|
+
// CHECK CREATE PERMISSION
|
|
124
|
+
if (!this.canCreate()) {
|
|
125
|
+
throw new ErrorResponse(403, "no_permission_to_create");
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
// VALIDATE PASSED FIELDS AND RELATIONSHIPS
|
|
118
|
-
this.
|
|
129
|
+
this.queryBuilder.create(data, this.user_id);
|
|
119
130
|
|
|
120
131
|
// CREATE
|
|
121
132
|
return await this.prisma.create({
|
|
@@ -132,11 +143,32 @@ class Model {
|
|
|
132
143
|
*/
|
|
133
144
|
_update = async (id, data, options = {}) => {
|
|
134
145
|
id = Number(id);
|
|
135
|
-
|
|
146
|
+
|
|
147
|
+
// CHECK UPDATE PERMISSION
|
|
148
|
+
const updateFilter = this.getUpdateFilter();
|
|
149
|
+
if (updateFilter === false) {
|
|
150
|
+
throw new ErrorResponse(403, "no_permission_to_update");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// GET DATA FIRST (also checks read access)
|
|
136
154
|
const current_data = await this._get(id, "ALL");
|
|
137
155
|
|
|
156
|
+
// If updateFilter is not empty, verify the record matches the filter
|
|
157
|
+
if (Object.keys(updateFilter).length > 0) {
|
|
158
|
+
const canUpdate = await this.prisma.findUnique({
|
|
159
|
+
'where': {
|
|
160
|
+
'id': id,
|
|
161
|
+
...updateFilter
|
|
162
|
+
},
|
|
163
|
+
'select': { 'id': true }
|
|
164
|
+
});
|
|
165
|
+
if (!canUpdate) {
|
|
166
|
+
throw new ErrorResponse(403, "no_permission_to_update");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
138
170
|
// VALIDATE PASSED FIELDS AND RELATIONSHIPS
|
|
139
|
-
this.
|
|
171
|
+
this.queryBuilder.update(id, data, this.user_id);
|
|
140
172
|
return await this.prisma.update({
|
|
141
173
|
'where': {
|
|
142
174
|
'id': id
|
|
@@ -163,9 +195,31 @@ class Model {
|
|
|
163
195
|
* @returns {Promise<Object>}
|
|
164
196
|
*/
|
|
165
197
|
_delete = async (id, options = {}) => {
|
|
166
|
-
|
|
198
|
+
id = Number(id);
|
|
199
|
+
|
|
200
|
+
// CHECK DELETE PERMISSION
|
|
201
|
+
const deleteFilter = this.getDeleteFilter();
|
|
202
|
+
if (deleteFilter === false) {
|
|
203
|
+
throw new ErrorResponse(403, "no_permission_to_delete");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// GET DATA FIRST (also checks read access)
|
|
167
207
|
const current_data = await this._get(id);
|
|
168
208
|
|
|
209
|
+
// If deleteFilter is not empty, verify the record matches the filter
|
|
210
|
+
if (Object.keys(deleteFilter).length > 0) {
|
|
211
|
+
const canDelete = await this.prisma.findUnique({
|
|
212
|
+
'where': {
|
|
213
|
+
'id': id,
|
|
214
|
+
...deleteFilter
|
|
215
|
+
},
|
|
216
|
+
'select': { 'id': true }
|
|
217
|
+
});
|
|
218
|
+
if (!canDelete) {
|
|
219
|
+
throw new ErrorResponse(403, "no_permission_to_delete");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
169
223
|
return await this.prisma.delete({
|
|
170
224
|
'where': {
|
|
171
225
|
id: parseInt(id)
|
|
@@ -233,10 +287,10 @@ class Model {
|
|
|
233
287
|
return this._include(include);
|
|
234
288
|
}
|
|
235
289
|
sort(sortBy, sortOrder) {
|
|
236
|
-
return this.
|
|
290
|
+
return this.queryBuilder.sort(sortBy, sortOrder);
|
|
237
291
|
}
|
|
238
292
|
take(limit){
|
|
239
|
-
return this.
|
|
293
|
+
return this.queryBuilder.take(Number(limit));
|
|
240
294
|
}
|
|
241
295
|
skip(offset){
|
|
242
296
|
const parsed = parseInt(offset);
|
|
@@ -267,6 +321,39 @@ class Model {
|
|
|
267
321
|
return this.user.role == "application" ? true : this._hasAccess(data, this.user);
|
|
268
322
|
}
|
|
269
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Check if user can create records
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
canCreate() {
|
|
329
|
+
if(this.user.role == "application") return true;
|
|
330
|
+
return this._canCreate();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get update filter for RLS
|
|
335
|
+
* @returns {Object|false}
|
|
336
|
+
*/
|
|
337
|
+
getUpdateFilter(){
|
|
338
|
+
const filter = this._getUpdateFilter();
|
|
339
|
+
if(this.user.role == "application" || filter == true){
|
|
340
|
+
return {};
|
|
341
|
+
}
|
|
342
|
+
return filter;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get delete filter for RLS
|
|
347
|
+
* @returns {Object|false}
|
|
348
|
+
*/
|
|
349
|
+
getDeleteFilter(){
|
|
350
|
+
const filter = this._getDeleteFilter();
|
|
351
|
+
if(this.user.role == "application" || filter == true){
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
return filter;
|
|
355
|
+
}
|
|
356
|
+
|
|
270
357
|
set modelName (name){
|
|
271
358
|
this.name = name;
|
|
272
359
|
this.prisma = prisma[name];
|
|
@@ -12,21 +12,10 @@ function generateModelFile(modelName, modelInfo) {
|
|
|
12
12
|
const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
13
13
|
|
|
14
14
|
return `const {Model, QueryBuilder, prisma} = require('../Model');
|
|
15
|
-
const {rls} = require('../../rapidd/rapidd');
|
|
16
15
|
|
|
17
16
|
class ${className} extends Model {
|
|
18
17
|
constructor(options){
|
|
19
|
-
super('${
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static queryBuilder = new QueryBuilder('${modelName}', rls.model.${modelName} || {});
|
|
23
|
-
|
|
24
|
-
static getAccessFilter(user) {
|
|
25
|
-
return rls.model.${modelName}?.getAccessFilter?.(user) || {};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static hasAccess(data, user) {
|
|
29
|
-
return rls.model.${modelName}?.hasAccess?.(data, user) || true;
|
|
18
|
+
super('${className}', options);
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
/**
|
|
@@ -150,7 +139,7 @@ function generateAllModels(models, modelDir, modelJsPath) {
|
|
|
150
139
|
|
|
151
140
|
// Copy rapidd.js to output if it exists
|
|
152
141
|
const sourceRapiddJs = path.join(process.cwd(), 'rapidd', 'rapidd.js');
|
|
153
|
-
const outputRapiddDir =
|
|
142
|
+
const outputRapiddDir = modelDir.replace(/src[\/\\]Model$/, 'rapidd');
|
|
154
143
|
const outputRapiddJs = path.join(outputRapiddDir, 'rapidd.js');
|
|
155
144
|
|
|
156
145
|
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 {
|
|
38
|
-
* @returns {Object} - RLS policies for each
|
|
37
|
+
* @param {Object} models - Models object with dbName mapping
|
|
38
|
+
* @returns {Object} - RLS policies for each model
|
|
39
39
|
*/
|
|
40
|
-
async function extractPostgreSQLPolicies(databaseUrl,
|
|
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
|
-
//
|
|
49
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
44
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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) {
|