@opensaas/stack-cli 0.5.0 → 0.6.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.
- package/README.md +76 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +94 -268
- package/dist/commands/migrate.js.map +1 -1
- package/package.json +7 -2
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -462
- package/CLAUDE.md +0 -298
- package/src/commands/__snapshots__/generate.test.ts.snap +0 -413
- package/src/commands/dev.test.ts +0 -215
- package/src/commands/dev.ts +0 -48
- package/src/commands/generate.test.ts +0 -282
- package/src/commands/generate.ts +0 -182
- package/src/commands/init.ts +0 -34
- package/src/commands/mcp.ts +0 -135
- package/src/commands/migrate.ts +0 -534
- package/src/generator/__snapshots__/context.test.ts.snap +0 -361
- package/src/generator/__snapshots__/prisma.test.ts.snap +0 -174
- package/src/generator/__snapshots__/types.test.ts.snap +0 -1702
- package/src/generator/context.test.ts +0 -139
- package/src/generator/context.ts +0 -227
- package/src/generator/index.ts +0 -7
- package/src/generator/lists.test.ts +0 -335
- package/src/generator/lists.ts +0 -140
- package/src/generator/plugin-types.ts +0 -147
- package/src/generator/prisma-config.ts +0 -46
- package/src/generator/prisma-extensions.ts +0 -159
- package/src/generator/prisma.test.ts +0 -211
- package/src/generator/prisma.ts +0 -161
- package/src/generator/types.test.ts +0 -268
- package/src/generator/types.ts +0 -537
- package/src/index.ts +0 -46
- package/src/mcp/lib/documentation-provider.ts +0 -710
- package/src/mcp/lib/features/catalog.ts +0 -301
- package/src/mcp/lib/generators/feature-generator.ts +0 -598
- package/src/mcp/lib/types.ts +0 -89
- package/src/mcp/lib/wizards/migration-wizard.ts +0 -584
- package/src/mcp/lib/wizards/wizard-engine.ts +0 -427
- package/src/mcp/server/index.ts +0 -361
- package/src/mcp/server/stack-mcp-server.ts +0 -544
- package/src/migration/generators/migration-generator.ts +0 -675
- package/src/migration/introspectors/index.ts +0 -12
- package/src/migration/introspectors/keystone-introspector.ts +0 -296
- package/src/migration/introspectors/nextjs-introspector.ts +0 -209
- package/src/migration/introspectors/prisma-introspector.ts +0 -233
- package/src/migration/types.ts +0 -92
- package/tests/introspectors/keystone-introspector.test.ts +0 -255
- package/tests/introspectors/nextjs-introspector.test.ts +0 -302
- package/tests/introspectors/prisma-introspector.test.ts +0 -268
- package/tests/migration-generator.test.ts +0 -592
- package/tests/migration-wizard.test.ts +0 -442
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -26
package/src/commands/dev.test.ts
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
import * as os from 'os'
|
|
5
|
-
|
|
6
|
-
// Mock chokidar
|
|
7
|
-
const mockWatcherOn = vi.fn()
|
|
8
|
-
const mockWatcherClose = vi.fn()
|
|
9
|
-
const mockWatch = vi.fn(() => ({
|
|
10
|
-
on: mockWatcherOn,
|
|
11
|
-
close: mockWatcherClose,
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
vi.mock('chokidar', () => ({
|
|
15
|
-
default: {
|
|
16
|
-
watch: mockWatch,
|
|
17
|
-
},
|
|
18
|
-
}))
|
|
19
|
-
|
|
20
|
-
// Mock the generate command
|
|
21
|
-
vi.mock('./generate.js', () => ({
|
|
22
|
-
generateCommand: vi.fn().mockResolvedValue(undefined),
|
|
23
|
-
}))
|
|
24
|
-
|
|
25
|
-
// Mock ora
|
|
26
|
-
vi.mock('ora', () => ({
|
|
27
|
-
default: vi.fn(() => ({
|
|
28
|
-
start: vi.fn().mockReturnThis(),
|
|
29
|
-
succeed: vi.fn().mockReturnThis(),
|
|
30
|
-
fail: vi.fn().mockReturnThis(),
|
|
31
|
-
text: '',
|
|
32
|
-
})),
|
|
33
|
-
}))
|
|
34
|
-
|
|
35
|
-
// Mock chalk
|
|
36
|
-
vi.mock('chalk', () => ({
|
|
37
|
-
default: {
|
|
38
|
-
bold: {
|
|
39
|
-
cyan: vi.fn((str) => str),
|
|
40
|
-
},
|
|
41
|
-
cyan: vi.fn((str) => str),
|
|
42
|
-
gray: vi.fn((str) => str),
|
|
43
|
-
red: vi.fn((str) => str),
|
|
44
|
-
yellow: vi.fn((str) => str),
|
|
45
|
-
},
|
|
46
|
-
}))
|
|
47
|
-
|
|
48
|
-
describe('Dev Command', () => {
|
|
49
|
-
let tempDir: string
|
|
50
|
-
let originalCwd: string
|
|
51
|
-
let originalExit: typeof process.exit
|
|
52
|
-
let exitCode: number | undefined
|
|
53
|
-
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
vi.clearAllMocks()
|
|
56
|
-
|
|
57
|
-
// Create temp directory
|
|
58
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dev-test-'))
|
|
59
|
-
originalCwd = process.cwd()
|
|
60
|
-
process.chdir(tempDir)
|
|
61
|
-
|
|
62
|
-
// Mock process.exit
|
|
63
|
-
originalExit = process.exit
|
|
64
|
-
exitCode = undefined
|
|
65
|
-
process.exit = vi.fn((code?: number) => {
|
|
66
|
-
exitCode = code
|
|
67
|
-
throw new Error(`process.exit(${code})`)
|
|
68
|
-
}) as never
|
|
69
|
-
|
|
70
|
-
// Create opensaas.config.ts file
|
|
71
|
-
fs.writeFileSync(
|
|
72
|
-
path.join(tempDir, 'opensaas.config.ts'),
|
|
73
|
-
`
|
|
74
|
-
import { config } from '@opensaas/stack-core'
|
|
75
|
-
export default config({
|
|
76
|
-
lists: {}
|
|
77
|
-
})
|
|
78
|
-
`,
|
|
79
|
-
)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
afterEach(() => {
|
|
83
|
-
// Restore
|
|
84
|
-
process.chdir(originalCwd)
|
|
85
|
-
process.exit = originalExit
|
|
86
|
-
|
|
87
|
-
// Clean up
|
|
88
|
-
if (fs.existsSync(tempDir)) {
|
|
89
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
describe('devCommand', () => {
|
|
94
|
-
it('should fail if config file does not exist', async () => {
|
|
95
|
-
// Remove config file
|
|
96
|
-
fs.unlinkSync(path.join(tempDir, 'opensaas.config.ts'))
|
|
97
|
-
|
|
98
|
-
const { devCommand } = await import('./dev.js')
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
await devCommand()
|
|
102
|
-
} catch {
|
|
103
|
-
// Expected to throw
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
expect(exitCode).toBe(1)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('should call generateCommand initially', async () => {
|
|
110
|
-
const { generateCommand } = await import('./generate.js')
|
|
111
|
-
const { devCommand } = await import('./dev.js')
|
|
112
|
-
|
|
113
|
-
// Run dev command in background (don't await)
|
|
114
|
-
devCommand().catch(() => {
|
|
115
|
-
// Ignore errors
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
// Wait a bit for initial generation
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
120
|
-
|
|
121
|
-
expect(generateCommand).toHaveBeenCalled()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('should set up file watcher for config file', async () => {
|
|
125
|
-
const { devCommand } = await import('./dev.js')
|
|
126
|
-
|
|
127
|
-
// Run dev command
|
|
128
|
-
devCommand().catch(() => {
|
|
129
|
-
// Ignore errors
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
133
|
-
|
|
134
|
-
// Verify watcher was set up
|
|
135
|
-
expect(mockWatch).toHaveBeenCalled()
|
|
136
|
-
expect(mockWatch.mock.calls.length).toBeGreaterThan(0)
|
|
137
|
-
|
|
138
|
-
const watchPath = mockWatch.mock.calls[0]![0]
|
|
139
|
-
expect(watchPath).toContain('opensaas.config.ts')
|
|
140
|
-
|
|
141
|
-
const watchOptions = mockWatch.mock.calls[0]![1]
|
|
142
|
-
expect(watchOptions).toMatchObject({
|
|
143
|
-
persistent: true,
|
|
144
|
-
ignoreInitial: true,
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('should register change event handler', async () => {
|
|
149
|
-
const { devCommand } = await import('./dev.js')
|
|
150
|
-
|
|
151
|
-
devCommand().catch(() => {
|
|
152
|
-
// Ignore errors
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
156
|
-
|
|
157
|
-
expect(mockWatcherOn).toHaveBeenCalledWith('change', expect.any(Function))
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('should register error event handler', async () => {
|
|
161
|
-
const { devCommand } = await import('./dev.js')
|
|
162
|
-
|
|
163
|
-
devCommand().catch(() => {
|
|
164
|
-
// Ignore errors
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
168
|
-
|
|
169
|
-
expect(mockWatcherOn).toHaveBeenCalledWith('error', expect.any(Function))
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('should regenerate on config file change', async () => {
|
|
173
|
-
const { generateCommand } = await import('./generate.js')
|
|
174
|
-
const { devCommand } = await import('./dev.js')
|
|
175
|
-
|
|
176
|
-
devCommand().catch(() => {
|
|
177
|
-
// Ignore errors
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
181
|
-
|
|
182
|
-
// Simulate file change
|
|
183
|
-
const changeHandler = mockWatcherOn.mock.calls.find((call) => call[0] === 'change')?.[1]
|
|
184
|
-
expect(changeHandler).toBeDefined()
|
|
185
|
-
|
|
186
|
-
if (changeHandler) {
|
|
187
|
-
await changeHandler()
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// generateCommand should be called again
|
|
191
|
-
expect(vi.mocked(generateCommand).mock.calls.length).toBeGreaterThan(1)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('should close watcher on SIGINT', async () => {
|
|
195
|
-
const { devCommand } = await import('./dev.js')
|
|
196
|
-
|
|
197
|
-
devCommand().catch(() => {
|
|
198
|
-
// Ignore errors
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
202
|
-
|
|
203
|
-
// Simulate SIGINT - catch the error from process.exit
|
|
204
|
-
try {
|
|
205
|
-
process.emit('SIGINT', 'SIGINT')
|
|
206
|
-
} catch {
|
|
207
|
-
// Expected to throw from process.exit mock
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
211
|
-
|
|
212
|
-
expect(mockWatcherClose).toHaveBeenCalled()
|
|
213
|
-
})
|
|
214
|
-
})
|
|
215
|
-
})
|
package/src/commands/dev.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import * as path from 'path'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import chalk from 'chalk'
|
|
4
|
-
import chokidar from 'chokidar'
|
|
5
|
-
import { generateCommand } from './generate.js'
|
|
6
|
-
|
|
7
|
-
export async function devCommand() {
|
|
8
|
-
const cwd = process.cwd()
|
|
9
|
-
const configPath = path.join(cwd, 'opensaas.config.ts')
|
|
10
|
-
|
|
11
|
-
// Check if config exists
|
|
12
|
-
if (!fs.existsSync(configPath)) {
|
|
13
|
-
console.error(chalk.red('Error: opensaas.config.ts not found in current directory'))
|
|
14
|
-
console.error(chalk.gray(' Please run this command from your project root'))
|
|
15
|
-
process.exit(1)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
console.log(chalk.bold.cyan('\nOpenSaas Dev Mode\n'))
|
|
19
|
-
console.log(chalk.gray('Watching for changes to opensaas.config.ts...\n'))
|
|
20
|
-
|
|
21
|
-
// Run initial generation
|
|
22
|
-
await generateCommand()
|
|
23
|
-
|
|
24
|
-
// Watch for changes
|
|
25
|
-
const watcher = chokidar.watch(configPath, {
|
|
26
|
-
persistent: true,
|
|
27
|
-
ignoreInitial: true,
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
watcher.on('change', async () => {
|
|
31
|
-
console.log(chalk.yellow('\nConfig changed, regenerating...\n'))
|
|
32
|
-
await generateCommand()
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
watcher.on('error', (error) => {
|
|
36
|
-
console.error(chalk.red('\nWatcher error:'), error)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// Keep the process running
|
|
40
|
-
console.log(chalk.gray('Press Ctrl+C to stop watching\n'))
|
|
41
|
-
|
|
42
|
-
// Handle graceful shutdown
|
|
43
|
-
process.on('SIGINT', () => {
|
|
44
|
-
console.log(chalk.yellow('\n\nStopping dev mode...'))
|
|
45
|
-
watcher.close()
|
|
46
|
-
process.exit(0)
|
|
47
|
-
})
|
|
48
|
-
}
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
import * as os from 'os'
|
|
5
|
-
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
6
|
-
import { text } from '@opensaas/stack-core/fields'
|
|
7
|
-
import { writePrismaSchema, writeTypes, writeContext } from '../generator/index.js'
|
|
8
|
-
|
|
9
|
-
// Mock ora module
|
|
10
|
-
vi.mock('ora', () => ({
|
|
11
|
-
default: vi.fn(() => ({
|
|
12
|
-
start: vi.fn().mockReturnThis(),
|
|
13
|
-
succeed: vi.fn().mockReturnThis(),
|
|
14
|
-
fail: vi.fn().mockReturnThis(),
|
|
15
|
-
text: '',
|
|
16
|
-
})),
|
|
17
|
-
}))
|
|
18
|
-
|
|
19
|
-
// Mock chalk module
|
|
20
|
-
vi.mock('chalk', () => ({
|
|
21
|
-
default: {
|
|
22
|
-
bold: vi.fn((str) => str),
|
|
23
|
-
cyan: vi.fn((str) => str),
|
|
24
|
-
gray: vi.fn((str) => str),
|
|
25
|
-
red: vi.fn((str) => str),
|
|
26
|
-
yellow: vi.fn((str) => str),
|
|
27
|
-
green: vi.fn((str) => str),
|
|
28
|
-
},
|
|
29
|
-
}))
|
|
30
|
-
|
|
31
|
-
describe('Generate Command Integration', () => {
|
|
32
|
-
let tempDir: string
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
// Create temp directory for testing
|
|
36
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'generate-test-'))
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
// Clean up
|
|
41
|
-
if (fs.existsSync(tempDir)) {
|
|
42
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('Generator Integration', () => {
|
|
47
|
-
it('should generate all files for a basic config', () => {
|
|
48
|
-
const config: OpenSaasConfig = {
|
|
49
|
-
db: {
|
|
50
|
-
provider: 'sqlite',
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
-
prismaClientConstructor: (() => null) as any,
|
|
53
|
-
},
|
|
54
|
-
lists: {
|
|
55
|
-
User: {
|
|
56
|
-
fields: {
|
|
57
|
-
name: text({ validation: { isRequired: true } }),
|
|
58
|
-
email: text({ validation: { isRequired: true } }),
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Generate files
|
|
65
|
-
const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
|
|
66
|
-
const typesPath = path.join(tempDir, '.opensaas', 'types.ts')
|
|
67
|
-
const contextPath = path.join(tempDir, '.opensaas', 'context.ts')
|
|
68
|
-
|
|
69
|
-
writePrismaSchema(config, prismaPath)
|
|
70
|
-
writeTypes(config, typesPath)
|
|
71
|
-
writeContext(config, contextPath)
|
|
72
|
-
|
|
73
|
-
// Verify all files exist
|
|
74
|
-
expect(fs.existsSync(prismaPath)).toBe(true)
|
|
75
|
-
expect(fs.existsSync(typesPath)).toBe(true)
|
|
76
|
-
expect(fs.existsSync(contextPath)).toBe(true)
|
|
77
|
-
|
|
78
|
-
// Verify file contents with snapshots
|
|
79
|
-
const prismaSchema = fs.readFileSync(prismaPath, 'utf-8')
|
|
80
|
-
expect(prismaSchema).toMatchSnapshot('prisma-schema')
|
|
81
|
-
|
|
82
|
-
const types = fs.readFileSync(typesPath, 'utf-8')
|
|
83
|
-
expect(types).toMatchSnapshot('types')
|
|
84
|
-
|
|
85
|
-
const context = fs.readFileSync(contextPath, 'utf-8')
|
|
86
|
-
expect(context).toMatchSnapshot('context')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('should create directories if they do not exist', () => {
|
|
90
|
-
const config: OpenSaasConfig = {
|
|
91
|
-
db: {
|
|
92
|
-
provider: 'sqlite',
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
-
prismaClientConstructor: (() => null) as any,
|
|
95
|
-
},
|
|
96
|
-
lists: {},
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
|
|
100
|
-
|
|
101
|
-
writePrismaSchema(config, prismaPath)
|
|
102
|
-
|
|
103
|
-
expect(fs.existsSync(path.join(tempDir, 'prisma'))).toBe(true)
|
|
104
|
-
expect(fs.existsSync(prismaPath)).toBe(true)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should overwrite existing files', () => {
|
|
108
|
-
const config1: OpenSaasConfig = {
|
|
109
|
-
db: {
|
|
110
|
-
provider: 'sqlite',
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
-
prismaClientConstructor: (() => null) as any,
|
|
113
|
-
},
|
|
114
|
-
lists: {
|
|
115
|
-
User: {
|
|
116
|
-
fields: {
|
|
117
|
-
name: text(),
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const config2: OpenSaasConfig = {
|
|
124
|
-
db: {
|
|
125
|
-
provider: 'sqlite',
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
-
prismaClientConstructor: (() => null) as any,
|
|
128
|
-
},
|
|
129
|
-
lists: {
|
|
130
|
-
Post: {
|
|
131
|
-
fields: {
|
|
132
|
-
title: text(),
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
|
|
139
|
-
|
|
140
|
-
// Generate first config
|
|
141
|
-
writePrismaSchema(config1, prismaPath)
|
|
142
|
-
let schema = fs.readFileSync(prismaPath, 'utf-8')
|
|
143
|
-
expect(schema).toMatchSnapshot('overwrite-before')
|
|
144
|
-
|
|
145
|
-
// Generate second config (should overwrite)
|
|
146
|
-
writePrismaSchema(config2, prismaPath)
|
|
147
|
-
schema = fs.readFileSync(prismaPath, 'utf-8')
|
|
148
|
-
expect(schema).toMatchSnapshot('overwrite-after')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('should handle custom opensaasPath', () => {
|
|
152
|
-
const config: OpenSaasConfig = {
|
|
153
|
-
db: {
|
|
154
|
-
provider: 'sqlite',
|
|
155
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
156
|
-
prismaClientConstructor: (() => null) as any,
|
|
157
|
-
},
|
|
158
|
-
opensaasPath: '.custom',
|
|
159
|
-
lists: {},
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const typesPath = path.join(tempDir, '.custom', 'types.ts')
|
|
163
|
-
const contextPath = path.join(tempDir, '.custom', 'context.ts')
|
|
164
|
-
|
|
165
|
-
writeTypes(config, typesPath)
|
|
166
|
-
writeContext(config, contextPath)
|
|
167
|
-
|
|
168
|
-
expect(fs.existsSync(path.join(tempDir, '.custom'))).toBe(true)
|
|
169
|
-
expect(fs.existsSync(typesPath)).toBe(true)
|
|
170
|
-
expect(fs.existsSync(contextPath)).toBe(true)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('should generate consistent output across multiple runs', () => {
|
|
174
|
-
const config: OpenSaasConfig = {
|
|
175
|
-
db: {
|
|
176
|
-
provider: 'sqlite',
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
-
prismaClientConstructor: (() => null) as any,
|
|
179
|
-
},
|
|
180
|
-
lists: {
|
|
181
|
-
User: {
|
|
182
|
-
fields: {
|
|
183
|
-
name: text(),
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
|
|
190
|
-
|
|
191
|
-
// Generate twice
|
|
192
|
-
writePrismaSchema(config, prismaPath)
|
|
193
|
-
const schema1 = fs.readFileSync(prismaPath, 'utf-8')
|
|
194
|
-
|
|
195
|
-
writePrismaSchema(config, prismaPath)
|
|
196
|
-
const schema2 = fs.readFileSync(prismaPath, 'utf-8')
|
|
197
|
-
|
|
198
|
-
// Should be identical
|
|
199
|
-
expect(schema1).toBe(schema2)
|
|
200
|
-
expect(schema1).toMatchSnapshot('consistent-output')
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('should handle empty lists config', () => {
|
|
204
|
-
const config: OpenSaasConfig = {
|
|
205
|
-
db: {
|
|
206
|
-
provider: 'sqlite',
|
|
207
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
-
prismaClientConstructor: (() => null) as any,
|
|
209
|
-
},
|
|
210
|
-
lists: {},
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const prismaPath = path.join(tempDir, 'prisma', 'schema.prisma')
|
|
214
|
-
const typesPath = path.join(tempDir, '.opensaas', 'types.ts')
|
|
215
|
-
|
|
216
|
-
writePrismaSchema(config, prismaPath)
|
|
217
|
-
writeTypes(config, typesPath)
|
|
218
|
-
|
|
219
|
-
expect(fs.existsSync(prismaPath)).toBe(true)
|
|
220
|
-
expect(fs.existsSync(typesPath)).toBe(true)
|
|
221
|
-
|
|
222
|
-
const schema = fs.readFileSync(prismaPath, 'utf-8')
|
|
223
|
-
expect(schema).toMatchSnapshot('empty-lists-schema')
|
|
224
|
-
|
|
225
|
-
const types = fs.readFileSync(typesPath, 'utf-8')
|
|
226
|
-
expect(types).toMatchSnapshot('empty-lists-types')
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('should handle different database providers', () => {
|
|
230
|
-
const providers = ['sqlite', 'postgresql', 'mysql'] as const
|
|
231
|
-
|
|
232
|
-
providers.forEach((provider) => {
|
|
233
|
-
const config: OpenSaasConfig = {
|
|
234
|
-
db: {
|
|
235
|
-
provider,
|
|
236
|
-
url: provider === 'sqlite' ? 'file:./dev.db' : 'postgresql://localhost:5432/db',
|
|
237
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
-
prismaClientConstructor: (() => null) as any,
|
|
239
|
-
},
|
|
240
|
-
lists: {},
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const prismaPath = path.join(tempDir, `${provider}-schema.prisma`)
|
|
244
|
-
writePrismaSchema(config, prismaPath)
|
|
245
|
-
|
|
246
|
-
const schema = fs.readFileSync(prismaPath, 'utf-8')
|
|
247
|
-
expect(schema).toMatchSnapshot(`${provider}-provider`)
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('should generate files in correct locations', () => {
|
|
252
|
-
const config: OpenSaasConfig = {
|
|
253
|
-
db: {
|
|
254
|
-
provider: 'sqlite',
|
|
255
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
256
|
-
prismaClientConstructor: (() => null) as any,
|
|
257
|
-
},
|
|
258
|
-
lists: {
|
|
259
|
-
User: {
|
|
260
|
-
fields: {
|
|
261
|
-
name: text(),
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
writePrismaSchema(config, path.join(tempDir, 'prisma', 'schema.prisma'))
|
|
268
|
-
writeTypes(config, path.join(tempDir, '.opensaas', 'types.ts'))
|
|
269
|
-
writeContext(config, path.join(tempDir, '.opensaas', 'context.ts'))
|
|
270
|
-
|
|
271
|
-
// Verify directory structure
|
|
272
|
-
const prismaDir = path.join(tempDir, 'prisma')
|
|
273
|
-
const opensaasDir = path.join(tempDir, '.opensaas')
|
|
274
|
-
|
|
275
|
-
expect(fs.existsSync(prismaDir)).toBe(true)
|
|
276
|
-
expect(fs.existsSync(opensaasDir)).toBe(true)
|
|
277
|
-
expect(fs.readdirSync(prismaDir)).toContain('schema.prisma')
|
|
278
|
-
expect(fs.readdirSync(opensaasDir)).toContain('types.ts')
|
|
279
|
-
expect(fs.readdirSync(opensaasDir)).toContain('context.ts')
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
})
|