@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.
@@ -0,0 +1,422 @@
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;
@@ -0,0 +1,245 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Extract blocks (model or enum) with proper brace matching
6
+ * @param {string} content - Schema content
7
+ * @param {string} keyword - 'model' or 'enum'
8
+ * @returns {Array} - Array of {name, body} objects
9
+ */
10
+ function extractBlocks(content, keyword) {
11
+ const blocks = [];
12
+ const regex = new RegExp(`${keyword}\\s+(\\w+)\\s*{`, 'g');
13
+ let match;
14
+
15
+ while ((match = regex.exec(content)) !== null) {
16
+ const name = match[1];
17
+ const startIndex = match.index + match[0].length;
18
+ let braceCount = 1;
19
+ let endIndex = startIndex;
20
+
21
+ // Find matching closing brace
22
+ while (braceCount > 0 && endIndex < content.length) {
23
+ if (content[endIndex] === '{') braceCount++;
24
+ if (content[endIndex] === '}') braceCount--;
25
+ endIndex++;
26
+ }
27
+
28
+ const body = content.substring(startIndex, endIndex - 1);
29
+ blocks.push({ name, body });
30
+ }
31
+
32
+ return blocks;
33
+ }
34
+
35
+ /**
36
+ * Parse Prisma schema file and extract model information
37
+ * @param {string} schemaPath - Path to Prisma schema file
38
+ * @returns {Object} - Object containing models and their fields
39
+ */
40
+ function parsePrismaSchema(schemaPath) {
41
+ const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
42
+ const models = {};
43
+
44
+ // Extract models with proper brace matching
45
+ const modelBlocks = extractBlocks(schemaContent, 'model');
46
+ for (const { name, body } of modelBlocks) {
47
+ const fields = parseModelFields(body);
48
+ const compositeKeyFields = parseCompositeKey(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
+ };
65
+ }
66
+
67
+ // Extract enums
68
+ const enums = {};
69
+ const enumBlocks = extractBlocks(schemaContent, 'enum');
70
+ for (const { name, body } of enumBlocks) {
71
+ enums[name] = parseEnumValues(body);
72
+ }
73
+
74
+ return { models, enums };
75
+ }
76
+
77
+ /**
78
+ * Parse composite key from @@id directive
79
+ * @param {string} modelBody - The content inside model braces
80
+ * @returns {Array|null} - Array of field names in composite key, or null
81
+ */
82
+ function parseCompositeKey(modelBody) {
83
+ const lines = modelBody.split('\n').map(line => line.trim());
84
+
85
+ for (const line of lines) {
86
+ // Match @@id([field1, field2, ...])
87
+ const match = line.match(/^@@id\(\[([^\]]+)\]\)/);
88
+ if (match) {
89
+ const fieldsStr = match[1];
90
+ return fieldsStr.split(',').map(f => f.trim());
91
+ }
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Parse model fields from model body
99
+ * @param {string} modelBody - The content inside model braces
100
+ * @returns {Object} - Field definitions
101
+ */
102
+ function parseModelFields(modelBody) {
103
+ const fields = {};
104
+ const lines = modelBody.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('@@'));
105
+
106
+ for (const line of lines) {
107
+ // Skip relation fields and index definitions
108
+ if (line.startsWith('//') || line.startsWith('@@')) continue;
109
+
110
+ // Match field definition: fieldName Type modifiers
111
+ const fieldMatch = line.match(/^(\w+)\s+(\w+)(\?|\[\])?\s*(.*)?$/);
112
+ if (fieldMatch) {
113
+ const [, fieldName, fieldType, modifier, attributes] = fieldMatch;
114
+
115
+ // Determine if it's a relation field (starts with uppercase)
116
+ const isRelation = fieldType[0] === fieldType[0].toUpperCase() &&
117
+ !['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Decimal', 'Json', 'Bytes'].includes(fieldType);
118
+
119
+ fields[fieldName] = {
120
+ type: fieldType,
121
+ optional: modifier === '?',
122
+ isArray: modifier === '[]',
123
+ isRelation: isRelation,
124
+ attributes: attributes || ''
125
+ };
126
+ }
127
+ }
128
+
129
+ return fields;
130
+ }
131
+
132
+ /**
133
+ * Parse model relations
134
+ * @param {string} modelBody - The content inside model braces
135
+ * @returns {Array} - Array of relation definitions
136
+ */
137
+ function parseModelRelations(modelBody) {
138
+ const relations = [];
139
+ const lines = modelBody.split('\n').map(line => line.trim()).filter(line => line);
140
+
141
+ for (const line of lines) {
142
+ if (line.startsWith('//') || line.startsWith('@@')) continue;
143
+
144
+ const fieldMatch = line.match(/^(\w+)\s+(\w+)(\?|\[\])?\s*(.*)?$/);
145
+ if (fieldMatch) {
146
+ const [, fieldName, fieldType, modifier, attributes] = fieldMatch;
147
+
148
+ // Check if it's a relation (not a Prisma scalar type)
149
+ const scalarTypes = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Decimal', 'Json', 'Bytes', 'BigInt'];
150
+ const isRelation = !scalarTypes.includes(fieldType);
151
+
152
+ if (isRelation) {
153
+ relations.push({
154
+ name: fieldName,
155
+ type: fieldType,
156
+ isArray: modifier === '[]',
157
+ optional: modifier === '?'
158
+ });
159
+ }
160
+ }
161
+ }
162
+
163
+ return relations;
164
+ }
165
+
166
+ /**
167
+ * Parse enum values
168
+ * @param {string} enumBody - The content inside enum braces
169
+ * @returns {Array} - Array of enum values
170
+ */
171
+ function parseEnumValues(enumBody) {
172
+ const values = [];
173
+ const lines = enumBody.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('//'));
174
+
175
+ for (const line of lines) {
176
+ const valueMatch = line.match(/^(\w+)/);
177
+ if (valueMatch) {
178
+ values.push(valueMatch[1]);
179
+ }
180
+ }
181
+
182
+ return values;
183
+ }
184
+
185
+ /**
186
+ * Use Prisma's generated DMMF (Data Model Meta Format) to get model information
187
+ * This is an alternative approach that uses Prisma's own abstraction
188
+ * @param {string} prismaClientPath - Path to generated Prisma Client
189
+ * @returns {Object} - Models extracted from DMMF
190
+ */
191
+ async function parsePrismaDMMF(prismaClientPath) {
192
+ try {
193
+ // Try to load the generated Prisma Client
194
+ const prismaClient = require(prismaClientPath);
195
+ const dmmf = prismaClient.Prisma.dmmf;
196
+
197
+ const models = {};
198
+
199
+ for (const model of dmmf.datamodel.models) {
200
+ // Extract composite key if present
201
+ const compositeKey = model.primaryKey && model.primaryKey.fields && model.primaryKey.fields.length > 1
202
+ ? model.primaryKey.fields
203
+ : null;
204
+
205
+ models[model.name] = {
206
+ name: model.name,
207
+ fields: {},
208
+ relations: [],
209
+ compositeKey
210
+ };
211
+
212
+ for (const field of model.fields) {
213
+ models[model.name].fields[field.name] = {
214
+ type: field.type,
215
+ optional: !field.isRequired,
216
+ isArray: field.isList,
217
+ isRelation: field.kind === 'object',
218
+ isId: field.isId || false,
219
+ isUnique: field.isUnique || false,
220
+ isUpdatedAt: field.isUpdatedAt || false,
221
+ hasDefaultValue: field.hasDefaultValue || false
222
+ };
223
+
224
+ if (field.kind === 'object') {
225
+ models[model.name].relations.push({
226
+ name: field.name,
227
+ type: field.type,
228
+ isArray: field.isList,
229
+ optional: !field.isRequired
230
+ });
231
+ }
232
+ }
233
+ }
234
+
235
+ return { models, enums: dmmf.datamodel.enums };
236
+ } catch (error) {
237
+ console.warn('Could not load Prisma Client DMMF, falling back to schema parsing');
238
+ return null;
239
+ }
240
+ }
241
+
242
+ module.exports = {
243
+ parsePrismaSchema,
244
+ parsePrismaDMMF
245
+ };