@@ -8,135 +8,159 @@ export interface PrismaParseResult {
88 setNullCount : number ;
99}
1010
11- export function parsePrismaSchema ( schemaPath : string ) : PrismaParseResult {
12- const content = readFileSync ( schemaPath , 'utf-8' ) ;
13- const models : SchemaModel [ ] = [ ] ;
14- const enums : SchemaEnum [ ] = [ ] ;
15-
16- // Parse enums
17- const enumRegex = / e n u m \s + ( \w + ) \s * \{ ( [ ^ } ] + ) \} / g;
18- let enumMatch ;
19- while ( ( enumMatch = enumRegex . exec ( content ) ) !== null ) {
20- const name = enumMatch [ 1 ] ;
21- const body = enumMatch [ 2 ] ;
22- const values = body
23- . split ( '\n' )
24- . map ( ( line ) => line . trim ( ) )
25- . filter ( ( line ) => line && ! line . startsWith ( '//' ) ) ;
26- enums . push ( { name, values } ) ;
11+ const PRISMA_SCALAR_TYPES = new Set ( [
12+ 'String' , 'Int' , 'Float' , 'Boolean' , 'DateTime' , 'Json' , 'BigInt' , 'Decimal' , 'Bytes' ,
13+ ] ) ;
14+
15+ /**
16+ * Extract brace-balanced blocks for a given keyword (model, enum, etc.)
17+ * Handles nested braces correctly — unlike [^}]+ regex which fails on @default({})
18+ */
19+ function extractBlocks ( content : string , keyword : string ) : { name : string ; body : string } [ ] {
20+ const blocks : { name : string ; body : string } [ ] = [ ] ;
21+ const headerRegex = new RegExp ( `${ keyword } \\s+(\\w+)\\s*\\{` , 'g' ) ;
22+ let match ;
23+ while ( ( match = headerRegex . exec ( content ) ) !== null ) {
24+ let depth = 1 ;
25+ let i = match . index + match [ 0 ] . length ;
26+ while ( i < content . length && depth > 0 ) {
27+ if ( content [ i ] === '{' ) depth ++ ;
28+ else if ( content [ i ] === '}' ) depth -- ;
29+ i ++ ;
30+ }
31+ if ( depth === 0 ) {
32+ blocks . push ( { name : match [ 1 ] , body : content . substring ( match . index + match [ 0 ] . length , i - 1 ) } ) ;
33+ }
2734 }
35+ return blocks ;
36+ }
2837
29- // Parse models
30- const modelRegex = / m o d e l \s + ( \w + ) \s * \{ ( [ ^ } ] + ) \} / g;
31- let modelMatch ;
32- while ( ( modelMatch = modelRegex . exec ( content ) ) !== null ) {
33- const name = modelMatch [ 1 ] ;
34- const body = modelMatch [ 2 ] ;
35- const fields : SchemaField [ ] = [ ] ;
36- const relations : SchemaRelation [ ] = [ ] ;
37- const indexes : string [ ] = [ ] ;
38- const uniqueConstraints : string [ ] = [ ] ;
39-
40- const allLines = body . split ( '\n' ) . map ( ( l ) => l . trim ( ) ) . filter ( ( l ) => l && ! l . startsWith ( '//' ) ) ;
41-
42- // Extract @@index and @@unique before filtering out @@ lines
43- for ( const line of allLines ) {
44- const indexMatch = line . match ( / @ @ i n d e x \( \[ ( [ ^ \] ] + ) \] / ) ;
45- if ( indexMatch ) {
46- indexes . push ( indexMatch [ 1 ] . replace ( / " / g, '' ) . trim ( ) ) ;
47- }
48- const uniqueMatch = line . match ( / @ @ u n i q u e \( \[ ( [ ^ \] ] + ) \] / ) ;
49- if ( uniqueMatch ) {
50- uniqueConstraints . push ( uniqueMatch [ 1 ] . replace ( / " / g, '' ) . trim ( ) ) ;
51- }
38+ /**
39+ * Parse a single model block into a SchemaModel
40+ */
41+ function parseModelBlock ( name : string , body : string , enumNames : Set < string > ) : SchemaModel {
42+ const fields : SchemaField [ ] = [ ] ;
43+ const relations : SchemaRelation [ ] = [ ] ;
44+ const indexes : string [ ] = [ ] ;
45+ const uniqueConstraints : string [ ] = [ ] ;
46+
47+ const allLines = body . split ( '\n' ) . map ( ( l ) => l . trim ( ) ) . filter ( ( l ) => l && ! l . startsWith ( '//' ) ) ;
48+
49+ // Extract @@index and @@unique before filtering out @@ lines
50+ for ( const line of allLines ) {
51+ const indexMatch = line . match ( / @ @ i n d e x \( \[ ( [ ^ \] ] + ) \] / ) ;
52+ if ( indexMatch ) {
53+ indexes . push ( indexMatch [ 1 ] . replace ( / " / g, '' ) . trim ( ) ) ;
54+ }
55+ const uniqueMatch = line . match ( / @ @ u n i q u e \( \[ ( [ ^ \] ] + ) \] / ) ;
56+ if ( uniqueMatch ) {
57+ uniqueConstraints . push ( uniqueMatch [ 1 ] . replace ( / " / g, '' ) . trim ( ) ) ;
5258 }
59+ }
5360
54- const lines = allLines . filter ( ( l ) => ! l . startsWith ( '@@' ) ) ;
61+ const lines = allLines . filter ( ( l ) => ! l . startsWith ( '@@' ) ) ;
5562
56- for ( const line of lines ) {
57- const parts = line . split ( / \s + / ) ;
58- if ( parts . length < 2 ) continue ;
63+ for ( const line of lines ) {
64+ const parts = line . split ( / \s + / ) ;
65+ if ( parts . length < 2 ) continue ;
5966
60- const fieldName = parts [ 0 ] ;
61- let fieldType = parts [ 1 ] ;
67+ const fieldName = parts [ 0 ] ;
68+ let fieldType = parts [ 1 ] ;
6269
63- // Skip directives
64- if ( fieldName . startsWith ( '@' ) ) continue ;
70+ // Skip directives
71+ if ( fieldName . startsWith ( '@' ) ) continue ;
6572
66- const constraints : string [ ] = [ ] ;
73+ const constraints : string [ ] = [ ] ;
6774
68- // Check for optional
69- if ( fieldType . endsWith ( '?' ) ) {
70- fieldType = fieldType . slice ( 0 , - 1 ) ;
71- constraints . push ( 'optional' ) ;
72- }
75+ // Check for optional
76+ if ( fieldType . endsWith ( '?' ) ) {
77+ fieldType = fieldType . slice ( 0 , - 1 ) ;
78+ constraints . push ( 'optional' ) ;
79+ }
7380
74- // Check for array
75- if ( fieldType . endsWith ( '[]' ) ) {
76- fieldType = fieldType . slice ( 0 , - 2 ) ;
77- constraints . push ( 'array' ) ;
78- }
81+ // Check for array
82+ if ( fieldType . endsWith ( '[]' ) ) {
83+ fieldType = fieldType . slice ( 0 , - 2 ) ;
84+ constraints . push ( 'array' ) ;
85+ }
7986
80- // Extract decorators
81- const decorators = line . match ( / @ \w + ( \( [ ^ ) ] * \) ) ? / g) ?? [ ] ;
82- for ( const d of decorators ) {
83- if ( d . startsWith ( '@id' ) ) constraints . push ( 'primary key' ) ;
84- if ( d . startsWith ( '@unique' ) ) constraints . push ( 'unique' ) ;
85- if ( d . startsWith ( '@default' ) ) constraints . push ( d ) ;
86- if ( d . startsWith ( '@map' ) ) constraints . push ( d ) ;
87- if ( d . startsWith ( '@updatedAt' ) ) constraints . push ( 'auto-updated' ) ;
88- }
87+ // Extract decorators
88+ const decorators = line . match ( / @ \w + ( \( [ ^ ) ] * \) ) ? / g) ?? [ ] ;
89+ for ( const d of decorators ) {
90+ if ( d . startsWith ( '@id' ) ) constraints . push ( 'primary key' ) ;
91+ if ( d . startsWith ( '@unique' ) ) constraints . push ( 'unique' ) ;
92+ if ( d . startsWith ( '@default' ) ) constraints . push ( d ) ;
93+ if ( d . startsWith ( '@map' ) ) constraints . push ( d ) ;
94+ if ( d . startsWith ( '@updatedAt' ) ) constraints . push ( 'auto-updated' ) ;
95+ }
8996
90- fields . push ( { name : fieldName , type : fieldType , constraints } ) ;
97+ fields . push ( { name : fieldName , type : fieldType , constraints } ) ;
98+
99+ // Detect explicit relations
100+ const relationMatch = line . match ( / @ r e l a t i o n \( ( [ ^ ) ] * ) \) / ) ;
101+ if ( relationMatch ) {
102+ const relBody = relationMatch [ 1 ] ;
103+ const isArray = line . includes ( '[]' ) ;
104+ const onDeleteMatch = relBody . match ( / o n D e l e t e : \s * ( \w + ) / ) ;
105+ const fkFieldsMatch = relBody . match ( / f i e l d s : \s * \[ ( [ ^ \] ] + ) \] / ) ;
106+ const referencesMatch = relBody . match ( / r e f e r e n c e s : \s * \[ ( [ ^ \] ] + ) \] / ) ;
107+
108+ const isManyToMany = isArray && ! fkFieldsMatch ;
109+ relations . push ( {
110+ field : fieldName ,
111+ target : fieldType ,
112+ type : isManyToMany ? 'many-to-many' : isArray ? 'one-to-many' : 'one-to-one' ,
113+ onDelete : onDeleteMatch ?. [ 1 ] ,
114+ fkFields : fkFieldsMatch ?. [ 1 ] . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ,
115+ references : referencesMatch ?. [ 1 ] . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ,
116+ } ) ;
117+ }
91118
92- // Detect relations
93- const relationMatch = line . match ( / @ r e l a t i o n \( ( [ ^ ) ] * ) \) / ) ;
94- if ( relationMatch ) {
95- const relBody = relationMatch [ 1 ] ;
119+ // Detect implicit relations — exclude scalars AND enums
120+ if (
121+ fieldType [ 0 ] === fieldType [ 0 ] . toUpperCase ( ) &&
122+ ! PRISMA_SCALAR_TYPES . has ( fieldType ) &&
123+ ! enumNames . has ( fieldType )
124+ ) {
125+ if ( ! relations . some ( ( r ) => r . field === fieldName ) ) {
96126 const isArray = line . includes ( '[]' ) ;
97- const onDeleteMatch = relBody . match ( / o n D e l e t e : \s * ( \w + ) / ) ;
98- const fkFieldsMatch = relBody . match ( / f i e l d s : \s * \[ ( [ ^ \] ] + ) \] / ) ;
99- const referencesMatch = relBody . match ( / r e f e r e n c e s : \s * \[ ( [ ^ \] ] + ) \] / ) ;
100-
101- // Detect M:N: array field without explicit FK fields (implicit many-to-many)
102- const isManyToMany = isArray && ! fkFieldsMatch ;
127+ const isManyToMany = isArray && ! line . includes ( '@relation' ) ;
103128 relations . push ( {
104129 field : fieldName ,
105130 target : fieldType ,
106131 type : isManyToMany ? 'many-to-many' : isArray ? 'one-to-many' : 'one-to-one' ,
107- onDelete : onDeleteMatch ?. [ 1 ] ,
108- fkFields : fkFieldsMatch ?. [ 1 ] . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ,
109- references : referencesMatch ?. [ 1 ] . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ,
110132 } ) ;
111133 }
112-
113- // Also detect implicit relations (type is another model name)
114- if (
115- fieldType [ 0 ] === fieldType [ 0 ] . toUpperCase ( ) &&
116- ! [ 'String' , 'Int' , 'Float' , 'Boolean' , 'DateTime' , 'Json' , 'BigInt' , 'Decimal' , 'Bytes' ] . includes ( fieldType ) &&
117- ! enums . some ( ( e ) => e . name === fieldType )
118- ) {
119- if ( ! relations . some ( ( r ) => r . field === fieldName ) ) {
120- const isArray = line . includes ( '[]' ) ;
121- const isManyToMany = isArray && ! line . includes ( '@relation' ) ;
122- relations . push ( {
123- field : fieldName ,
124- target : fieldType ,
125- type : isManyToMany ? 'many-to-many' : isArray ? 'one-to-many' : 'one-to-one' ,
126- } ) ;
127- }
128- }
129134 }
130-
131- models . push ( {
132- name,
133- fields,
134- relations,
135- ...( indexes . length > 0 && { indexes } ) ,
136- ...( uniqueConstraints . length > 0 && { uniqueConstraints } ) ,
137- } ) ;
138135 }
139136
137+ return {
138+ name,
139+ fields,
140+ relations,
141+ ...( indexes . length > 0 && { indexes } ) ,
142+ ...( uniqueConstraints . length > 0 && { uniqueConstraints } ) ,
143+ } ;
144+ }
145+
146+ export function parsePrismaSchema ( schemaPath : string ) : PrismaParseResult {
147+ const content = readFileSync ( schemaPath , 'utf-8' ) ;
148+
149+ // First pass: collect ALL enum names (handles forward references)
150+ const enumBlocks = extractBlocks ( content , 'enum' ) ;
151+ const enumNames = new Set ( enumBlocks . map ( ( b ) => b . name ) ) ;
152+ const enums : SchemaEnum [ ] = enumBlocks . map ( ( b ) => ( {
153+ name : b . name ,
154+ values : b . body
155+ . split ( '\n' )
156+ . map ( ( line ) => line . trim ( ) )
157+ . filter ( ( line ) => line && ! line . startsWith ( '//' ) ) ,
158+ } ) ) ;
159+
160+ // Second pass: parse models with enum awareness
161+ const modelBlocks = extractBlocks ( content , 'model' ) ;
162+ const models = modelBlocks . map ( ( b ) => parseModelBlock ( b . name , b . body , enumNames ) ) ;
163+
140164 // Compute cascade/setNull stats
141165 let cascadeCount = 0 ;
142166 let setNullCount = 0 ;
0 commit comments