@opensaas/stack-cli 0.3.0 → 0.5.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.
Files changed (105) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +193 -0
  3. package/dist/commands/generate.d.ts.map +1 -1
  4. package/dist/commands/generate.js +4 -13
  5. package/dist/commands/generate.js.map +1 -1
  6. package/dist/commands/migrate.d.ts +9 -0
  7. package/dist/commands/migrate.d.ts.map +1 -0
  8. package/dist/commands/migrate.js +473 -0
  9. package/dist/commands/migrate.js.map +1 -0
  10. package/dist/generator/context.d.ts.map +1 -1
  11. package/dist/generator/context.js +20 -5
  12. package/dist/generator/context.js.map +1 -1
  13. package/dist/generator/index.d.ts +1 -1
  14. package/dist/generator/index.d.ts.map +1 -1
  15. package/dist/generator/index.js +1 -1
  16. package/dist/generator/index.js.map +1 -1
  17. package/dist/generator/lists.d.ts.map +1 -1
  18. package/dist/generator/lists.js +33 -1
  19. package/dist/generator/lists.js.map +1 -1
  20. package/dist/generator/prisma-extensions.d.ts +11 -0
  21. package/dist/generator/prisma-extensions.d.ts.map +1 -0
  22. package/dist/generator/prisma-extensions.js +134 -0
  23. package/dist/generator/prisma-extensions.js.map +1 -0
  24. package/dist/generator/prisma.d.ts.map +1 -1
  25. package/dist/generator/prisma.js +4 -0
  26. package/dist/generator/prisma.js.map +1 -1
  27. package/dist/generator/types.d.ts.map +1 -1
  28. package/dist/generator/types.js +151 -17
  29. package/dist/generator/types.js.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp/lib/documentation-provider.d.ts +23 -0
  33. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -1
  34. package/dist/mcp/lib/documentation-provider.js +471 -0
  35. package/dist/mcp/lib/documentation-provider.js.map +1 -1
  36. package/dist/mcp/lib/wizards/migration-wizard.d.ts +80 -0
  37. package/dist/mcp/lib/wizards/migration-wizard.d.ts.map +1 -0
  38. package/dist/mcp/lib/wizards/migration-wizard.js +499 -0
  39. package/dist/mcp/lib/wizards/migration-wizard.js.map +1 -0
  40. package/dist/mcp/server/index.d.ts.map +1 -1
  41. package/dist/mcp/server/index.js +103 -0
  42. package/dist/mcp/server/index.js.map +1 -1
  43. package/dist/mcp/server/stack-mcp-server.d.ts +85 -0
  44. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -1
  45. package/dist/mcp/server/stack-mcp-server.js +219 -0
  46. package/dist/mcp/server/stack-mcp-server.js.map +1 -1
  47. package/dist/migration/generators/migration-generator.d.ts +60 -0
  48. package/dist/migration/generators/migration-generator.d.ts.map +1 -0
  49. package/dist/migration/generators/migration-generator.js +510 -0
  50. package/dist/migration/generators/migration-generator.js.map +1 -0
  51. package/dist/migration/introspectors/index.d.ts +12 -0
  52. package/dist/migration/introspectors/index.d.ts.map +1 -0
  53. package/dist/migration/introspectors/index.js +10 -0
  54. package/dist/migration/introspectors/index.js.map +1 -0
  55. package/dist/migration/introspectors/keystone-introspector.d.ts +59 -0
  56. package/dist/migration/introspectors/keystone-introspector.d.ts.map +1 -0
  57. package/dist/migration/introspectors/keystone-introspector.js +229 -0
  58. package/dist/migration/introspectors/keystone-introspector.js.map +1 -0
  59. package/dist/migration/introspectors/nextjs-introspector.d.ts +59 -0
  60. package/dist/migration/introspectors/nextjs-introspector.d.ts.map +1 -0
  61. package/dist/migration/introspectors/nextjs-introspector.js +159 -0
  62. package/dist/migration/introspectors/nextjs-introspector.js.map +1 -0
  63. package/dist/migration/introspectors/prisma-introspector.d.ts +45 -0
  64. package/dist/migration/introspectors/prisma-introspector.d.ts.map +1 -0
  65. package/dist/migration/introspectors/prisma-introspector.js +190 -0
  66. package/dist/migration/introspectors/prisma-introspector.js.map +1 -0
  67. package/dist/migration/types.d.ts +86 -0
  68. package/dist/migration/types.d.ts.map +1 -0
  69. package/dist/migration/types.js +5 -0
  70. package/dist/migration/types.js.map +1 -0
  71. package/package.json +12 -9
  72. package/src/commands/__snapshots__/generate.test.ts.snap +92 -21
  73. package/src/commands/generate.ts +8 -19
  74. package/src/commands/migrate.ts +534 -0
  75. package/src/generator/__snapshots__/context.test.ts.snap +60 -15
  76. package/src/generator/__snapshots__/types.test.ts.snap +689 -95
  77. package/src/generator/context.test.ts +3 -1
  78. package/src/generator/context.ts +20 -5
  79. package/src/generator/index.ts +1 -1
  80. package/src/generator/lists.ts +39 -1
  81. package/src/generator/prisma-extensions.ts +159 -0
  82. package/src/generator/prisma.ts +5 -0
  83. package/src/generator/types.ts +204 -17
  84. package/src/index.ts +4 -0
  85. package/src/mcp/lib/documentation-provider.ts +507 -0
  86. package/src/mcp/lib/wizards/migration-wizard.ts +584 -0
  87. package/src/mcp/server/index.ts +121 -0
  88. package/src/mcp/server/stack-mcp-server.ts +243 -0
  89. package/src/migration/generators/migration-generator.ts +675 -0
  90. package/src/migration/introspectors/index.ts +12 -0
  91. package/src/migration/introspectors/keystone-introspector.ts +296 -0
  92. package/src/migration/introspectors/nextjs-introspector.ts +209 -0
  93. package/src/migration/introspectors/prisma-introspector.ts +233 -0
  94. package/src/migration/types.ts +92 -0
  95. package/tests/introspectors/keystone-introspector.test.ts +255 -0
  96. package/tests/introspectors/nextjs-introspector.test.ts +302 -0
  97. package/tests/introspectors/prisma-introspector.test.ts +268 -0
  98. package/tests/migration-generator.test.ts +592 -0
  99. package/tests/migration-wizard.test.ts +442 -0
  100. package/tsconfig.tsbuildinfo +1 -1
  101. package/dist/generator/type-patcher.d.ts +0 -13
  102. package/dist/generator/type-patcher.d.ts.map +0 -1
  103. package/dist/generator/type-patcher.js +0 -68
  104. package/dist/generator/type-patcher.js.map +0 -1
  105. package/src/generator/type-patcher.ts +0 -93
