@rapidd/build 1.2.3 → 2.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/README.md +219 -68
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +31 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/src/commands/build.d.ts +17 -0
- package/dist/src/commands/build.d.ts.map +1 -0
- package/dist/src/commands/build.js +236 -0
- package/dist/src/commands/build.js.map +1 -0
- package/dist/src/generators/aclGenerator.d.ts +6 -0
- package/dist/src/generators/aclGenerator.d.ts.map +1 -0
- package/dist/src/generators/aclGenerator.js +384 -0
- package/dist/src/generators/aclGenerator.js.map +1 -0
- package/dist/src/generators/index.d.ts +4 -0
- package/dist/src/generators/index.d.ts.map +1 -0
- package/dist/src/generators/index.js +13 -0
- package/dist/src/generators/index.js.map +1 -0
- package/dist/src/generators/modelGenerator.d.ts +10 -0
- package/dist/src/generators/modelGenerator.d.ts.map +1 -0
- package/dist/src/generators/modelGenerator.js +143 -0
- package/dist/src/generators/modelGenerator.js.map +1 -0
- package/dist/src/generators/routeGenerator.d.ts +10 -0
- package/dist/src/generators/routeGenerator.d.ts.map +1 -0
- package/dist/src/generators/routeGenerator.js +172 -0
- package/dist/src/generators/routeGenerator.js.map +1 -0
- package/dist/src/parsers/datasourceParser.d.ts +11 -0
- package/dist/src/parsers/datasourceParser.d.ts.map +1 -0
- package/dist/src/parsers/datasourceParser.js +131 -0
- package/dist/src/parsers/datasourceParser.js.map +1 -0
- package/dist/src/parsers/deepSQLAnalyzer.d.ts +85 -0
- package/dist/src/parsers/deepSQLAnalyzer.d.ts.map +1 -0
- package/dist/src/parsers/deepSQLAnalyzer.js +482 -0
- package/dist/src/parsers/deepSQLAnalyzer.js.map +1 -0
- package/dist/src/parsers/enhancedRLSConverter.d.ts +14 -0
- package/dist/src/parsers/enhancedRLSConverter.d.ts.map +1 -0
- package/dist/src/parsers/enhancedRLSConverter.js +168 -0
- package/dist/src/parsers/enhancedRLSConverter.js.map +1 -0
- package/dist/src/parsers/functionAnalyzer.d.ts +55 -0
- package/dist/src/parsers/functionAnalyzer.d.ts.map +1 -0
- package/dist/src/parsers/functionAnalyzer.js +274 -0
- package/dist/src/parsers/functionAnalyzer.js.map +1 -0
- package/dist/src/parsers/index.d.ts +13 -0
- package/dist/src/parsers/index.d.ts.map +1 -0
- package/dist/src/parsers/index.js +20 -0
- package/dist/src/parsers/index.js.map +1 -0
- package/dist/src/parsers/prismaFilterBuilder.d.ts +79 -0
- package/dist/src/parsers/prismaFilterBuilder.d.ts.map +1 -0
- package/dist/src/parsers/prismaFilterBuilder.js +322 -0
- package/dist/src/parsers/prismaFilterBuilder.js.map +1 -0
- package/dist/src/parsers/prismaParser.d.ts +14 -0
- package/dist/src/parsers/prismaParser.d.ts.map +1 -0
- package/dist/src/parsers/prismaParser.js +263 -0
- package/dist/src/parsers/prismaParser.js.map +1 -0
- package/package.json +21 -13
- package/bin/cli.js +0 -33
- package/index.js +0 -11
- package/src/commands/build.js +0 -638
- package/src/generators/aclGenerator.js +0 -394
- package/src/generators/modelGenerator.js +0 -174
- package/src/generators/relationshipsGenerator.js +0 -200
- package/src/generators/routeGenerator.js +0 -119
- package/src/parsers/datasourceParser.js +0 -121
- package/src/parsers/deepSQLAnalyzer.js +0 -554
- package/src/parsers/enhancedRLSConverter.js +0 -181
- package/src/parsers/functionAnalyzer.js +0 -302
- package/src/parsers/prismaFilterBuilder.js +0 -422
- package/src/parsers/prismaParser.js +0 -287
package/src/commands/build.js
DELETED
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { parsePrismaSchema, parsePrismaDMMF } = require('../parsers/prismaParser');
|
|
4
|
-
const { generateAllModels } = require('../generators/modelGenerator');
|
|
5
|
-
const { generateRelationshipsFromDMMF, generateRelationshipsFromSchema } = require('../generators/relationshipsGenerator');
|
|
6
|
-
const { generateACL } = require('../generators/aclGenerator');
|
|
7
|
-
const { parseDatasource } = require('../parsers/datasourceParser');
|
|
8
|
-
const { generateAllRoutes } = require('../generators/routeGenerator');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generate rapidd/rapidd.js file
|
|
12
|
-
* @param {string} rapiddJsPath - Path to rapidd.js
|
|
13
|
-
* @param {boolean} isPostgreSQL - Whether the database is PostgreSQL
|
|
14
|
-
*/
|
|
15
|
-
function generateRapiddFile(rapiddJsPath, isPostgreSQL = true) {
|
|
16
|
-
let content;
|
|
17
|
-
|
|
18
|
-
if (isPostgreSQL) {
|
|
19
|
-
// PostgreSQL version with RLS support
|
|
20
|
-
content = `const { PrismaClient, Prisma } = require('../prisma/client');
|
|
21
|
-
const { AsyncLocalStorage } = require('async_hooks');
|
|
22
|
-
const acl = require('./acl');
|
|
23
|
-
|
|
24
|
-
// Request Context Storage
|
|
25
|
-
const requestContext = new AsyncLocalStorage();
|
|
26
|
-
|
|
27
|
-
// RLS Configuration aus Environment Variables
|
|
28
|
-
const RLS_CONFIG = {
|
|
29
|
-
namespace: process.env.RLS_NAMESPACE || 'app',
|
|
30
|
-
userId: process.env.RLS_USER_ID || 'current_user_id',
|
|
31
|
-
userRole: process.env.RLS_USER_ROLE || 'current_user_role',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// =====================================================
|
|
35
|
-
// BASE PRISMA CLIENTS
|
|
36
|
-
// =====================================================
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* ADMIN CLIENT - Bypasses ALL RLS
|
|
40
|
-
* Uses DATABASE_URL_ADMIN connection (e.g., app_auth_proxy user)
|
|
41
|
-
* Use ONLY for authentication operations:
|
|
42
|
-
* - Login
|
|
43
|
-
* - Register
|
|
44
|
-
* - Email Verification
|
|
45
|
-
* - Password Reset
|
|
46
|
-
* - OAuth operations
|
|
47
|
-
*/
|
|
48
|
-
const authPrisma = new PrismaClient({
|
|
49
|
-
datasources: {
|
|
50
|
-
db: {
|
|
51
|
-
url: process.env.DATABASE_URL_ADMIN
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* BASE CLIENT - Regular user with RLS
|
|
59
|
-
* Uses DATABASE_URL connection
|
|
60
|
-
* Use for all business operations
|
|
61
|
-
*/
|
|
62
|
-
const basePrisma = new PrismaClient({
|
|
63
|
-
datasources: {
|
|
64
|
-
db: {
|
|
65
|
-
url: process.env.DATABASE_URL
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// =====================================================
|
|
72
|
-
// RLS HELPER FUNCTIONS
|
|
73
|
-
// =====================================================
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Set RLS Session Variables in PostgreSQL
|
|
77
|
-
* Execute each SET command separately to avoid prepared statement error
|
|
78
|
-
*/
|
|
79
|
-
async function setRLSVariables(tx, userId, userRole) {
|
|
80
|
-
const namespace = RLS_CONFIG.namespace;
|
|
81
|
-
const userIdVar = RLS_CONFIG.userId;
|
|
82
|
-
const userRoleVar = RLS_CONFIG.userRole;
|
|
83
|
-
|
|
84
|
-
// Execute SET commands separately
|
|
85
|
-
await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userIdVar} = '\${userId}'\`);
|
|
86
|
-
await tx.$executeRawUnsafe(\`SET LOCAL \${namespace}.\${userRoleVar} = '\${userRole}'\`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Reset RLS Session Variables
|
|
91
|
-
*/
|
|
92
|
-
async function resetRLSVariables(tx) {
|
|
93
|
-
const namespace = RLS_CONFIG.namespace;
|
|
94
|
-
const userIdVar = RLS_CONFIG.userId;
|
|
95
|
-
const userRoleVar = RLS_CONFIG.userRole;
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
await tx.$executeRawUnsafe(\`RESET \${namespace}.\${userIdVar}\`);
|
|
99
|
-
await tx.$executeRawUnsafe(\`RESET \${namespace}.\${userRoleVar}\`);
|
|
100
|
-
} catch (e) {
|
|
101
|
-
// Ignore errors on reset
|
|
102
|
-
console.error('Failed to reset RLS variables:', e);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// =====================================================
|
|
107
|
-
// EXTENDED PRISMA WITH AUTOMATIC RLS
|
|
108
|
-
// =====================================================
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Extended Prisma Client with automatic RLS context
|
|
112
|
-
* Automatically wraps all operations in RLS context from AsyncLocalStorage
|
|
113
|
-
*/
|
|
114
|
-
const prisma = basePrisma.$extends({
|
|
115
|
-
query: {
|
|
116
|
-
async $allOperations({ operation, args, query, model }) {
|
|
117
|
-
const context = requestContext.getStore();
|
|
118
|
-
|
|
119
|
-
// No context = no RLS (e.g., system operations)
|
|
120
|
-
if (!context?.userId || !context?.userRole) {
|
|
121
|
-
return query(args);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const { userId, userRole } = context;
|
|
125
|
-
|
|
126
|
-
// For operations that are already transactions, just set the variables
|
|
127
|
-
if (operation === '$transaction') {
|
|
128
|
-
return basePrisma.$transaction(async (tx) => {
|
|
129
|
-
await setRLSVariables(tx, userId, userRole);
|
|
130
|
-
return query(args);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// For regular operations, wrap in transaction with RLS
|
|
135
|
-
return basePrisma.$transaction(async (tx) => {
|
|
136
|
-
// Set session variables
|
|
137
|
-
await setRLSVariables(tx, userId, userRole);
|
|
138
|
-
|
|
139
|
-
// Execute the original query using the transaction client
|
|
140
|
-
if (model) {
|
|
141
|
-
// Model query (e.g., user.findMany())
|
|
142
|
-
return tx[model][operation](args);
|
|
143
|
-
} else {
|
|
144
|
-
// Raw query or special operation
|
|
145
|
-
return tx[operation](args);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// =====================================================
|
|
153
|
-
// TRANSACTION HELPERS
|
|
154
|
-
// =====================================================
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Helper for batch operations in single transaction
|
|
158
|
-
*/
|
|
159
|
-
async function prismaTransaction(operations) {
|
|
160
|
-
const context = requestContext.getStore();
|
|
161
|
-
|
|
162
|
-
return basePrisma.$transaction(async (tx) => {
|
|
163
|
-
if (context?.userId && context?.userRole) {
|
|
164
|
-
await setRLSVariables(tx, context.userId, context.userRole);
|
|
165
|
-
}
|
|
166
|
-
return Promise.all(operations.map(op => op(tx)));
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// =====================================================
|
|
171
|
-
// CONTEXT HELPERS
|
|
172
|
-
// =====================================================
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Express Middleware: Set RLS context from authenticated user
|
|
176
|
-
* Use this AFTER your authentication middleware
|
|
177
|
-
*/
|
|
178
|
-
function setRLSContext(req, res, next) {
|
|
179
|
-
if (req.user) {
|
|
180
|
-
// Set context for async operations
|
|
181
|
-
requestContext.run(
|
|
182
|
-
{
|
|
183
|
-
userId: req.user.id,
|
|
184
|
-
userRole: req.user.role
|
|
185
|
-
},
|
|
186
|
-
() => next()
|
|
187
|
-
);
|
|
188
|
-
} else {
|
|
189
|
-
next();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Get RLS Config (for SQL generation)
|
|
195
|
-
*/
|
|
196
|
-
function getRLSConfig() {
|
|
197
|
-
return RLS_CONFIG;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// =====================================================
|
|
201
|
-
// GRACEFUL SHUTDOWN
|
|
202
|
-
// =====================================================
|
|
203
|
-
|
|
204
|
-
async function disconnectAll() {
|
|
205
|
-
await authPrisma.$disconnect();
|
|
206
|
-
await basePrisma.$disconnect();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
process.on('beforeExit', async () => {
|
|
210
|
-
await disconnectAll();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// =====================================================
|
|
214
|
-
// EXPORTS
|
|
215
|
-
// =====================================================
|
|
216
|
-
|
|
217
|
-
module.exports = {
|
|
218
|
-
// Main clients
|
|
219
|
-
prisma, // Use for regular operations with automatic RLS from context
|
|
220
|
-
authPrisma, // Use ONLY for auth operations (login, register, etc.)
|
|
221
|
-
|
|
222
|
-
// Transaction helpers
|
|
223
|
-
prismaTransaction,
|
|
224
|
-
|
|
225
|
-
// Context helpers
|
|
226
|
-
requestContext,
|
|
227
|
-
setRLSContext,
|
|
228
|
-
|
|
229
|
-
// RLS utilities
|
|
230
|
-
setRLSVariables,
|
|
231
|
-
resetRLSVariables,
|
|
232
|
-
getRLSConfig,
|
|
233
|
-
|
|
234
|
-
// Utilities
|
|
235
|
-
disconnectAll,
|
|
236
|
-
PrismaClient,
|
|
237
|
-
Prisma,
|
|
238
|
-
acl
|
|
239
|
-
};
|
|
240
|
-
`;
|
|
241
|
-
} else {
|
|
242
|
-
// Non-PostgreSQL version (MySQL, SQLite, etc.) - simplified without RLS
|
|
243
|
-
content = `const { PrismaClient } = require('../prisma/client');
|
|
244
|
-
const acl = require('./acl');
|
|
245
|
-
|
|
246
|
-
// Standard Prisma Client
|
|
247
|
-
const prisma = new PrismaClient({
|
|
248
|
-
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const prismaTransaction = async (operations) => prisma.$transaction(async (tx) => {
|
|
252
|
-
return Promise.all(operations.map(op => op(tx)));
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
module.exports = {
|
|
256
|
-
prisma,
|
|
257
|
-
prismaTransaction,
|
|
258
|
-
PrismaClient,
|
|
259
|
-
acl
|
|
260
|
-
};
|
|
261
|
-
`;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Ensure rapidd directory exists
|
|
265
|
-
const rapiddDir = path.dirname(rapiddJsPath);
|
|
266
|
-
if (!fs.existsSync(rapiddDir)) {
|
|
267
|
-
fs.mkdirSync(rapiddDir, { recursive: true });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
fs.writeFileSync(rapiddJsPath, content);
|
|
271
|
-
console.log('✓ Generated rapidd/rapidd.js');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Update relationships.json for a specific model
|
|
276
|
-
*/
|
|
277
|
-
async function updateRelationshipsForModel(filteredModels, relationshipsPath, schemaPath, usedDMMF) {
|
|
278
|
-
let existingRelationships = {};
|
|
279
|
-
|
|
280
|
-
// Load existing relationships if file exists
|
|
281
|
-
if (fs.existsSync(relationshipsPath)) {
|
|
282
|
-
try {
|
|
283
|
-
existingRelationships = JSON.parse(fs.readFileSync(relationshipsPath, 'utf8'));
|
|
284
|
-
} catch (error) {
|
|
285
|
-
console.warn('Could not parse existing relationships.json, will create new');
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Generate relationships for the filtered model(s)
|
|
290
|
-
let newRelationships = {};
|
|
291
|
-
if (usedDMMF) {
|
|
292
|
-
// Use DMMF to get relationships for specific model
|
|
293
|
-
const { generateRelationshipsFromDMMF } = require('../generators/relationshipsGenerator');
|
|
294
|
-
const tempPath = relationshipsPath + '.tmp';
|
|
295
|
-
await generateRelationshipsFromDMMF(schemaPath, tempPath);
|
|
296
|
-
const allRelationships = JSON.parse(fs.readFileSync(tempPath, 'utf8'));
|
|
297
|
-
fs.unlinkSync(tempPath);
|
|
298
|
-
|
|
299
|
-
// Extract only the filtered model's relationships
|
|
300
|
-
for (const modelName of Object.keys(filteredModels)) {
|
|
301
|
-
if (allRelationships[modelName]) {
|
|
302
|
-
newRelationships[modelName] = allRelationships[modelName];
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
// Use schema parser
|
|
307
|
-
const { generateRelationshipsFromSchema } = require('../generators/relationshipsGenerator');
|
|
308
|
-
const tempPath = relationshipsPath + '.tmp';
|
|
309
|
-
generateRelationshipsFromSchema(schemaPath, tempPath);
|
|
310
|
-
const allRelationships = JSON.parse(fs.readFileSync(tempPath, 'utf8'));
|
|
311
|
-
fs.unlinkSync(tempPath);
|
|
312
|
-
|
|
313
|
-
// Extract only the filtered model's relationships
|
|
314
|
-
for (const modelName of Object.keys(filteredModels)) {
|
|
315
|
-
if (allRelationships[modelName]) {
|
|
316
|
-
newRelationships[modelName] = allRelationships[modelName];
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Merge with existing relationships
|
|
322
|
-
const updatedRelationships = { ...existingRelationships, ...newRelationships };
|
|
323
|
-
|
|
324
|
-
// Write back to file
|
|
325
|
-
fs.writeFileSync(relationshipsPath, JSON.stringify(updatedRelationships, null, 2));
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Update acl.js for a specific model
|
|
330
|
-
*/
|
|
331
|
-
async function updateACLForModel(filteredModels, allModels, aclPath, datasource, userTable, relationships, debug = false) {
|
|
332
|
-
const { generateACL } = require('../generators/aclGenerator');
|
|
333
|
-
|
|
334
|
-
// Generate ACL for the filtered model (but pass all models for user table detection)
|
|
335
|
-
const tempPath = aclPath + '.tmp';
|
|
336
|
-
await generateACL(
|
|
337
|
-
filteredModels,
|
|
338
|
-
tempPath,
|
|
339
|
-
datasource.url,
|
|
340
|
-
datasource.isPostgreSQL,
|
|
341
|
-
userTable,
|
|
342
|
-
relationships,
|
|
343
|
-
debug,
|
|
344
|
-
allModels
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
// Read the generated ACL for the specific model
|
|
348
|
-
const tempContent = fs.readFileSync(tempPath, 'utf8');
|
|
349
|
-
fs.unlinkSync(tempPath);
|
|
350
|
-
|
|
351
|
-
// Extract the model's ACL configuration
|
|
352
|
-
const modelName = Object.keys(filteredModels)[0];
|
|
353
|
-
|
|
354
|
-
// Find the start of the model definition
|
|
355
|
-
const modelStart = tempContent.indexOf(`${modelName}:`);
|
|
356
|
-
if (modelStart === -1) {
|
|
357
|
-
throw new Error(`Could not find model ${modelName} in generated RLS`);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Find the matching closing brace by counting braces
|
|
361
|
-
let braceCount = 0;
|
|
362
|
-
let inString = false;
|
|
363
|
-
let stringChar = null;
|
|
364
|
-
let i = tempContent.indexOf('{', modelStart);
|
|
365
|
-
const contentStart = i;
|
|
366
|
-
|
|
367
|
-
for (; i < tempContent.length; i++) {
|
|
368
|
-
const char = tempContent[i];
|
|
369
|
-
const prevChar = i > 0 ? tempContent[i - 1] : '';
|
|
370
|
-
|
|
371
|
-
// Handle string literals
|
|
372
|
-
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
|
373
|
-
if (!inString) {
|
|
374
|
-
inString = true;
|
|
375
|
-
stringChar = char;
|
|
376
|
-
} else if (char === stringChar) {
|
|
377
|
-
inString = false;
|
|
378
|
-
stringChar = null;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (!inString) {
|
|
383
|
-
if (char === '{') braceCount++;
|
|
384
|
-
if (char === '}') braceCount--;
|
|
385
|
-
|
|
386
|
-
if (braceCount === 0) {
|
|
387
|
-
// Found the closing brace
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (braceCount !== 0) {
|
|
394
|
-
throw new Error(`Could not extract ACL for model ${modelName} - unmatched braces`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const modelAcl = tempContent.substring(modelStart, i + 1);
|
|
398
|
-
|
|
399
|
-
// Read existing acl.js
|
|
400
|
-
if (fs.existsSync(aclPath)) {
|
|
401
|
-
let existingContent = fs.readFileSync(aclPath, 'utf8');
|
|
402
|
-
|
|
403
|
-
// Check if model already exists in ACL
|
|
404
|
-
const existingModelPattern = new RegExp(`${modelName}:\\s*\\{[\\s\\S]*?\\n \\}(?=,|\\n)`);
|
|
405
|
-
|
|
406
|
-
if (existingModelPattern.test(existingContent)) {
|
|
407
|
-
// Replace existing model ACL
|
|
408
|
-
existingContent = existingContent.replace(existingModelPattern, modelAcl);
|
|
409
|
-
} else {
|
|
410
|
-
// Add new model ACL before the closing of acl.model
|
|
411
|
-
// Find the last closing brace of a model object and add comma after it
|
|
412
|
-
existingContent = existingContent.replace(
|
|
413
|
-
/(\n \})\n(\};)/,
|
|
414
|
-
`$1,\n ${modelAcl}\n$2`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
fs.writeFileSync(aclPath, existingContent);
|
|
419
|
-
console.log(`✓ Updated RLS for model: ${modelName}`);
|
|
420
|
-
} else {
|
|
421
|
-
// If acl.js doesn't exist, create it with just this model
|
|
422
|
-
await generateACL(
|
|
423
|
-
filteredModels,
|
|
424
|
-
aclPath,
|
|
425
|
-
datasource.url,
|
|
426
|
-
datasource.isPostgreSQL,
|
|
427
|
-
userTable,
|
|
428
|
-
relationships,
|
|
429
|
-
debug,
|
|
430
|
-
allModels
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Build models from Prisma schema
|
|
437
|
-
* @param {Object} options - Build options
|
|
438
|
-
* @param {string} options.schema - Path to Prisma schema file
|
|
439
|
-
* @param {string} options.output - Output directory for generated models
|
|
440
|
-
* @param {string} options.model - Optional: specific model to generate
|
|
441
|
-
* @param {string} options.only - Optional: specific component to generate
|
|
442
|
-
*/
|
|
443
|
-
async function buildModels(options) {
|
|
444
|
-
const schemaPath = path.resolve(process.cwd(), options.schema);
|
|
445
|
-
const outputBase = path.resolve(process.cwd(), options.output);
|
|
446
|
-
|
|
447
|
-
// If output is "/", use process.cwd() as the base
|
|
448
|
-
const baseDir = options.output === '/' ? process.cwd() : outputBase;
|
|
449
|
-
|
|
450
|
-
// Construct paths
|
|
451
|
-
const srcDir = path.join(baseDir, 'src');
|
|
452
|
-
const modelDir = path.join(srcDir, 'Model');
|
|
453
|
-
const modelJsPath = path.join(srcDir, 'Model.js');
|
|
454
|
-
const rapiddDir = path.join(baseDir, 'rapidd');
|
|
455
|
-
const relationshipsPath = path.join(rapiddDir, 'relationships.json');
|
|
456
|
-
const aclPath = path.join(rapiddDir, 'acl.js');
|
|
457
|
-
const rapiddJsPath = path.join(rapiddDir, 'rapidd.js');
|
|
458
|
-
const routesDir = path.join(baseDir, 'routes', 'api', 'v1');
|
|
459
|
-
const logsDir = path.join(baseDir, 'logs');
|
|
460
|
-
|
|
461
|
-
console.log('Building Rapidd models...');
|
|
462
|
-
console.log(`Schema: ${schemaPath}`);
|
|
463
|
-
console.log(`Output: ${baseDir}`);
|
|
464
|
-
|
|
465
|
-
// Create logs directory
|
|
466
|
-
if (!fs.existsSync(logsDir)) {
|
|
467
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Check if schema file exists
|
|
471
|
-
if (!fs.existsSync(schemaPath)) {
|
|
472
|
-
throw new Error(`Prisma schema file not found at: ${schemaPath}`);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Run npx prisma generate first
|
|
476
|
-
console.log('\nRunning npx prisma generate...');
|
|
477
|
-
const { execSync } = require('child_process');
|
|
478
|
-
try {
|
|
479
|
-
execSync(`npx prisma generate --schema=${schemaPath}`, {
|
|
480
|
-
stdio: 'inherit',
|
|
481
|
-
cwd: process.cwd()
|
|
482
|
-
});
|
|
483
|
-
console.log('✓ Prisma client generated successfully\n');
|
|
484
|
-
} catch (error) {
|
|
485
|
-
console.warn('⚠ Warning: Failed to generate Prisma client');
|
|
486
|
-
console.warn('Continuing with schema parsing fallback...\n');
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Try to use Prisma DMMF first (using @prisma/internals getDMMF)
|
|
490
|
-
let parsedData = null;
|
|
491
|
-
let usedDMMF = false;
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
parsedData = await parsePrismaDMMF(schemaPath);
|
|
495
|
-
if (parsedData) {
|
|
496
|
-
console.log('Using Prisma DMMF (via @prisma/internals)');
|
|
497
|
-
usedDMMF = true;
|
|
498
|
-
}
|
|
499
|
-
} catch (error) {
|
|
500
|
-
// Fall back to schema parsing
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// If DMMF parsing failed, parse schema file directly
|
|
504
|
-
if (!parsedData) {
|
|
505
|
-
console.log('Parsing Prisma schema file...');
|
|
506
|
-
parsedData = parsePrismaSchema(schemaPath);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const { models, enums } = parsedData;
|
|
510
|
-
|
|
511
|
-
// Filter models if --model option is provided
|
|
512
|
-
let filteredModels = models;
|
|
513
|
-
if (options.model) {
|
|
514
|
-
const modelName = options.model.toLowerCase();
|
|
515
|
-
const matchedModel = Object.keys(models).find(m => m.toLowerCase() === modelName);
|
|
516
|
-
|
|
517
|
-
if (!matchedModel) {
|
|
518
|
-
throw new Error(`Model "${options.model}" not found in schema. Available models: ${Object.keys(models).join(', ')}`);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
filteredModels = { [matchedModel]: models[matchedModel] };
|
|
522
|
-
console.log(`Filtering to model: ${matchedModel}`);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
console.log(`Found ${Object.keys(models).length} models${options.model ? ` (generating ${Object.keys(filteredModels).length})` : ''}`);
|
|
526
|
-
|
|
527
|
-
// Determine which components to generate
|
|
528
|
-
const shouldGenerate = {
|
|
529
|
-
model: !options.only || options.only === 'model',
|
|
530
|
-
route: !options.only || options.only === 'route',
|
|
531
|
-
acl: !options.only || options.only === 'acl',
|
|
532
|
-
relationship: !options.only || options.only === 'relationship'
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
// Validate --only option
|
|
536
|
-
if (options.only && !['model', 'route', 'acl', 'relationship'].includes(options.only)) {
|
|
537
|
-
throw new Error(`Invalid --only value "${options.only}". Must be one of: model, route, acl, relationship`);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Generate model files
|
|
541
|
-
if (shouldGenerate.model) {
|
|
542
|
-
generateAllModels(filteredModels, modelDir, modelJsPath);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Parse datasource to determine database type
|
|
546
|
-
let datasource = { isPostgreSQL: true, url: null }; // Default to PostgreSQL
|
|
547
|
-
try {
|
|
548
|
-
datasource = parseDatasource(schemaPath);
|
|
549
|
-
} catch (error) {
|
|
550
|
-
// Only warn if it's not the expected "No url found" error in Prisma 7
|
|
551
|
-
if (!error.message.includes('No url found')) {
|
|
552
|
-
console.warn('Could not parse datasource, assuming PostgreSQL:', error.message);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Generate rapidd/rapidd.js if it doesn't exist
|
|
557
|
-
if (!fs.existsSync(rapiddJsPath)) {
|
|
558
|
-
console.log('Generating rapidd/rapidd.js...');
|
|
559
|
-
generateRapiddFile(rapiddJsPath, datasource.isPostgreSQL);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Generate relationships.json
|
|
563
|
-
if (shouldGenerate.relationship) {
|
|
564
|
-
console.log(`\nGenerating relationships.json...`);
|
|
565
|
-
|
|
566
|
-
try {
|
|
567
|
-
if (options.model) {
|
|
568
|
-
// Update only specific model in relationships.json
|
|
569
|
-
await updateRelationshipsForModel(filteredModels, relationshipsPath, schemaPath, usedDMMF);
|
|
570
|
-
} else {
|
|
571
|
-
// Generate all relationships
|
|
572
|
-
if (usedDMMF) {
|
|
573
|
-
await generateRelationshipsFromDMMF(schemaPath, relationshipsPath);
|
|
574
|
-
} else {
|
|
575
|
-
generateRelationshipsFromSchema(schemaPath, relationshipsPath);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
console.log(`✓ Relationships file generated at: ${relationshipsPath}`);
|
|
579
|
-
} catch (error) {
|
|
580
|
-
console.error('Failed to generate relationships.json:', error.message);
|
|
581
|
-
console.log('Note: You may need to create relationships.json manually.');
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Generate ACL configuration
|
|
586
|
-
if (shouldGenerate.acl) {
|
|
587
|
-
console.log(`\nGenerating ACL configuration...`);
|
|
588
|
-
|
|
589
|
-
// Load relationships for Prisma filter building
|
|
590
|
-
let relationships = {};
|
|
591
|
-
try {
|
|
592
|
-
if (fs.existsSync(relationshipsPath)) {
|
|
593
|
-
relationships = JSON.parse(fs.readFileSync(relationshipsPath, 'utf8'));
|
|
594
|
-
}
|
|
595
|
-
} catch (error) {
|
|
596
|
-
console.warn('Could not load relationships.json:', error.message);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
try {
|
|
600
|
-
|
|
601
|
-
// For non-PostgreSQL databases (MySQL, SQLite, etc.), generate permissive ACL
|
|
602
|
-
if (!datasource.isPostgreSQL) {
|
|
603
|
-
console.log(`${datasource.provider || 'Non-PostgreSQL'} database detected - generating permissive ACL...`);
|
|
604
|
-
await generateACL(models, aclPath, null, false, options.userTable, relationships, options.debug);
|
|
605
|
-
} else if (options.model) {
|
|
606
|
-
// Update only specific model in acl.js
|
|
607
|
-
await updateACLForModel(filteredModels, models, aclPath, datasource, options.userTable, relationships, options.debug);
|
|
608
|
-
} else {
|
|
609
|
-
// Generate ACL for all models
|
|
610
|
-
await generateACL(
|
|
611
|
-
models,
|
|
612
|
-
aclPath,
|
|
613
|
-
datasource.url,
|
|
614
|
-
datasource.isPostgreSQL,
|
|
615
|
-
options.userTable,
|
|
616
|
-
relationships,
|
|
617
|
-
options.debug
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
} catch (error) {
|
|
621
|
-
console.error('Failed to generate ACL:', error.message);
|
|
622
|
-
console.log('Generating permissive ACL fallback...');
|
|
623
|
-
// Pass null for URL and false for isPostgreSQL to skip database connection
|
|
624
|
-
await generateACL(models, aclPath, null, false, options.userTable, relationships, options.debug);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Generate routes
|
|
629
|
-
if (shouldGenerate.route) {
|
|
630
|
-
generateAllRoutes(filteredModels, routesDir);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return { models, enums };
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
module.exports = {
|
|
637
|
-
buildModels
|
|
638
|
-
};
|