@rapidd/build 1.0.8 → 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 +13 -22
- package/package.json +1 -1
- package/src/commands/build.js +6 -1
- package/src/generators/aclGenerator.js +0 -1
- package/src/generators/relationshipsGenerator.js +25 -26
- package/src/generators/routeGenerator.js +1 -1
- package/src/parsers/prismaParser.js +11 -1
- package/src/parsers/advancedRLSConverter.js +0 -305
- package/src/parsers/autoRLSConverter.js +0 -322
- package/src/parsers/dynamicRLSConverter.js +0 -379
- package/src/parsers/postgresRLSConverter.js +0 -192
- package/src/parsers/sqlToJsConverter.js +0 -611
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
|
|
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
|
|
@@ -34,7 +34,7 @@ npx rapidd build --model user
|
|
|
34
34
|
# Generate only specific component
|
|
35
35
|
npx rapidd build --only model
|
|
36
36
|
npx rapidd build --only route
|
|
37
|
-
npx rapidd build --only
|
|
37
|
+
npx rapidd build --only acl
|
|
38
38
|
npx rapidd build --only relationship
|
|
39
39
|
|
|
40
40
|
# Combine model and component filters
|
|
@@ -49,8 +49,8 @@ npx rapidd build --user-table accounts
|
|
|
49
49
|
- `-o, --output <path>` - Output directory (default: `./`)
|
|
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
|
-
- `--only <component>` - Generate only specific component: "model", "route", "
|
|
53
|
-
- `--user-table <name>` - User table name for
|
|
52
|
+
- `--only <component>` - Generate only specific component: "model", "route", "acl", or "relationship"
|
|
53
|
+
- `--user-table <name>` - User table name for ACL (default: auto-detected)
|
|
54
54
|
|
|
55
55
|
## Selective Generation
|
|
56
56
|
|
|
@@ -65,7 +65,7 @@ This will:
|
|
|
65
65
|
- Generate/update `src/Model/Account.js`
|
|
66
66
|
- Generate/update `routes/api/v1/account.js`
|
|
67
67
|
- Update the `account` entry in `rapidd/relationships.json`
|
|
68
|
-
- Update the `account` entry in `rapidd/
|
|
68
|
+
- Update the `account` entry in `rapidd/acl.js`
|
|
69
69
|
|
|
70
70
|
### Update Single Component
|
|
71
71
|
|
|
@@ -73,8 +73,8 @@ This will:
|
|
|
73
73
|
# Regenerate all routes
|
|
74
74
|
npx rapidd build --only route
|
|
75
75
|
|
|
76
|
-
# Regenerate all
|
|
77
|
-
npx rapidd build --only
|
|
76
|
+
# Regenerate all ACL configs
|
|
77
|
+
npx rapidd build --only acl
|
|
78
78
|
|
|
79
79
|
# Regenerate all models
|
|
80
80
|
npx rapidd build --only model
|
|
@@ -89,8 +89,8 @@ npx rapidd build --only relationship
|
|
|
89
89
|
# Update only the route for a specific model
|
|
90
90
|
npx rapidd build --model user --only route
|
|
91
91
|
|
|
92
|
-
# Update
|
|
93
|
-
npx rapidd build --model account --only
|
|
92
|
+
# Update ACL for account model
|
|
93
|
+
npx rapidd build --model account --only acl
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
## Generated Structure
|
|
@@ -106,12 +106,12 @@ npx rapidd build --model account --only rls
|
|
|
106
106
|
│ ├── post.js
|
|
107
107
|
│ └── ...
|
|
108
108
|
└── rapidd/
|
|
109
|
-
├──
|
|
109
|
+
├── acl.js
|
|
110
110
|
├── relationships.json
|
|
111
111
|
└── rapidd.js
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
##
|
|
114
|
+
## ACL Translation Example
|
|
115
115
|
|
|
116
116
|
**PostgreSQL Policy:**
|
|
117
117
|
```sql
|
|
@@ -122,21 +122,12 @@ 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 };
|
|
131
128
|
}
|
|
132
129
|
```
|
|
133
130
|
|
|
134
|
-
## Usage with PostgreSQL RLS
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" npx rapidd build
|
|
138
|
-
```
|
|
139
|
-
|
|
140
131
|
## Use Cases
|
|
141
132
|
|
|
142
133
|
### During Development
|
|
@@ -148,7 +139,7 @@ npx rapidd build --model newModel
|
|
|
148
139
|
npx rapidd build --only relationship
|
|
149
140
|
|
|
150
141
|
# After updating RLS policies
|
|
151
|
-
npx rapidd build --only
|
|
142
|
+
npx rapidd build --only acl
|
|
152
143
|
```
|
|
153
144
|
|
|
154
145
|
### Continuous Integration
|
|
@@ -161,7 +152,7 @@ npx rapidd build --output ./generated
|
|
|
161
152
|
```bash
|
|
162
153
|
# Update specific model after schema changes
|
|
163
154
|
npx rapidd build --model user --only model
|
|
164
|
-
npx rapidd build --model user --only
|
|
155
|
+
npx rapidd build --model user --only acl
|
|
165
156
|
```
|
|
166
157
|
|
|
167
158
|
## License
|
package/package.json
CHANGED
package/src/commands/build.js
CHANGED
|
@@ -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
|
|
@@ -597,8 +597,13 @@ const prisma = new PrismaClient({
|
|
|
597
597
|
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
598
598
|
});
|
|
599
599
|
|
|
600
|
+
const prismaTransaction = async (operations) => prisma.$transaction(async (tx) => {
|
|
601
|
+
return Promise.all(operations.map(op => op(tx)));
|
|
602
|
+
});
|
|
603
|
+
|
|
600
604
|
module.exports = {
|
|
601
605
|
prisma,
|
|
606
|
+
prismaTransaction,
|
|
602
607
|
PrismaClient,
|
|
603
608
|
acl
|
|
604
609
|
};
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
85
|
+
// For array relations (one-to-many), the FK is in the related model (child)
|
|
83
86
|
if (relation.isArray) {
|
|
84
|
-
// Find
|
|
85
|
-
for (const
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
return `${relation.type}_id`;
|
|
97
|
+
// Fallback
|
|
98
|
+
return null;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// For singular relations (many-to-one),
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
//
|
|
109
|
-
return
|
|
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,
|
|
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
|
-
};
|