@@ -0,0 +1,302 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { NextjsIntrospector } from '../../src/migration/introspectors/nextjs-introspector.js'
3
+ import fs from 'fs-extra'
4
+ import path from 'path'
5
+ import os from 'os'
6
+
7
+ describe('NextjsIntrospector', () => {
8
+ let introspector: NextjsIntrospector
9
+ let tempDir: string
10
+
11
+ beforeEach(async () => {
12
+ introspector = new NextjsIntrospector()
13
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'nextjs-test-'))
14
+ })
15
+
16
+ afterEach(async () => {
17
+ await fs.remove(tempDir)
18
+ })
19
+
20
+ it('should detect Next.js version', async () => {
21
+ const pkg = {
22
+ dependencies: {
23
+ next: '^14.0.0',
24
+ },
25
+ }
26
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
27
+
28
+ const result = await introspector.introspect(tempDir)
29
+
30
+ expect(result.version).toBe('14.0.0')
31
+ })
32
+
33
+ it('should detect app router', async () => {
34
+ const pkg = {
35
+ dependencies: {
36
+ next: '14.0.0',
37
+ },
38
+ }
39
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
40
+ await fs.ensureDir(path.join(tempDir, 'app'))
41
+
42
+ const result = await introspector.introspect(tempDir)
43
+
44
+ expect(result.routerType).toBe('app')
45
+ })
46
+
47
+ it('should detect pages router', async () => {
48
+ const pkg = {
49
+ dependencies: {
50
+ next: '14.0.0',
51
+ },
52
+ }
53
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
54
+ await fs.ensureDir(path.join(tempDir, 'pages'))
55
+
56
+ const result = await introspector.introspect(tempDir)
57
+
58
+ expect(result.routerType).toBe('pages')
59
+ })
60
+
61
+ it('should detect both routers', async () => {
62
+ const pkg = {
63
+ dependencies: {
64
+ next: '14.0.0',
65
+ },
66
+ }
67
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
68
+ await fs.ensureDir(path.join(tempDir, 'app'))
69
+ await fs.ensureDir(path.join(tempDir, 'pages'))
70
+
71
+ const result = await introspector.introspect(tempDir)
72
+
73
+ expect(result.routerType).toBe('both')
74
+ })
75
+
76
+ it('should detect TypeScript usage', async () => {
77
+ const pkg = {
78
+ dependencies: {
79
+ next: '14.0.0',
80
+ },
81
+ }
82
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
83
+ await fs.writeJSON(path.join(tempDir, 'tsconfig.json'), {
84
+ compilerOptions: {
85
+ target: 'es2015',
86
+ },
87
+ })
88
+
89
+ const result = await introspector.introspect(tempDir)
90
+
91
+ expect(result.typescript).toBe(true)
92
+ })
93
+
94
+ it('should detect auth libraries', async () => {
95
+ const pkg = {
96
+ dependencies: {
97
+ next: '14.0.0',
98
+ 'next-auth': '^4.0.0',
99
+ },
100
+ }
101
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
102
+
103
+ const result = await introspector.introspect(tempDir)
104
+
105
+ expect(result.authLibrary).toBe('next-auth')
106
+ })
107
+
108
+ it('should detect better-auth', async () => {
109
+ const pkg = {
110
+ dependencies: {
111
+ next: '14.0.0',
112
+ 'better-auth': '^1.0.0',
113
+ },
114
+ }
115
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
116
+
117
+ const result = await introspector.introspect(tempDir)
118
+
119
+ expect(result.authLibrary).toBe('better-auth')
120
+ })
121
+
122
+ it('should detect database libraries', async () => {
123
+ const pkg = {
124
+ dependencies: {
125
+ next: '14.0.0',
126
+ '@prisma/client': '^5.0.0',
127
+ },
128
+ }
129
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
130
+
131
+ const result = await introspector.introspect(tempDir)
132
+
133
+ expect(result.databaseLibrary).toBe('prisma')
134
+ expect(result.hasPrisma).toBe(true)
135
+ })
136
+
137
+ it('should detect Prisma directory', async () => {
138
+ const pkg = {
139
+ dependencies: {
140
+ next: '14.0.0',
141
+ },
142
+ }
143
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
144
+ await fs.ensureDir(path.join(tempDir, 'prisma'))
145
+
146
+ const result = await introspector.introspect(tempDir)
147
+
148
+ expect(result.hasPrisma).toBe(true)
149
+ })
150
+
151
+ it('should detect .env file', async () => {
152
+ const pkg = {
153
+ dependencies: {
154
+ next: '14.0.0',
155
+ },
156
+ }
157
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
158
+ await fs.writeFile(path.join(tempDir, '.env'), 'DATABASE_URL=...')
159
+
160
+ const result = await introspector.introspect(tempDir)
161
+
162
+ expect(result.hasEnvFile).toBe(true)
163
+ })
164
+
165
+ it('should list all dependencies', async () => {
166
+ const pkg = {
167
+ dependencies: {
168
+ next: '14.0.0',
169
+ react: '18.0.0',
170
+ },
171
+ devDependencies: {
172
+ typescript: '5.0.0',
173
+ },
174
+ }
175
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
176
+
177
+ const result = await introspector.introspect(tempDir)
178
+
179
+ expect(result.existingDependencies).toContain('next')
180
+ expect(result.existingDependencies).toContain('react')
181
+ expect(result.existingDependencies).toContain('typescript')
182
+ })
183
+
184
+ it('should generate recommendations for pages router', async () => {
185
+ const pkg = {
186
+ dependencies: {
187
+ next: '14.0.0',
188
+ },
189
+ }
190
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
191
+ await fs.ensureDir(path.join(tempDir, 'pages'))
192
+
193
+ const result = await introspector.introspect(tempDir)
194
+ const recommendations = introspector.getRecommendations(result)
195
+
196
+ expect(recommendations.some((r) => r.includes('App Router'))).toBe(true)
197
+ })
198
+
199
+ it('should recommend Better-auth for non-better-auth projects', async () => {
200
+ const pkg = {
201
+ dependencies: {
202
+ next: '14.0.0',
203
+ 'next-auth': '4.0.0',
204
+ },
205
+ }
206
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
207
+
208
+ const result = await introspector.introspect(tempDir)
209
+ const recommendations = introspector.getRecommendations(result)
210
+
211
+ expect(recommendations.some((r) => r.includes('Better-auth'))).toBe(true)
212
+ })
213
+
214
+ it('should recommend Prisma setup if missing', async () => {
215
+ const pkg = {
216
+ dependencies: {
217
+ next: '14.0.0',
218
+ },
219
+ }
220
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
221
+
222
+ const result = await introspector.introspect(tempDir)
223
+ const recommendations = introspector.getRecommendations(result)
224
+
225
+ expect(recommendations.some((r) => r.includes('Prisma'))).toBe(true)
226
+ })
227
+
228
+ it('should generate warnings for old Next.js versions', async () => {
229
+ const pkg = {
230
+ dependencies: {
231
+ next: '12.0.0',
232
+ },
233
+ }
234
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
235
+
236
+ const result = await introspector.introspect(tempDir)
237
+ const warnings = introspector.getWarnings(result)
238
+
239
+ expect(warnings.some((w) => w.includes('12.0.0'))).toBe(true)
240
+ })
241
+
242
+ it('should warn about MongoDB/Mongoose', async () => {
243
+ const pkg = {
244
+ dependencies: {
245
+ next: '14.0.0',
246
+ mongoose: '7.0.0',
247
+ },
248
+ }
249
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
250
+
251
+ const result = await introspector.introspect(tempDir)
252
+ const warnings = introspector.getWarnings(result)
253
+
254
+ expect(warnings.some((w) => w.includes('MongoDB'))).toBe(true)
255
+ })
256
+
257
+ it('should throw for missing package.json', async () => {
258
+ await expect(introspector.introspect(tempDir)).rejects.toThrow('package.json not found')
259
+ })
260
+
261
+ it('should detect router in src directory', async () => {
262
+ const pkg = {
263
+ dependencies: {
264
+ next: '14.0.0',
265
+ },
266
+ }
267
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
268
+ await fs.ensureDir(path.join(tempDir, 'src', 'app'))
269
+
270
+ const result = await introspector.introspect(tempDir)
271
+
272
+ expect(result.routerType).toBe('app')
273
+ })
274
+
275
+ it('should handle multiple auth libraries', async () => {
276
+ const pkg = {
277
+ dependencies: {
278
+ next: '14.0.0',
279
+ '@clerk/nextjs': '4.0.0',
280
+ },
281
+ }
282
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
283
+
284
+ const result = await introspector.introspect(tempDir)
285
+
286
+ expect(result.authLibrary).toBe('clerk')
287
+ })
288
+
289
+ it('should detect drizzle', async () => {
290
+ const pkg = {
291
+ dependencies: {
292
+ next: '14.0.0',
293
+ 'drizzle-orm': '0.28.0',
294
+ },
295
+ }
296
+ await fs.writeJSON(path.join(tempDir, 'package.json'), pkg)
297
+
298
+ const result = await introspector.introspect(tempDir)
299
+
300
+ expect(result.databaseLibrary).toBe('drizzle')
301
+ })
302
+ })
@@ -0,0 +1,268 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { PrismaIntrospector } from '../../src/migration/introspectors/prisma-introspector.js'
3
+ import fs from 'fs-extra'
4
+ import path from 'path'
5
+ import os from 'os'
6
+
7
+ describe('PrismaIntrospector', () => {
8
+ let introspector: PrismaIntrospector
9
+ let tempDir: string
10
+
11
+ beforeEach(async () => {
12
+ introspector = new PrismaIntrospector()
13
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prisma-test-'))
14
+ await fs.ensureDir(path.join(tempDir, 'prisma'))
15
+ })
16
+
17
+ afterEach(async () => {
18
+ await fs.remove(tempDir)
19
+ })
20
+
21
+ it('should parse a simple schema', async () => {
22
+ const schema = `
23
+ datasource db {
24
+ provider = "sqlite"
25
+ url = "file:./dev.db"
26
+ }
27
+
28
+ model User {
29
+ id String @id @default(cuid())
30
+ email String @unique
31
+ name String?
32
+ posts Post[]
33
+ }
34
+
35
+ model Post {
36
+ id String @id @default(cuid())
37
+ title String
38
+ author User @relation(fields: [authorId], references: [id])
39
+ authorId String
40
+ }
41
+ `
42
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
43
+
44
+ const result = await introspector.introspect(tempDir)
45
+
46
+ expect(result.provider).toBe('sqlite')
47
+ expect(result.models).toHaveLength(2)
48
+
49
+ const user = result.models.find((m) => m.name === 'User')
50
+ expect(user).toBeDefined()
51
+ expect(user!.fields).toHaveLength(4)
52
+
53
+ const post = result.models.find((m) => m.name === 'Post')
54
+ expect(post).toBeDefined()
55
+ expect(post!.hasRelations).toBe(true)
56
+ })
57
+
58
+ it('should parse enums', async () => {
59
+ const schema = `
60
+ datasource db {
61
+ provider = "postgresql"
62
+ url = env("DATABASE_URL")
63
+ }
64
+
65
+ enum Role {
66
+ USER
67
+ ADMIN
68
+ MODERATOR
69
+ }
70
+
71
+ model User {
72
+ id String @id
73
+ role Role @default(USER)
74
+ }
75
+ `
76
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
77
+
78
+ const result = await introspector.introspect(tempDir)
79
+
80
+ expect(result.enums).toHaveLength(1)
81
+ expect(result.enums[0].name).toBe('Role')
82
+ expect(result.enums[0].values).toEqual(['USER', 'ADMIN', 'MODERATOR'])
83
+ })
84
+
85
+ it('should handle optional and list fields', async () => {
86
+ const schema = `
87
+ datasource db {
88
+ provider = "sqlite"
89
+ url = "file:./dev.db"
90
+ }
91
+
92
+ model User {
93
+ id String @id
94
+ name String?
95
+ emails String[]
96
+ isActive Boolean @default(true)
97
+ }
98
+ `
99
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
100
+
101
+ const result = await introspector.introspect(tempDir)
102
+ const user = result.models[0]
103
+
104
+ const nameField = user.fields.find((f) => f.name === 'name')
105
+ expect(nameField!.isRequired).toBe(false)
106
+
107
+ const emailsField = user.fields.find((f) => f.name === 'emails')
108
+ expect(emailsField!.isList).toBe(true)
109
+
110
+ const isActiveField = user.fields.find((f) => f.name === 'isActive')
111
+ expect(isActiveField!.defaultValue).toBe('true')
112
+ })
113
+
114
+ it('should parse field attributes correctly', async () => {
115
+ const schema = `
116
+ datasource db {
117
+ provider = "postgresql"
118
+ url = env("DATABASE_URL")
119
+ }
120
+
121
+ model User {
122
+ id String @id @default(cuid())
123
+ email String @unique
124
+ username String
125
+ age Int
126
+ }
127
+ `
128
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
129
+
130
+ const result = await introspector.introspect(tempDir)
131
+ const user = result.models[0]
132
+
133
+ const idField = user.fields.find((f) => f.name === 'id')
134
+ expect(idField!.isId).toBe(true)
135
+ expect(idField!.defaultValue).toBe('cuid()')
136
+
137
+ const emailField = user.fields.find((f) => f.name === 'email')
138
+ expect(emailField!.isUnique).toBe(true)
139
+ })
140
+
141
+ it('should parse relationships correctly', async () => {
142
+ const schema = `
143
+ datasource db {
144
+ provider = "sqlite"
145
+ url = "file:./dev.db"
146
+ }
147
+
148
+ model Post {
149
+ id String @id
150
+ author User @relation(fields: [authorId], references: [id])
151
+ authorId String
152
+ }
153
+
154
+ model User {
155
+ id String @id
156
+ posts Post[]
157
+ }
158
+ `
159
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
160
+
161
+ const result = await introspector.introspect(tempDir)
162
+ const post = result.models.find((m) => m.name === 'Post')
163
+
164
+ const authorField = post!.fields.find((f) => f.name === 'author')
165
+ expect(authorField!.relation).toBeDefined()
166
+ expect(authorField!.relation!.model).toBe('User')
167
+ expect(authorField!.relation!.fields).toEqual(['authorId'])
168
+ expect(authorField!.relation!.references).toEqual(['id'])
169
+ })
170
+
171
+ it('should map Prisma types to OpenSaaS types', () => {
172
+ expect(introspector.mapPrismaTypeToOpenSaas('String')).toEqual({ type: 'text', import: 'text' })
173
+ expect(introspector.mapPrismaTypeToOpenSaas('Int')).toEqual({
174
+ type: 'integer',
175
+ import: 'integer',
176
+ })
177
+ expect(introspector.mapPrismaTypeToOpenSaas('Boolean')).toEqual({
178
+ type: 'checkbox',
179
+ import: 'checkbox',
180
+ })
181
+ expect(introspector.mapPrismaTypeToOpenSaas('DateTime')).toEqual({
182
+ type: 'timestamp',
183
+ import: 'timestamp',
184
+ })
185
+ expect(introspector.mapPrismaTypeToOpenSaas('Json')).toEqual({ type: 'json', import: 'json' })
186
+ })
187
+
188
+ it('should generate warnings for unsupported types', async () => {
189
+ const schema = `
190
+ datasource db {
191
+ provider = "postgresql"
192
+ url = env("DATABASE_URL")
193
+ }
194
+
195
+ model Data {
196
+ id String @id
197
+ bigValue BigInt
198
+ decimal Decimal
199
+ bytes Bytes
200
+ }
201
+ `
202
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
203
+
204
+ const result = await introspector.introspect(tempDir)
205
+ const warnings = introspector.getWarnings(result)
206
+
207
+ expect(warnings).toHaveLength(3)
208
+ expect(warnings[0]).toContain('BigInt')
209
+ expect(warnings[1]).toContain('Decimal')
210
+ expect(warnings[2]).toContain('Bytes')
211
+ })
212
+
213
+ it('should throw for missing schema', async () => {
214
+ await expect(introspector.introspect(tempDir, 'nonexistent.prisma')).rejects.toThrow(
215
+ 'Schema file not found',
216
+ )
217
+ })
218
+
219
+ it('should handle comments in schema', async () => {
220
+ const schema = `
221
+ datasource db {
222
+ provider = "sqlite"
223
+ url = "file:./dev.db"
224
+ }
225
+
226
+ // This is a comment
227
+ model User {
228
+ id String @id // Comment after field
229
+ // Another comment
230
+ email String
231
+ }
232
+ `
233
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
234
+
235
+ const result = await introspector.introspect(tempDir)
236
+ const user = result.models[0]
237
+
238
+ expect(user.fields).toHaveLength(2)
239
+ expect(user.fields.find((f) => f.name === 'id')).toBeDefined()
240
+ expect(user.fields.find((f) => f.name === 'email')).toBeDefined()
241
+ })
242
+
243
+ it('should handle model-level attributes', async () => {
244
+ const schema = `
245
+ datasource db {
246
+ provider = "postgresql"
247
+ url = env("DATABASE_URL")
248
+ }
249
+
250
+ model User {
251
+ firstName String
252
+ lastName String
253
+
254
+ @@unique([firstName, lastName])
255
+ @@index([lastName])
256
+ }
257
+ `
258
+ await fs.writeFile(path.join(tempDir, 'prisma', 'schema.prisma'), schema)
259
+
260
+ const result = await introspector.introspect(tempDir)
261
+ const user = result.models[0]
262
+
263
+ // Should only include actual fields, not model-level attributes
264
+ expect(user.fields).toHaveLength(2)
265
+ expect(user.fields.find((f) => f.name === 'firstName')).toBeDefined()
266
+ expect(user.fields.find((f) => f.name === 'lastName')).toBeDefined()
267
+ })
268
+ })