@rapidd/build 1.2.3 → 2.0.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.
Files changed (71) hide show
  1. package/README.md +219 -68
  2. package/dist/bin/cli.d.ts +3 -0
  3. package/dist/bin/cli.d.ts.map +1 -0
  4. package/dist/bin/cli.js +31 -0
  5. package/dist/bin/cli.js.map +1 -0
  6. package/dist/index.d.ts +18 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +32 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/src/commands/build.d.ts +17 -0
  11. package/dist/src/commands/build.d.ts.map +1 -0
  12. package/dist/src/commands/build.js +236 -0
  13. package/dist/src/commands/build.js.map +1 -0
  14. package/dist/src/generators/aclGenerator.d.ts +6 -0
  15. package/dist/src/generators/aclGenerator.d.ts.map +1 -0
  16. package/dist/src/generators/aclGenerator.js +384 -0
  17. package/dist/src/generators/aclGenerator.js.map +1 -0
  18. package/dist/src/generators/index.d.ts +4 -0
  19. package/dist/src/generators/index.d.ts.map +1 -0
  20. package/dist/src/generators/index.js +13 -0
  21. package/dist/src/generators/index.js.map +1 -0
  22. package/dist/src/generators/modelGenerator.d.ts +10 -0
  23. package/dist/src/generators/modelGenerator.d.ts.map +1 -0
  24. package/dist/src/generators/modelGenerator.js +143 -0
  25. package/dist/src/generators/modelGenerator.js.map +1 -0
  26. package/dist/src/generators/routeGenerator.d.ts +10 -0
  27. package/dist/src/generators/routeGenerator.d.ts.map +1 -0
  28. package/dist/src/generators/routeGenerator.js +148 -0
  29. package/dist/src/generators/routeGenerator.js.map +1 -0
  30. package/dist/src/parsers/datasourceParser.d.ts +11 -0
  31. package/dist/src/parsers/datasourceParser.d.ts.map +1 -0
  32. package/dist/src/parsers/datasourceParser.js +131 -0
  33. package/dist/src/parsers/datasourceParser.js.map +1 -0
  34. package/dist/src/parsers/deepSQLAnalyzer.d.ts +85 -0
  35. package/dist/src/parsers/deepSQLAnalyzer.d.ts.map +1 -0
  36. package/dist/src/parsers/deepSQLAnalyzer.js +482 -0
  37. package/dist/src/parsers/deepSQLAnalyzer.js.map +1 -0
  38. package/dist/src/parsers/enhancedRLSConverter.d.ts +14 -0
  39. package/dist/src/parsers/enhancedRLSConverter.d.ts.map +1 -0
  40. package/dist/src/parsers/enhancedRLSConverter.js +168 -0
  41. package/dist/src/parsers/enhancedRLSConverter.js.map +1 -0
  42. package/dist/src/parsers/functionAnalyzer.d.ts +55 -0
  43. package/dist/src/parsers/functionAnalyzer.d.ts.map +1 -0
  44. package/dist/src/parsers/functionAnalyzer.js +274 -0
  45. package/dist/src/parsers/functionAnalyzer.js.map +1 -0
  46. package/dist/src/parsers/index.d.ts +13 -0
  47. package/dist/src/parsers/index.d.ts.map +1 -0
  48. package/dist/src/parsers/index.js +20 -0
  49. package/dist/src/parsers/index.js.map +1 -0
  50. package/dist/src/parsers/prismaFilterBuilder.d.ts +79 -0
  51. package/dist/src/parsers/prismaFilterBuilder.d.ts.map +1 -0
  52. package/dist/src/parsers/prismaFilterBuilder.js +322 -0
  53. package/dist/src/parsers/prismaFilterBuilder.js.map +1 -0
  54. package/dist/src/parsers/prismaParser.d.ts +14 -0
  55. package/dist/src/parsers/prismaParser.d.ts.map +1 -0
  56. package/dist/src/parsers/prismaParser.js +263 -0
  57. package/dist/src/parsers/prismaParser.js.map +1 -0
  58. package/package.json +21 -13
  59. package/bin/cli.js +0 -33
  60. package/index.js +0 -11
  61. package/src/commands/build.js +0 -638
  62. package/src/generators/aclGenerator.js +0 -394
  63. package/src/generators/modelGenerator.js +0 -174
  64. package/src/generators/relationshipsGenerator.js +0 -200
  65. package/src/generators/routeGenerator.js +0 -119
  66. package/src/parsers/datasourceParser.js +0 -121
  67. package/src/parsers/deepSQLAnalyzer.js +0 -554
  68. package/src/parsers/enhancedRLSConverter.js +0 -181
  69. package/src/parsers/functionAnalyzer.js +0 -302
  70. package/src/parsers/prismaFilterBuilder.js +0 -422
  71. package/src/parsers/prismaParser.js +0 -287
