@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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/cli.js +30 -0
- package/index.js +11 -0
- package/package.json +63 -0
- package/src/commands/build.js +448 -0
- package/src/generators/modelGenerator.js +168 -0
- package/src/generators/relationshipsGenerator.js +186 -0
- package/src/generators/rlsGenerator.js +334 -0
- package/src/generators/rlsGeneratorV2.js +381 -0
- package/src/generators/routeGenerator.js +127 -0
- package/src/parsers/advancedRLSConverter.js +305 -0
- package/src/parsers/autoRLSConverter.js +322 -0
- package/src/parsers/datasourceParser.js +73 -0
- package/src/parsers/deepSQLAnalyzer.js +540 -0
- package/src/parsers/dynamicRLSConverter.js +353 -0
- package/src/parsers/enhancedRLSConverter.js +181 -0
- package/src/parsers/functionAnalyzer.js +302 -0
- package/src/parsers/postgresRLSConverter.js +192 -0
- package/src/parsers/prismaFilterBuilder.js +422 -0
- package/src/parsers/prismaParser.js +245 -0
- package/src/parsers/sqlToJsConverter.js +611 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a single model file
|
|
6
|
+
* @param {string} modelName - Name of the model
|
|
7
|
+
* @param {Object} modelInfo - Model information from parser
|
|
8
|
+
* @returns {string} - Generated model class code
|
|
9
|
+
*/
|
|
10
|
+
function generateModelFile(modelName, modelInfo) {
|
|
11
|
+
// Capitalize first letter for class name
|
|
12
|
+
const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
13
|
+
|
|
14
|
+
return `const {Model, QueryBuilder, prisma} = require('../Model');
|
|
15
|
+
const {rls} = require('../../rapidd/rapidd');
|
|
16
|
+
|
|
17
|
+
class ${className} extends Model {
|
|
18
|
+
constructor(options){
|
|
19
|
+
super('${modelName}', options);
|
|
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;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} q
|
|
34
|
+
* @property {string|Object} include
|
|
35
|
+
* @param {number} limit
|
|
36
|
+
* @param {number} offset
|
|
37
|
+
* @param {string} sortBy
|
|
38
|
+
* @param {'asc'|'desc'} sortOrder
|
|
39
|
+
* @returns {Object[]}
|
|
40
|
+
*/
|
|
41
|
+
async getMany(q = {}, include = "", limit = 25, offset = 0, sortBy = "id", sortOrder = "asc"){
|
|
42
|
+
return await this._getMany(q, include, Number(limit), Number(offset), sortBy, sortOrder);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {number} id
|
|
47
|
+
* @param {string | Object} include
|
|
48
|
+
* @returns {{} | null}
|
|
49
|
+
*/
|
|
50
|
+
async get(id, include){
|
|
51
|
+
return await this._get(Number(id), include);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {Object} data
|
|
56
|
+
* @returns {Object}
|
|
57
|
+
*/
|
|
58
|
+
async create(data){
|
|
59
|
+
return await this._create(data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {number} id
|
|
64
|
+
* @param {{}} data
|
|
65
|
+
* @returns {Object}
|
|
66
|
+
*/
|
|
67
|
+
async update(id, data){
|
|
68
|
+
return await this._update(Number(id), data);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {number} id
|
|
73
|
+
* @returns {Object}
|
|
74
|
+
*/
|
|
75
|
+
async delete(id){
|
|
76
|
+
return await this._delete(Number(id));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string | Object} include
|
|
81
|
+
* @returns {Object}
|
|
82
|
+
*/
|
|
83
|
+
filter(include){
|
|
84
|
+
return {...this._filter(include), ...this.getAccessFilter()};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string | Object} include
|
|
89
|
+
* @returns {Object}
|
|
90
|
+
*/
|
|
91
|
+
include(include){
|
|
92
|
+
return this._include(include);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {${className}, QueryBuilder, prisma};
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Map Prisma types to JavaScript types
|
|
102
|
+
* @param {string} prismaType - Prisma field type
|
|
103
|
+
* @returns {string} - JavaScript type
|
|
104
|
+
*/
|
|
105
|
+
function mapPrismaTypeToJS(prismaType) {
|
|
106
|
+
const typeMap = {
|
|
107
|
+
'String': 'string',
|
|
108
|
+
'Int': 'number',
|
|
109
|
+
'Float': 'number',
|
|
110
|
+
'Decimal': 'number',
|
|
111
|
+
'Boolean': 'boolean',
|
|
112
|
+
'DateTime': 'Date',
|
|
113
|
+
'Json': 'object',
|
|
114
|
+
'Bytes': 'Buffer'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return typeMap[prismaType] || prismaType;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Generate all model files
|
|
122
|
+
* @param {Object} models - Models object from parser
|
|
123
|
+
* @param {string} modelDir - Directory to output model files
|
|
124
|
+
* @param {string} modelJsPath - Path to output Model.js
|
|
125
|
+
*/
|
|
126
|
+
function generateAllModels(models, modelDir, modelJsPath) {
|
|
127
|
+
// Create model directory if it doesn't exist
|
|
128
|
+
if (!fs.existsSync(modelDir)) {
|
|
129
|
+
fs.mkdirSync(modelDir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Generate individual model files
|
|
133
|
+
for (const [modelName, modelInfo] of Object.entries(models)) {
|
|
134
|
+
const modelCode = generateModelFile(modelName, modelInfo);
|
|
135
|
+
// Capitalize first letter for filename
|
|
136
|
+
const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
137
|
+
const modelPath = path.join(modelDir, `${className}.js`);
|
|
138
|
+
fs.writeFileSync(modelPath, modelCode);
|
|
139
|
+
console.log(`Generated model: ${className}.js`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Copy Model.js to output if it exists in the project
|
|
143
|
+
const sourceModelJs = path.join(process.cwd(), 'Model.js');
|
|
144
|
+
if (fs.existsSync(sourceModelJs)) {
|
|
145
|
+
fs.copyFileSync(sourceModelJs, modelJsPath);
|
|
146
|
+
console.log('Copied Model.js to output');
|
|
147
|
+
} else {
|
|
148
|
+
console.warn('Warning: Model.js not found in project root');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Copy rapidd.js to output if it exists
|
|
152
|
+
const sourceRapiddJs = path.join(process.cwd(), 'rapidd', 'rapidd.js');
|
|
153
|
+
const outputRapiddDir = path.dirname(modelDir.replace(/src[\/\\]Model$/, 'rapidd'));
|
|
154
|
+
const outputRapiddJs = path.join(outputRapiddDir, 'rapidd.js');
|
|
155
|
+
|
|
156
|
+
if (fs.existsSync(sourceRapiddJs)) {
|
|
157
|
+
if (!fs.existsSync(outputRapiddDir)) {
|
|
158
|
+
fs.mkdirSync(outputRapiddDir, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
fs.copyFileSync(sourceRapiddJs, outputRapiddJs);
|
|
161
|
+
console.log('Copied rapidd.js to output');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
generateAllModels,
|
|
167
|
+
generateModelFile
|
|
168
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate relationships.json from Prisma DMMF
|
|
6
|
+
* @param {Object} models - Models object from parser
|
|
7
|
+
* @param {string} outputPath - Path to output relationships.json
|
|
8
|
+
*/
|
|
9
|
+
function generateRelationships(models, outputPath) {
|
|
10
|
+
const relationships = {};
|
|
11
|
+
|
|
12
|
+
for (const [modelName, modelInfo] of Object.entries(models)) {
|
|
13
|
+
relationships[modelName] = {};
|
|
14
|
+
|
|
15
|
+
for (const relation of modelInfo.relations) {
|
|
16
|
+
const relatedModel = models[relation.type];
|
|
17
|
+
|
|
18
|
+
if (!relatedModel) {
|
|
19
|
+
console.warn(`Warning: Related model ${relation.type} not found for ${modelName}.${relation.name}`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if the related model has a composite primary key (many-to-many junction table)
|
|
24
|
+
const compositeKeyFields = getCompositeKeyFromModel(relatedModel);
|
|
25
|
+
|
|
26
|
+
// Only add field/fields if:
|
|
27
|
+
// 1. Related model has composite key (junction table)
|
|
28
|
+
// 2. It's a many relationship (array)
|
|
29
|
+
// 3. The composite key includes this model's foreign key (proving it's a junction for this model)
|
|
30
|
+
const currentModelFk = `${modelName.toLowerCase()}_id`;
|
|
31
|
+
const isJunctionTable = compositeKeyFields &&
|
|
32
|
+
compositeKeyFields.length > 1 &&
|
|
33
|
+
relation.isArray &&
|
|
34
|
+
compositeKeyFields.includes(currentModelFk);
|
|
35
|
+
|
|
36
|
+
if (isJunctionTable) {
|
|
37
|
+
// Many-to-many relationship through junction table
|
|
38
|
+
// Reorder fields so current model's field comes first
|
|
39
|
+
const reorderedFields = reorderFieldsForModel(compositeKeyFields, modelName);
|
|
40
|
+
|
|
41
|
+
relationships[modelName][relation.name] = {
|
|
42
|
+
'object': relation.type,
|
|
43
|
+
'field': compositeKeyFields.join('_'), // e.g., "course_id_teacher_id"
|
|
44
|
+
'fields': reorderedFields
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
// Simple one-to-one or one-to-many relationship
|
|
48
|
+
// Find the foreign key field name
|
|
49
|
+
const foreignKeyField = findForeignKeyField(relation, modelInfo, relatedModel);
|
|
50
|
+
|
|
51
|
+
relationships[modelName][relation.name] = {
|
|
52
|
+
'object': relation.type,
|
|
53
|
+
'field': foreignKeyField || `${relation.type}_id` // Use actual FK or fallback to convention
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Ensure output directory exists
|
|
60
|
+
const outputDir = path.dirname(outputPath);
|
|
61
|
+
if (!fs.existsSync(outputDir)) {
|
|
62
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(outputPath, JSON.stringify(relationships, null, 4));
|
|
66
|
+
console.log('Generated relationships.json');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find the foreign key field for a relation
|
|
71
|
+
* @param {Object} relation - Relation object
|
|
72
|
+
* @param {Object} currentModel - Current model info
|
|
73
|
+
* @param {Object} relatedModel - Related model info
|
|
74
|
+
* @returns {string|null} - Foreign key field name
|
|
75
|
+
*/
|
|
76
|
+
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
|
+
}
|
|
81
|
+
|
|
82
|
+
// For array relations (one-to-many from parent), look for the FK in the related model
|
|
83
|
+
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];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback: convention-based
|
|
95
|
+
return `${relation.type}_id`;
|
|
96
|
+
}
|
|
97
|
+
|
|
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Final fallback
|
|
109
|
+
return `${relation.type}_id`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get composite key fields from a model
|
|
114
|
+
* @param {Object} modelInfo - Model information
|
|
115
|
+
* @returns {Array|null} - Array of composite key field names or null
|
|
116
|
+
*/
|
|
117
|
+
function getCompositeKeyFromModel(modelInfo) {
|
|
118
|
+
// First check if the model has a compositeKey property (from parser)
|
|
119
|
+
if (modelInfo.compositeKey && modelInfo.compositeKey.length > 1) {
|
|
120
|
+
return modelInfo.compositeKey;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback: check for fields marked with isId (for schema parser)
|
|
124
|
+
const compositeFields = [];
|
|
125
|
+
for (const [fieldName, fieldInfo] of Object.entries(modelInfo.fields)) {
|
|
126
|
+
if (fieldInfo.isId && !fieldInfo.isRelation) {
|
|
127
|
+
compositeFields.push(fieldName);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// If we found multiple ID fields, it's a composite key
|
|
132
|
+
if (compositeFields.length > 1) {
|
|
133
|
+
return compositeFields;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reorder composite key fields so the current model's field comes first
|
|
141
|
+
* @param {Array} fields - Array of field names
|
|
142
|
+
* @param {String} currentModelName - Name of the current model
|
|
143
|
+
* @returns {Array} - Reordered array with current model's field first
|
|
144
|
+
*/
|
|
145
|
+
function reorderFieldsForModel(fields, currentModelName) {
|
|
146
|
+
const currentModelField = `${currentModelName.toLowerCase()}_id`;
|
|
147
|
+
const index = fields.indexOf(currentModelField);
|
|
148
|
+
|
|
149
|
+
if (index > 0) {
|
|
150
|
+
// Move current model's field to the front
|
|
151
|
+
const reordered = [...fields];
|
|
152
|
+
reordered.splice(index, 1);
|
|
153
|
+
reordered.unshift(currentModelField);
|
|
154
|
+
return reordered;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return fields;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate relationships.json from schema
|
|
162
|
+
* @param {string} schemaPath - Path to Prisma schema file
|
|
163
|
+
* @param {string} outputPath - Path to output relationships.json
|
|
164
|
+
*/
|
|
165
|
+
function generateRelationshipsFromSchema(schemaPath, outputPath) {
|
|
166
|
+
const { parsePrismaSchema } = require('../parsers/prismaParser');
|
|
167
|
+
const parsedData = parsePrismaSchema(schemaPath);
|
|
168
|
+
generateRelationships(parsedData.models, outputPath);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate relationships.json from DMMF
|
|
173
|
+
* @param {string} prismaClientPath - Path to Prisma client
|
|
174
|
+
* @param {string} outputPath - Path to output relationships.json
|
|
175
|
+
*/
|
|
176
|
+
async function generateRelationshipsFromDMMF(prismaClientPath, outputPath) {
|
|
177
|
+
const { parsePrismaDMMF } = require('../parsers/prismaParser');
|
|
178
|
+
const parsedData = await parsePrismaDMMF(prismaClientPath);
|
|
179
|
+
generateRelationships(parsedData.models, outputPath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
generateRelationships,
|
|
184
|
+
generateRelationshipsFromSchema,
|
|
185
|
+
generateRelationshipsFromDMMF
|
|
186
|
+
};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { Client } = require('pg');
|
|
4
|
+
const { createConverter } = require('../parsers/autoRLSConverter');
|
|
5
|
+
const { analyzeFunctions, generateMappingConfig } = require('../parsers/functionAnalyzer');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Auto-detect user table name (case-insensitive search for user/users)
|
|
9
|
+
* @param {Object} models - Models object from parser
|
|
10
|
+
* @param {string} userTableOption - User-specified table name (optional)
|
|
11
|
+
* @returns {string} - Name of the user table
|
|
12
|
+
*/
|
|
13
|
+
function detectUserTable(models, userTableOption) {
|
|
14
|
+
if (userTableOption) {
|
|
15
|
+
return userTableOption;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const modelNames = Object.keys(models);
|
|
19
|
+
const userTables = modelNames.filter(name =>
|
|
20
|
+
name.toLowerCase() === 'user' || name.toLowerCase() === 'users'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (userTables.length === 0) {
|
|
24
|
+
throw new Error('No user table found (user/users). Please specify --user-table option.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (userTables.length > 1) {
|
|
28
|
+
throw new Error(`Multiple user tables found: ${userTables.join(', ')}. Please specify --user-table option.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return userTables[0];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract RLS policies from PostgreSQL
|
|
36
|
+
* @param {string} databaseUrl - PostgreSQL connection URL
|
|
37
|
+
* @param {Array} modelNames - Array of model names
|
|
38
|
+
* @returns {Object} - RLS policies for each table
|
|
39
|
+
*/
|
|
40
|
+
async function extractPostgreSQLPolicies(databaseUrl, modelNames) {
|
|
41
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await client.connect();
|
|
45
|
+
|
|
46
|
+
const policies = {};
|
|
47
|
+
|
|
48
|
+
// Initialize all models with empty policies
|
|
49
|
+
for (const modelName of modelNames) {
|
|
50
|
+
policies[modelName] = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Query all RLS policies from pg_policies
|
|
54
|
+
const result = await client.query(`
|
|
55
|
+
SELECT
|
|
56
|
+
tablename,
|
|
57
|
+
policyname,
|
|
58
|
+
permissive,
|
|
59
|
+
roles,
|
|
60
|
+
cmd,
|
|
61
|
+
qual,
|
|
62
|
+
with_check
|
|
63
|
+
FROM pg_policies
|
|
64
|
+
WHERE schemaname = 'public'
|
|
65
|
+
ORDER BY tablename, policyname
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
// Group policies by table
|
|
69
|
+
for (const row of result.rows) {
|
|
70
|
+
const tableName = row.tablename;
|
|
71
|
+
if (policies[tableName] !== undefined) {
|
|
72
|
+
policies[tableName].push({
|
|
73
|
+
name: row.policyname,
|
|
74
|
+
permissive: row.permissive === 'PERMISSIVE',
|
|
75
|
+
roles: row.roles,
|
|
76
|
+
command: row.cmd, // SELECT, INSERT, UPDATE, DELETE, ALL
|
|
77
|
+
using: row.qual, // USING expression
|
|
78
|
+
withCheck: row.with_check // WITH CHECK expression
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await client.end();
|
|
84
|
+
return policies;
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
try {
|
|
88
|
+
await client.end();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Ignore cleanup errors
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate RLS functions for a single model from PostgreSQL policies
|
|
98
|
+
* @param {string} modelName - Name of the model
|
|
99
|
+
* @param {Array} policies - Array of policy objects for this model
|
|
100
|
+
* @param {string} userTable - Name of the user table
|
|
101
|
+
* @returns {string} - JavaScript code for RLS functions
|
|
102
|
+
*/
|
|
103
|
+
function generateModelRLS(modelName, policies, userTable) {
|
|
104
|
+
const hasPolicies = policies && policies.length > 0;
|
|
105
|
+
|
|
106
|
+
if (!hasPolicies) {
|
|
107
|
+
// No RLS policies - generate permissive access
|
|
108
|
+
return ` ${modelName}: {
|
|
109
|
+
canCreate: (user) => true,
|
|
110
|
+
hasAccess: (data, user) => true,
|
|
111
|
+
getAccessFilter: (user) => ({}),
|
|
112
|
+
getUpdateFilter: (user) => ({}),
|
|
113
|
+
getDeleteFilter: (user) => ({}),
|
|
114
|
+
getOmitFields: (user) => []
|
|
115
|
+
}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Find policies by command type
|
|
119
|
+
const selectPolicies = policies.filter(p => p.command === 'SELECT' || p.command === 'ALL');
|
|
120
|
+
const insertPolicies = policies.filter(p => p.command === 'INSERT' || p.command === 'ALL');
|
|
121
|
+
const updatePolicies = policies.filter(p => p.command === 'UPDATE' || p.command === 'ALL');
|
|
122
|
+
const deletePolicies = policies.filter(p => p.command === 'DELETE' || p.command === 'ALL');
|
|
123
|
+
|
|
124
|
+
// Generate canCreate (INSERT policies with WITH CHECK)
|
|
125
|
+
const canCreateCode = generateCanCreate(insertPolicies);
|
|
126
|
+
|
|
127
|
+
// Generate hasAccess (SELECT policies with USING)
|
|
128
|
+
const hasAccessCode = generateHasAccess(selectPolicies);
|
|
129
|
+
|
|
130
|
+
// Generate getAccessFilter (SELECT policies)
|
|
131
|
+
const accessFilterCode = generateFilter(selectPolicies, 'using');
|
|
132
|
+
|
|
133
|
+
// Generate getUpdateFilter (UPDATE policies)
|
|
134
|
+
const updateFilterCode = generateFilter(updatePolicies, 'using');
|
|
135
|
+
|
|
136
|
+
// Generate getDeleteFilter (DELETE policies)
|
|
137
|
+
const deleteFilterCode = generateFilter(deletePolicies, 'using');
|
|
138
|
+
|
|
139
|
+
return ` ${modelName}: {
|
|
140
|
+
canCreate: (user) => {
|
|
141
|
+
${canCreateCode}
|
|
142
|
+
},
|
|
143
|
+
hasAccess: (data, user) => {
|
|
144
|
+
${hasAccessCode}
|
|
145
|
+
},
|
|
146
|
+
getAccessFilter: (user) => {
|
|
147
|
+
${accessFilterCode}
|
|
148
|
+
},
|
|
149
|
+
getUpdateFilter: (user) => {
|
|
150
|
+
${updateFilterCode}
|
|
151
|
+
},
|
|
152
|
+
getDeleteFilter: (user) => {
|
|
153
|
+
${deleteFilterCode}
|
|
154
|
+
},
|
|
155
|
+
getOmitFields: (user) => []
|
|
156
|
+
}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generate canCreate function from INSERT policies
|
|
161
|
+
*/
|
|
162
|
+
function generateCanCreate(insertPolicies, converter) {
|
|
163
|
+
if (insertPolicies.length === 0) {
|
|
164
|
+
return 'return true;';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const conditions = [];
|
|
168
|
+
|
|
169
|
+
for (const policy of insertPolicies) {
|
|
170
|
+
const expr = policy.withCheck || policy.using;
|
|
171
|
+
if (expr) {
|
|
172
|
+
try {
|
|
173
|
+
const jsExpr = converter.convertToJavaScript(expr, 'data', 'user');
|
|
174
|
+
conditions.push(jsExpr);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
conditions.push(`true /* Error parsing: ${expr.substring(0, 50)}... */`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (conditions.length === 0) {
|
|
182
|
+
return 'return true;';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Policies are OR'd together (any policy allows)
|
|
186
|
+
return `return ${conditions.join(' || ')};`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate hasAccess function from SELECT policies
|
|
191
|
+
*/
|
|
192
|
+
function generateHasAccess(selectPolicies) {
|
|
193
|
+
if (selectPolicies.length === 0) {
|
|
194
|
+
return 'return true;';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const conditions = [];
|
|
198
|
+
|
|
199
|
+
for (const policy of selectPolicies) {
|
|
200
|
+
if (policy.using) {
|
|
201
|
+
try {
|
|
202
|
+
const jsExpr = convertToJavaScript(policy.using, 'data', 'user');
|
|
203
|
+
conditions.push(jsExpr);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
conditions.push(`true /* Error parsing: ${policy.using.substring(0, 50)}... */`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (conditions.length === 0) {
|
|
211
|
+
return 'return true;';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Policies are OR'd together (any policy allows)
|
|
215
|
+
return `return ${conditions.join(' || ')};`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Generate Prisma filter function
|
|
220
|
+
*/
|
|
221
|
+
function generateFilter(policies, expressionField) {
|
|
222
|
+
if (policies.length === 0) {
|
|
223
|
+
return 'return {};';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const filters = [];
|
|
227
|
+
|
|
228
|
+
for (const policy of policies) {
|
|
229
|
+
const expr = policy[expressionField];
|
|
230
|
+
if (expr) {
|
|
231
|
+
try {
|
|
232
|
+
const prismaFilter = convertToPrismaFilter(expr, 'user');
|
|
233
|
+
if (prismaFilter !== '{}') {
|
|
234
|
+
filters.push(prismaFilter);
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
// On error, return empty filter (permissive)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (filters.length === 0) {
|
|
243
|
+
return 'return {};';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (filters.length === 1) {
|
|
247
|
+
return `return ${filters[0]};`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Multiple policies are OR'd together
|
|
251
|
+
return `return { OR: [${filters.join(', ')}] };`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Generate complete rls.js file
|
|
256
|
+
* @param {Object} models - Models object
|
|
257
|
+
* @param {string} outputPath - Path to output rls.js
|
|
258
|
+
* @param {string} databaseUrl - Database connection URL
|
|
259
|
+
* @param {boolean} isPostgreSQL - Whether database is PostgreSQL
|
|
260
|
+
* @param {string} userTableOption - User-specified table name
|
|
261
|
+
*/
|
|
262
|
+
async function generateRLS(models, outputPath, databaseUrl, isPostgreSQL, userTableOption) {
|
|
263
|
+
const userTable = detectUserTable(models, userTableOption);
|
|
264
|
+
const modelNames = Object.keys(models);
|
|
265
|
+
|
|
266
|
+
let policies = {};
|
|
267
|
+
const timestamp = new Date().toISOString();
|
|
268
|
+
|
|
269
|
+
let rlsCode = `const rls = {\n model: {},\n lastUpdateDate: '${timestamp}'\n};\n\n`;
|
|
270
|
+
|
|
271
|
+
// Analyze PostgreSQL functions if available
|
|
272
|
+
let functionAnalysis = null;
|
|
273
|
+
|
|
274
|
+
if (isPostgreSQL && databaseUrl) {
|
|
275
|
+
console.log('PostgreSQL detected - analyzing database functions...');
|
|
276
|
+
try {
|
|
277
|
+
functionAnalysis = await analyzeFunctions(databaseUrl);
|
|
278
|
+
console.log(`✓ Analyzed ${Object.keys(functionAnalysis.functionMappings).length} PostgreSQL functions`);
|
|
279
|
+
|
|
280
|
+
// Save function analysis for debugging/manual adjustment
|
|
281
|
+
const configPath = path.join(path.dirname(outputPath), 'rls-mappings.json');
|
|
282
|
+
const mappingConfig = generateMappingConfig(functionAnalysis);
|
|
283
|
+
fs.writeFileSync(configPath, JSON.stringify(mappingConfig, null, 2));
|
|
284
|
+
console.log(`✓ Function mappings saved to ${configPath}`);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.warn(`⚠ Could not analyze functions: ${error.message}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log('Extracting RLS policies from database...');
|
|
290
|
+
try {
|
|
291
|
+
policies = await extractPostgreSQLPolicies(databaseUrl, modelNames);
|
|
292
|
+
const totalPolicies = Object.values(policies).reduce((sum, p) => sum + p.length, 0);
|
|
293
|
+
console.log(`✓ Extracted ${totalPolicies} RLS policies from PostgreSQL`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.warn(`⚠ Failed to extract PostgreSQL RLS: ${error.message}`);
|
|
296
|
+
console.log('Generating permissive RLS for all models...');
|
|
297
|
+
// Initialize empty policies for all models
|
|
298
|
+
for (const modelName of modelNames) {
|
|
299
|
+
policies[modelName] = [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
if (!isPostgreSQL) {
|
|
304
|
+
console.log('Non-PostgreSQL database detected (MySQL/SQLite/etc) - RLS not supported');
|
|
305
|
+
}
|
|
306
|
+
console.log('Generating permissive RLS for all models...');
|
|
307
|
+
// Initialize empty policies for all models
|
|
308
|
+
for (const modelName of modelNames) {
|
|
309
|
+
policies[modelName] = [];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Generate RLS for each model
|
|
314
|
+
rlsCode += 'rls.model = {\n';
|
|
315
|
+
const modelRLSCode = modelNames.map(modelName => {
|
|
316
|
+
return generateModelRLS(modelName, policies[modelName], userTable);
|
|
317
|
+
});
|
|
318
|
+
rlsCode += modelRLSCode.join(',\n');
|
|
319
|
+
rlsCode += '\n};\n\n';
|
|
320
|
+
rlsCode += 'module.exports = rls;\n';
|
|
321
|
+
|
|
322
|
+
// Ensure output directory exists
|
|
323
|
+
const outputDir = path.dirname(outputPath);
|
|
324
|
+
if (!fs.existsSync(outputDir)) {
|
|
325
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fs.writeFileSync(outputPath, rlsCode);
|
|
329
|
+
console.log('Generated rls.js');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
generateRLS
|
|
334
|
+
};
|