@@ -1,422 +0,0 @@
1
- /**
2
- * Prisma Filter Builder for RLS
3
- * Generates correct Prisma filter syntax based on schema relationships
4
- */
5
-
6
- class PrismaFilterBuilder {
7
- constructor(models, relationships) {
8
- this.models = models;
9
- this.relationships = relationships || {};
10
-
11
- // Find the user model
12
- this.userModel = null;
13
- this.userModelName = null;
14
- for (const [modelName, modelInfo] of Object.entries(models)) {
15
- if (modelName.toLowerCase() === 'user' || modelName.toLowerCase() === 'users') {
16
- this.userModel = modelInfo;
17
- this.userModelName = modelName;
18
- break;
19
- }
20
- }
21
- }
22
-
23
- /**
24
- * Build a Prisma filter from SQL RLS analysis
25
- * @param {string} modelName - The model to build filter for
26
- * @param {Object} analysis - SQL analysis from DeepSQLAnalyzer
27
- * @param {string} userVar - User variable name (default: 'user')
28
- * @returns {string} - Prisma filter code
29
- */
30
- buildFilter(modelName, analysis, userVar = 'user') {
31
- if (!analysis.filters || analysis.filters.length === 0) {
32
- return '{}';
33
- }
34
-
35
- const filters = [];
36
- const modelInfo = this.models[modelName];
37
-
38
- for (const filter of analysis.filters) {
39
- if (filter.type.startsWith('user_') || filter.type.startsWith('session_')) {
40
- // This is a user field comparison
41
- const userField = filter.userField || filter.type.replace(/^(user_|session_)/, '');
42
-
43
- // Skip role filters - they're runtime checks, not data filters
44
- if (filter.type === 'user_role' || userField === 'role') {
45
- continue;
46
- }
47
-
48
- const prismaFilter = this.buildUserFieldFilter(modelName, filter.field, userField, userVar);
49
- if (prismaFilter) {
50
- filters.push(prismaFilter);
51
- }
52
- } else {
53
- // Direct field comparison
54
- switch (filter.type) {
55
- case 'equal':
56
- const value = isNaN(filter.value) && filter.value !== 'true' && filter.value !== 'false'
57
- ? `'${filter.value}'`
58
- : filter.value;
59
- filters.push(`{ ${filter.field}: ${value} }`);
60
- break;
61
- case 'not_equal':
62
- filters.push(`{ ${filter.field}: { not: '${filter.value}' } }`);
63
- break;
64
- case 'is_null':
65
- filters.push(`{ ${filter.field}: null }`);
66
- break;
67
- case 'not_null':
68
- filters.push(`{ ${filter.field}: { not: null } }`);
69
- break;
70
- case 'in':
71
- filters.push(`{ ${filter.field}: { in: [${filter.values.join(', ')}] } }`);
72
- break;
73
- }
74
- }
75
- }
76
-
77
- // Check if there are role conditions
78
- const hasRoleConditions = analysis.conditions && analysis.conditions.some(c =>
79
- c.type === 'role_any' || c.type === 'role_equal'
80
- );
81
-
82
- // If we have role conditions with OR logic, generate conditional filter
83
- const hasOrLogic = analysis.sql && analysis.sql.includes(' OR ');
84
- if (hasRoleConditions && hasOrLogic && filters.length > 0) {
85
- // Generate: if (roleCheck) return {}; else return filter;
86
- const roleConditions = analysis.conditions.filter(c => c.type === 'role_any' || c.type === 'role_equal');
87
- const roleChecks = roleConditions.map(c => {
88
- if (c.type === 'role_any') {
89
- return `[${c.roles.map(r => `'${r}'`).join(', ')}].includes(${userVar}?.role)`;
90
- } else if (c.type === 'role_equal') {
91
- return `${userVar}?.role === '${c.role}'`;
92
- }
93
- }).filter(Boolean);
94
-
95
- const roleCheck = roleChecks.length > 1 ? `(${roleChecks.join(' || ')})` : roleChecks[0];
96
- const dataFilter = filters.length === 1 ? filters[0] : `{ AND: [${filters.join(', ')}] }`;
97
-
98
- return `if (${roleCheck}) { return {}; } return ${dataFilter};`;
99
- }
100
-
101
- if (filters.length === 0) {
102
- return '{}';
103
- }
104
-
105
- // Deduplicate filters
106
- const uniqueFilters = [...new Set(filters)];
107
-
108
- if (uniqueFilters.length === 1) {
109
- return uniqueFilters[0];
110
- }
111
-
112
- // Check if we need OR or AND
113
- if (hasOrLogic) {
114
- return `{ OR: [${uniqueFilters.join(', ')}] }`;
115
- }
116
-
117
- return `{ AND: [${uniqueFilters.join(', ')}] }`;
118
- }
119
-
120
- /**
121
- * Build filter for user field comparison (handles relationships)
122
- * @param {string} modelName - Current model name
123
- * @param {string} fieldName - Field being checked
124
- * @param {string} userField - User field to compare against
125
- * @param {string} userVar - User variable
126
- * @returns {string|null} - Prisma filter or null
127
- */
128
- buildUserFieldFilter(modelName, fieldName, userField, userVar) {
129
- const modelInfo = this.models[modelName];
130
- if (!modelInfo) return null;
131
-
132
- // Check if this field exists directly in the model
133
- const field = modelInfo.fields[fieldName];
134
-
135
- if (field && !field.isRelation) {
136
- // Direct field comparison
137
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
138
- return `{ ${fieldName}: ${userFieldPath} }`;
139
- }
140
-
141
- // Check if this is a relationship check
142
- // For example: checking if course has a student with student_id = user.student?.id
143
- const modelRelations = this.relationships[modelName] || {};
144
-
145
- // First pass: Look for junction table relationships (prioritize these)
146
- for (const [relationName, relationInfo] of Object.entries(modelRelations)) {
147
- const relatedModel = this.models[relationInfo.object];
148
- if (!relatedModel) continue;
149
-
150
- // Prioritize many-to-many through junction table
151
- // e.g., course -> students (course_student) where student_id = user.student?.id
152
- if (relationInfo.fields && relationInfo.fields.length > 1) {
153
- // This is a junction table relation
154
- // Check if one of the junction fields matches what we're looking for
155
- if (relationInfo.fields.includes(fieldName)) {
156
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
157
- return this.buildRelationFilter(
158
- relationName,
159
- relationInfo,
160
- fieldName,
161
- userFieldPath
162
- );
163
- }
164
- }
165
- }
166
-
167
- // Second pass: Look for regular relationships
168
- for (const [relationName, relationInfo] of Object.entries(modelRelations)) {
169
- const relatedModel = this.models[relationInfo.object];
170
- if (!relatedModel) continue;
171
-
172
- // Check if the related model has this field
173
- if (relatedModel.fields[fieldName] && !relatedModel.fields[fieldName].isRelation) {
174
- // Found it! Build a relation filter
175
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
176
- return this.buildRelationFilter(
177
- relationName,
178
- relationInfo,
179
- fieldName,
180
- userFieldPath
181
- );
182
- }
183
- }
184
-
185
- // Fallback: direct comparison
186
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
187
- return `{ ${fieldName}: ${userFieldPath} }`;
188
- }
189
-
190
- /**
191
- * Convert user field to proper path
192
- * Checks if user model actually has the field before converting to relation path
193
- * Examples:
194
- * 'id' -> 'user?.id'
195
- * 'student_id' (if exists on user) -> 'user?.student_id'
196
- * 'student_id' (if NOT exists on user) -> 'user.student?.id'
197
- * @param {string} userField - Field name like 'id', 'student_id', 'teacher_id'
198
- * @param {string} userVar - User variable name
199
- * @returns {string} - Proper user field path
200
- */
201
- convertToUserFieldPath(userField, userVar) {
202
- // Special case for 'id'
203
- if (userField === 'id') {
204
- return `${userVar}?.id`;
205
- }
206
-
207
- // Check if user model actually has this field
208
- if (this.userModel && this.userModel.fields) {
209
- const userHasField = this.userModel.fields[userField];
210
-
211
- if (userHasField && !userHasField.isRelation) {
212
- // User has this field directly - use it
213
- return `${userVar}?.${userField}`;
214
- }
215
- }
216
-
217
- // User doesn't have this field - check if it's a relation pattern
218
- if (userField.endsWith('_id')) {
219
- // Extract the relation name (e.g., 'student_id' -> 'student')
220
- const relationName = userField.slice(0, -3);
221
-
222
- // Check if user has this relation
223
- if (this.userModel && this.userModel.fields) {
224
- const userHasRelation = this.userModel.fields[relationName];
225
- if (userHasRelation && userHasRelation.isRelation) {
226
- // User has this relation - use relation path
227
- return `${userVar}.${relationName}?.id`;
228
- }
229
- }
230
-
231
- // Assume it's a relation even if we can't verify
232
- return `${userVar}.${relationName}?.id`;
233
- }
234
-
235
- // Default: direct field access
236
- return `${userVar}?.${userField}`;
237
- }
238
-
239
- /**
240
- * Build Prisma relation filter
241
- * @param {string} relationName - Name of the relation
242
- * @param {Object} relationInfo - Relation metadata
243
- * @param {string} fieldName - Field to filter on
244
- * @param {string} value - Value to compare
245
- * @returns {string} - Prisma filter
246
- */
247
- buildRelationFilter(relationName, relationInfo, fieldName, value) {
248
- // Determine if this is a one-to-many or many-to-many
249
- const relatedModel = this.models[relationInfo.object];
250
-
251
- if (!relatedModel) {
252
- return `{ ${relationName}: { some: { ${fieldName}: ${value} } } }`;
253
- }
254
-
255
- // Check if related model is a junction table (has composite key)
256
- const isJunctionTable = relationInfo.fields && relationInfo.fields.length > 1;
257
-
258
- if (isJunctionTable) {
259
- // Many-to-many through junction
260
- // e.g., { students: { some: { student_id: user?.student_id } } }
261
- return `{ ${relationName}: { some: { ${fieldName}: ${value} } } }`;
262
- }
263
-
264
- // One-to-many or one-to-one
265
- // Check if relationName is plural (array) -> use 'some'
266
- if (relationName.endsWith('s') || relationInfo.fields) {
267
- return `{ ${relationName}: { some: { ${fieldName}: ${value} } } }`;
268
- }
269
-
270
- // Singular relation (one-to-one or many-to-one)
271
- return `{ ${relationName}: { ${fieldName}: ${value} } }`;
272
- }
273
-
274
- /**
275
- * Infer relation type from model data
276
- * @param {Object} modelInfo - Model information
277
- * @param {string} relationName - Relation name
278
- * @returns {string} - 'one' | 'many'
279
- */
280
- inferRelationType(modelInfo, relationName) {
281
- if (!modelInfo || !modelInfo.relations) return 'one';
282
-
283
- const relation = modelInfo.relations.find(r => r.name === relationName);
284
- return relation && relation.isArray ? 'many' : 'one';
285
- }
286
-
287
- /**
288
- * Build JavaScript equivalent of Prisma filter (for hasAccess)
289
- * @param {string} modelName - The model name
290
- * @param {Object} analysis - SQL analysis from DeepSQLAnalyzer
291
- * @param {string} dataVar - Data variable name
292
- * @param {string} userVar - User variable name
293
- * @returns {string} - JavaScript condition
294
- */
295
- buildJavaScriptCondition(modelName, analysis, dataVar = 'data', userVar = 'user') {
296
- const conditions = [];
297
- const modelInfo = this.models[modelName];
298
-
299
- for (const filter of analysis.filters) {
300
- if (filter.type.startsWith('user_') || filter.type.startsWith('session_')) {
301
- const userField = filter.userField || filter.type.replace(/^(user_|session_)/, '');
302
-
303
- // Skip role filters - they're handled in conditions section
304
- if (filter.type === 'user_role' || userField === 'role') {
305
- continue;
306
- }
307
-
308
- const jsCondition = this.buildUserFieldJavaScript(modelName, filter.field, userField, dataVar, userVar);
309
- if (jsCondition) {
310
- conditions.push(jsCondition);
311
- }
312
- } else {
313
- // Direct field comparison
314
- switch (filter.type) {
315
- case 'equal':
316
- const value = isNaN(filter.value) && filter.value !== 'true' && filter.value !== 'false'
317
- ? `'${filter.value}'`
318
- : filter.value;
319
- conditions.push(`${dataVar}?.${filter.field} === ${value}`);
320
- break;
321
- }
322
- }
323
- }
324
-
325
- // Add condition-based checks (roles, etc.)
326
- if (analysis.conditions && analysis.conditions.length > 0) {
327
- for (const condition of analysis.conditions) {
328
- if (condition.javascript) {
329
- // Replace user placeholder with actual variable
330
- const jsCondition = condition.javascript.replace(/user/g, userVar);
331
- conditions.push(jsCondition);
332
- } else if (condition.type === 'role_any') {
333
- conditions.push(`[${condition.roles.map(r => `'${r}'`).join(', ')}].includes(${userVar}?.role)`);
334
- } else if (condition.type === 'role_equal') {
335
- conditions.push(`${userVar}?.role === '${condition.role}'`);
336
- }
337
- }
338
- }
339
-
340
- if (conditions.length === 0) return 'true';
341
-
342
- // Deduplicate conditions
343
- const uniqueConditions = [...new Set(conditions)];
344
-
345
- if (uniqueConditions.length === 1) return uniqueConditions[0];
346
-
347
- // Check if we need OR or AND
348
- const hasOrLogic = analysis.sql && analysis.sql.includes(' OR ');
349
- if (hasOrLogic) {
350
- return uniqueConditions.join(' || ');
351
- }
352
-
353
- return '(' + uniqueConditions.join(' && ') + ')';
354
- }
355
-
356
- /**
357
- * Build JavaScript condition for user field (handles relations)
358
- * @param {string} modelName - Current model name
359
- * @param {string} fieldName - Field being checked
360
- * @param {string} userField - User field to compare against
361
- * @param {string} dataVar - Data variable name
362
- * @param {string} userVar - User variable name
363
- * @returns {string|null} - JavaScript condition
364
- */
365
- buildUserFieldJavaScript(modelName, fieldName, userField, dataVar, userVar) {
366
- const modelInfo = this.models[modelName];
367
- if (!modelInfo) return null;
368
-
369
- // Check if this field exists directly in the model
370
- const field = modelInfo.fields[fieldName];
371
-
372
- if (field && !field.isRelation) {
373
- // Direct field comparison
374
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
375
- return `${dataVar}?.${fieldName} === ${userFieldPath}`;
376
- }
377
-
378
- // Field doesn't exist directly - check relationships
379
- const modelRelations = this.relationships[modelName] || {};
380
-
381
- // First pass: Look for junction table relationships (prioritize these)
382
- for (const [relationName, relationInfo] of Object.entries(modelRelations)) {
383
- const relatedModel = this.models[relationInfo.object];
384
- if (!relatedModel) continue;
385
-
386
- // Prioritize many-to-many through junction table
387
- if (relationInfo.fields && relationInfo.fields.length > 1) {
388
- if (relationInfo.fields.includes(fieldName)) {
389
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
390
- // Generate: data?.students?.find(s => s.student_id === user.student?.id)
391
- return `${dataVar}?.${relationName}?.find(item => item.${fieldName} === ${userFieldPath})`;
392
- }
393
- }
394
- }
395
-
396
- // Second pass: Look for regular relationships
397
- for (const [relationName, relationInfo] of Object.entries(modelRelations)) {
398
- const relatedModel = this.models[relationInfo.object];
399
- if (!relatedModel) continue;
400
-
401
- // Check if the related model has this field
402
- if (relatedModel.fields[fieldName] && !relatedModel.fields[fieldName].isRelation) {
403
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
404
-
405
- // Check if this is an array relation (1:n or n:m)
406
- if (relationName.endsWith('s') || (relationInfo.fields && relationInfo.fields.length > 1)) {
407
- // Use .find() for array relations
408
- return `${dataVar}?.${relationName}?.find(item => item.${fieldName} === ${userFieldPath})`;
409
- } else {
410
- // Singular relation
411
- return `${dataVar}?.${relationName}?.${fieldName} === ${userFieldPath}`;
412
- }
413
- }
414
- }
415
-
416
- // Fallback: direct comparison
417
- const userFieldPath = this.convertToUserFieldPath(userField, userVar);
418
- return `${dataVar}?.${fieldName} === ${userFieldPath}`;
419
- }
420
- }
421
-
422
- module.exports = PrismaFilterBuilder;
@@ -1,287 +0,0 @@
1
- const fs = require('fs');
2
-
3
- /**
4
- * Extract blocks (model or enum) with proper brace matching
5
- * @param {string} content - Schema content
6
- * @param {string} keyword - 'model' or 'enum'
7
- * @returns {Array} - Array of {name, body} objects
8
- */
9
- function extractBlocks(content, keyword) {
10
- const blocks = [];
11
- const regex = new RegExp(`${keyword}\\s+(\\w+)\\s*{`, 'g');
12
- let match;
13
-
14
- while ((match = regex.exec(content)) !== null) {
15
- const name = match[1];
16
- const startIndex = match.index + match[0].length;
17
- let braceCount = 1;
18
- let endIndex = startIndex;
19
-
20
- // Find matching closing brace
21
- while (braceCount > 0 && endIndex < content.length) {
22
- if (content[endIndex] === '{') braceCount++;
23
- if (content[endIndex] === '}') braceCount--;
24
- endIndex++;
25
- }
26
-
27
- const body = content.substring(startIndex, endIndex - 1);
28
- blocks.push({ name, body });
29
- }
30
-
31
- return blocks;
32
- }
33
-
34
- /**
35
- * Parse Prisma schema file and extract model information
36
- * @param {string} schemaPath - Path to Prisma schema file
37
- * @returns {Object} - Object containing models and their fields
38
- */
39
- function parsePrismaSchema(schemaPath) {
40
- const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
41
- const models = {};
42
-
43
- // Extract models with proper brace matching
44
- const modelBlocks = extractBlocks(schemaContent, 'model');
45
- for (const { name, body } of modelBlocks) {
46
- const fields = parseModelFields(body);
47
- const compositeKeyFields = parseCompositeKey(body);
48
- const dbName = parseMapDirective(body);
49
-
50
- // Mark composite key fields with isId
51
- if (compositeKeyFields) {
52
- for (const fieldName of compositeKeyFields) {
53
- if (fields[fieldName]) {
54
- fields[fieldName].isId = true;
55
- }
56
- }
57
- }
58
-
59
- models[name] = {
60
- name,
61
- fields,
62
- relations: parseModelRelations(body),
63
- compositeKey: compositeKeyFields,
64
- dbName: dbName || name.toLowerCase() // Default to lowercase model name
65
- };
66
- }
67
-
68
- // Extract enums
69
- const enums = {};
70
- const enumBlocks = extractBlocks(schemaContent, 'enum');
71
- for (const { name, body } of enumBlocks) {
72
- enums[name] = parseEnumValues(body);
73
- }
74
-
75
- return { models, enums };
76
- }
77
-
78
- /**
79
- * Parse composite key from @@id directive
80
- * @param {string} modelBody - The content inside model braces
81
- * @returns {Array|null} - Array of field names in composite key, or null
82
- */
83
- function parseCompositeKey(modelBody) {
84
- const lines = modelBody.split('\n').map(line => line.trim());
85
-
86
- for (const line of lines) {
87
- // Match @@id([field1, field2, ...])
88
- const match = line.match(/^@@id\(\[([^\]]+)\]\)/);
89
- if (match) {
90
- const fieldsStr = match[1];
91
- return fieldsStr.split(',').map(f => f.trim());
92
- }
93
- }
94
-
95
- return null;
96
- }
97
-
98
- /**
99
- * Parse @@map directive to get database table name
100
- * @param {string} modelBody - The content inside model braces
101
- * @returns {string|null} - Database table name, or null if no @@map directive
102
- */
103
- function parseMapDirective(modelBody) {
104
- const lines = modelBody.split('\n').map(line => line.trim());
105
-
106
- for (const line of lines) {
107
- // Match @@map("table_name") or @@map('table_name')
108
- const match = line.match(/^@@map\(["']([^"']+)["']\)/);
109
- if (match) {
110
- return match[1];
111
- }
112
- }
113
-
114
- return null;
115
- }
116
-
117
- /**
118
- * Parse model fields from model body
119
- * @param {string} modelBody - The content inside model braces
120
- * @returns {Object} - Field definitions
121
- */
122
- function parseModelFields(modelBody) {
123
- const fields = {};
124
- const lines = modelBody.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('@@'));
125
-
126
- for (const line of lines) {
127
- // Skip relation fields and index definitions
128
- if (line.startsWith('//') || line.startsWith('@@')) continue;
129
-
130
- // Match field definition: fieldName Type modifiers
131
- const fieldMatch = line.match(/^(\w+)\s+(\w+)(\?|\[\])?\s*(.*)?$/);
132
- if (fieldMatch) {
133
- const [, fieldName, fieldType, modifier, attributes] = fieldMatch;
134
-
135
- // Determine if it's a relation field (starts with uppercase)
136
- const isRelation = fieldType[0] === fieldType[0].toUpperCase() &&
137
- !['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Decimal', 'Json', 'Bytes'].includes(fieldType);
138
-
139
- fields[fieldName] = {
140
- type: fieldType,
141
- optional: modifier === '?',
142
- isArray: modifier === '[]',
143
- isRelation: isRelation,
144
- attributes: attributes || ''
145
- };
146
- }
147
- }
148
-
149
- return fields;
150
- }
151
-
152
- /**
153
- * Parse model relations
154
- * @param {string} modelBody - The content inside model braces
155
- * @returns {Array} - Array of relation definitions
156
- */
157
- function parseModelRelations(modelBody) {
158
- const relations = [];
159
- const lines = modelBody.split('\n').map(line => line.trim()).filter(line => line);
160
-
161
- for (const line of lines) {
162
- if (line.startsWith('//') || line.startsWith('@@')) continue;
163
-
164
- const fieldMatch = line.match(/^(\w+)\s+(\w+)(\?|\[\])?\s*(.*)?$/);
165
- if (fieldMatch) {
166
- const [, fieldName, fieldType, modifier, attributes] = fieldMatch;
167
-
168
- // Check if it's a relation (not a Prisma scalar type)
169
- const scalarTypes = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Decimal', 'Json', 'Bytes', 'BigInt'];
170
- const isRelation = !scalarTypes.includes(fieldType);
171
-
172
- if (isRelation) {
173
- relations.push({
174
- name: fieldName,
175
- type: fieldType,
176
- isArray: modifier === '[]',
177
- optional: modifier === '?'
178
- });
179
- }
180
- }
181
- }
182
-
183
- return relations;
184
- }
185
-
186
- /**
187
- * Parse enum values
188
- * @param {string} enumBody - The content inside enum braces
189
- * @returns {Array} - Array of enum values
190
- */
191
- function parseEnumValues(enumBody) {
192
- const values = [];
193
- const lines = enumBody.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('//'));
194
-
195
- for (const line of lines) {
196
- const valueMatch = line.match(/^(\w+)/);
197
- if (valueMatch) {
198
- values.push(valueMatch[1]);
199
- }
200
- }
201
-
202
- return values;
203
- }
204
-
205
- /**
206
- * Use Prisma's DMMF (Data Model Meta Format) to get model information
207
- * This is an alternative approach that uses Prisma's own abstraction
208
- * In Prisma 7, we use getDMMF from @prisma/internals instead of accessing it from the client
209
- * @param {string} schemaPath - Path to Prisma schema file
210
- * @returns {Object} - Models extracted from DMMF
211
- */
212
- async function parsePrismaDMMF(schemaPath) {
213
- try {
214
- // In Prisma 7, DMMF is no longer exposed on the client
215
- // We need to use getDMMF from @prisma/internals instead
216
- const { getDMMF } = require('@prisma/internals');
217
- const fs = require('fs');
218
-
219
- // Read the schema file
220
- const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
221
-
222
- // Get DMMF from the schema
223
- const dmmf = await getDMMF({
224
- datamodel: schemaContent
225
- });
226
-
227
- const models = {};
228
-
229
- for (const model of dmmf.datamodel.models) {
230
- // Extract composite key if present
231
- const compositeKey = model.primaryKey && model.primaryKey.fields && model.primaryKey.fields.length > 1
232
- ? model.primaryKey.fields
233
- : null;
234
-
235
- models[model.name] = {
236
- name: model.name,
237
- fields: {},
238
- relations: [],
239
- compositeKey,
240
- dbName: model.dbName || model.name.toLowerCase() // Use dbName from DMMF or default to lowercase
241
- };
242
-
243
- for (const field of model.fields) {
244
- models[model.name].fields[field.name] = {
245
- type: field.type,
246
- optional: !field.isRequired,
247
- isArray: field.isList,
248
- isRelation: field.kind === 'object',
249
- isId: field.isId || false,
250
- isUnique: field.isUnique || false,
251
- isUpdatedAt: field.isUpdatedAt || false,
252
- hasDefaultValue: field.hasDefaultValue || false
253
- };
254
-
255
- if (field.kind === 'object') {
256
- models[model.name].relations.push({
257
- name: field.name,
258
- type: field.type,
259
- isArray: field.isList,
260
- optional: !field.isRequired,
261
- relationName: field.relationName,
262
- relationFromFields: field.relationFromFields || [],
263
- relationToFields: field.relationToFields || [],
264
- kind: field.kind
265
- });
266
-
267
- // Also add these to the field object for consistency
268
- models[model.name].fields[field.name].relationName = field.relationName;
269
- models[model.name].fields[field.name].relationFromFields = field.relationFromFields || [];
270
- models[model.name].fields[field.name].relationToFields = field.relationToFields || [];
271
- models[model.name].fields[field.name].kind = field.kind;
272
- }
273
- }
274
- }
275
-
276
- return { models, enums: dmmf.datamodel.enums };
277
- } catch (error) {
278
- console.warn('Could not use getDMMF from @prisma/internals, falling back to schema parsing');
279
- console.warn('Error:', error.message);
280
- return null;
281
- }
282
- }
283
-
284
- module.exports = {
285
- parsePrismaSchema,
286
- parsePrismaDMMF
287
- };