@opensaas/stack-cli 0.1.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 (51) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/README.md +328 -0
  3. package/bin/opensaas.js +3 -0
  4. package/dist/commands/dev.d.ts +2 -0
  5. package/dist/commands/dev.d.ts.map +1 -0
  6. package/dist/commands/dev.js +40 -0
  7. package/dist/commands/dev.js.map +1 -0
  8. package/dist/commands/generate.d.ts +2 -0
  9. package/dist/commands/generate.d.ts.map +1 -0
  10. package/dist/commands/generate.js +90 -0
  11. package/dist/commands/generate.js.map +1 -0
  12. package/dist/commands/init.d.ts +2 -0
  13. package/dist/commands/init.d.ts.map +1 -0
  14. package/dist/commands/init.js +343 -0
  15. package/dist/commands/init.js.map +1 -0
  16. package/dist/generator/context.d.ts +13 -0
  17. package/dist/generator/context.d.ts.map +1 -0
  18. package/dist/generator/context.js +69 -0
  19. package/dist/generator/context.js.map +1 -0
  20. package/dist/generator/index.d.ts +5 -0
  21. package/dist/generator/index.d.ts.map +1 -0
  22. package/dist/generator/index.js +5 -0
  23. package/dist/generator/index.js.map +1 -0
  24. package/dist/generator/prisma.d.ts +10 -0
  25. package/dist/generator/prisma.d.ts.map +1 -0
  26. package/dist/generator/prisma.js +129 -0
  27. package/dist/generator/prisma.js.map +1 -0
  28. package/dist/generator/type-patcher.d.ts +13 -0
  29. package/dist/generator/type-patcher.d.ts.map +1 -0
  30. package/dist/generator/type-patcher.js +68 -0
  31. package/dist/generator/type-patcher.js.map +1 -0
  32. package/dist/generator/types.d.ts +10 -0
  33. package/dist/generator/types.d.ts.map +1 -0
  34. package/dist/generator/types.js +225 -0
  35. package/dist/generator/types.js.map +1 -0
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +28 -0
  39. package/dist/index.js.map +1 -0
  40. package/package.json +48 -0
  41. package/src/commands/dev.ts +48 -0
  42. package/src/commands/generate.ts +103 -0
  43. package/src/commands/init.ts +367 -0
  44. package/src/generator/context.ts +75 -0
  45. package/src/generator/index.ts +4 -0
  46. package/src/generator/prisma.ts +157 -0
  47. package/src/generator/type-patcher.ts +93 -0
  48. package/src/generator/types.ts +263 -0
  49. package/src/index.ts +34 -0
  50. package/tsconfig.json +13 -0
  51. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,367 @@
1
+ import * as path from 'path'
2
+ import * as fs from 'fs'
3
+ import chalk from 'chalk'
4
+ import ora from 'ora'
5
+ import prompts from 'prompts'
6
+
7
+ export async function initCommand(projectName: string | undefined) {
8
+ console.log(chalk.bold.cyan('\nšŸš€ Create OpenSaas Project\n'))
9
+
10
+ // Prompt for project name if not provided
11
+ if (!projectName) {
12
+ const response = await prompts({
13
+ type: 'text',
14
+ name: 'name',
15
+ message: 'Project name:',
16
+ initial: 'my-opensaas-app',
17
+ validate: (value) => {
18
+ if (!value) return 'Project name is required'
19
+ if (!/^[a-z0-9-]+$/.test(value)) {
20
+ return 'Project name must contain only lowercase letters, numbers, and hyphens'
21
+ }
22
+ return true
23
+ },
24
+ })
25
+
26
+ if (!response.name) {
27
+ console.log(chalk.yellow('\nāŒ Cancelled'))
28
+ process.exit(0)
29
+ }
30
+
31
+ projectName = response.name
32
+ }
33
+
34
+ // Type guard to ensure projectName is defined
35
+ if (!projectName) {
36
+ console.error(chalk.red('\nāŒ Project name is required'))
37
+ process.exit(1)
38
+ }
39
+
40
+ const projectPath = path.join(process.cwd(), projectName)
41
+
42
+ // Check if directory already exists
43
+ if (fs.existsSync(projectPath)) {
44
+ console.error(chalk.red(`\nāŒ Directory "${projectName}" already exists`))
45
+ process.exit(1)
46
+ }
47
+
48
+ const spinner = ora('Creating project structure...').start()
49
+
50
+ try {
51
+ // Create project directory
52
+ fs.mkdirSync(projectPath, { recursive: true })
53
+
54
+ // Create basic structure
55
+ fs.mkdirSync(path.join(projectPath, 'app'), { recursive: true })
56
+ fs.mkdirSync(path.join(projectPath, 'lib'), { recursive: true })
57
+ fs.mkdirSync(path.join(projectPath, 'prisma'), { recursive: true })
58
+
59
+ spinner.text = 'Writing configuration files...'
60
+
61
+ // Create package.json
62
+ const packageJson = {
63
+ name: projectName,
64
+ version: '0.1.0',
65
+ private: true,
66
+ scripts: {
67
+ dev: 'next dev',
68
+ build: 'next build',
69
+ start: 'next start',
70
+ generate: 'opensaas generate',
71
+ 'db:push': 'prisma db push',
72
+ 'db:studio': 'prisma studio',
73
+ },
74
+ dependencies: {
75
+ '@opensaas/stack-core': '^0.1.0',
76
+ '@prisma/client': '^5.7.1',
77
+ next: '^14.0.4',
78
+ react: '^18.2.0',
79
+ 'react-dom': '^18.2.0',
80
+ },
81
+ devDependencies: {
82
+ '@opensaas/stack-cli': '^0.1.0',
83
+ '@types/node': '^20.10.0',
84
+ '@types/react': '^18.2.45',
85
+ '@types/react-dom': '^18.2.18',
86
+ prisma: '^5.7.1',
87
+ tsx: '^4.7.0',
88
+ typescript: '^5.3.3',
89
+ },
90
+ }
91
+
92
+ fs.writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2))
93
+
94
+ // Create tsconfig.json
95
+ const tsConfig = {
96
+ compilerOptions: {
97
+ target: 'ES2022',
98
+ lib: ['dom', 'dom.iterable', 'esnext'],
99
+ allowJs: true,
100
+ skipLibCheck: true,
101
+ strict: true,
102
+ noEmit: true,
103
+ esModuleInterop: true,
104
+ module: 'esnext',
105
+ moduleResolution: 'bundler',
106
+ resolveJsonModule: true,
107
+ isolatedModules: true,
108
+ jsx: 'preserve',
109
+ incremental: true,
110
+ plugins: [{ name: 'next' }],
111
+ paths: {
112
+ '@/*': ['./*'],
113
+ },
114
+ },
115
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
116
+ exclude: ['node_modules'],
117
+ }
118
+
119
+ fs.writeFileSync(path.join(projectPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2))
120
+
121
+ // Create next.config.js
122
+ const nextConfig = `/** @type {import('next').NextConfig} */
123
+ const nextConfig = {
124
+ experimental: {
125
+ serverComponentsExternalPackages: ['@prisma/client', '@opensaas/stack-core'],
126
+ },
127
+ }
128
+
129
+ module.exports = nextConfig
130
+ `
131
+ fs.writeFileSync(path.join(projectPath, 'next.config.js'), nextConfig)
132
+
133
+ // Create .env
134
+ const env = `DATABASE_URL="file:./dev.db"
135
+ `
136
+ fs.writeFileSync(path.join(projectPath, '.env'), env)
137
+
138
+ // Create .gitignore
139
+ const gitignore = `# Dependencies
140
+ node_modules
141
+ .pnp
142
+ .pnp.js
143
+
144
+ # Testing
145
+ coverage
146
+
147
+ # Next.js
148
+ .next
149
+ out
150
+
151
+ # Production
152
+ build
153
+ dist
154
+
155
+ # Misc
156
+ .DS_Store
157
+ *.pem
158
+
159
+ # Debug
160
+ npm-debug.log*
161
+ yarn-debug.log*
162
+ yarn-error.log*
163
+
164
+ # Local env files
165
+ .env
166
+ .env.local
167
+ .env.development.local
168
+ .env.test.local
169
+ .env.production.local
170
+
171
+ # Vercel
172
+ .vercel
173
+
174
+ # TypeScript
175
+ *.tsbuildinfo
176
+
177
+ # OpenSaas generated
178
+ .opensaas
179
+
180
+ # Prisma
181
+ prisma/dev.db
182
+ prisma/dev.db-journal
183
+ `
184
+ fs.writeFileSync(path.join(projectPath, '.gitignore'), gitignore)
185
+
186
+ // Create opensaas.config.ts
187
+ const config = `import { config, list } from '@opensaas/stack-core'
188
+ import { text, relationship, password } from '@opensaas/stack-core/fields'
189
+ import type { AccessControl } from '@opensaas/stack-core'
190
+
191
+ // Access control helpers
192
+ const isSignedIn: AccessControl = ({ session }) => {
193
+ return !!session
194
+ }
195
+
196
+ export default config({
197
+ db: {
198
+ provider: 'sqlite',
199
+ url: process.env.DATABASE_URL || 'file:./dev.db',
200
+ },
201
+
202
+ lists: {
203
+ User: list({
204
+ fields: {
205
+ name: text({ validation: { isRequired: true } }),
206
+ email: text({
207
+ validation: { isRequired: true },
208
+ isIndexed: 'unique',
209
+ }),
210
+ password: password({ validation: { isRequired: true } }),
211
+ },
212
+ }),
213
+ },
214
+
215
+ session: {
216
+ getSession: async () => {
217
+ // TODO: Integrate with your auth system
218
+ return null
219
+ }
220
+ },
221
+
222
+ ui: {
223
+ basePath: '/admin',
224
+ }
225
+ })
226
+ `
227
+ fs.writeFileSync(path.join(projectPath, 'opensaas.config.ts'), config)
228
+
229
+ // Create lib/context.ts
230
+ const contextFile = `import { PrismaClient } from '@prisma/client'
231
+ import { getContext as createContext } from '@opensaas/stack-core'
232
+ import config from '../opensaas.config'
233
+ import type { Context } from '../.opensaas/types'
234
+
235
+ // Singleton Prisma client
236
+ const globalForPrisma = globalThis as unknown as {
237
+ prisma: PrismaClient | undefined
238
+ }
239
+
240
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient()
241
+
242
+ if (process.env.NODE_ENV !== 'production') {
243
+ globalForPrisma.prisma = prisma
244
+ }
245
+
246
+ /**
247
+ * Get an access-controlled context for the current session
248
+ */
249
+ export async function getContext(): Promise<Context> {
250
+ const session = config.session ? await config.session.getSession() : null
251
+ const context = await createContext<PrismaClient>(config, prisma, session)
252
+ return context as Context
253
+ }
254
+ `
255
+ fs.writeFileSync(path.join(projectPath, 'lib', 'context.ts'), contextFile)
256
+
257
+ // Create app/page.tsx
258
+ const page = `export default function Home() {
259
+ return (
260
+ <main className="flex min-h-screen flex-col items-center justify-center p-24">
261
+ <h1 className="text-4xl font-bold mb-4">Welcome to OpenSaas</h1>
262
+ <p className="text-gray-600">Your project is ready to go!</p>
263
+
264
+ <div className="mt-8 space-y-2">
265
+ <p className="text-sm">Next steps:</p>
266
+ <ol className="text-sm text-gray-600 list-decimal list-inside space-y-1">
267
+ <li>Run <code className="bg-gray-100 px-2 py-1 rounded">npm run generate</code></li>
268
+ <li>Run <code className="bg-gray-100 px-2 py-1 rounded">npm run db:push</code></li>
269
+ <li>Edit <code className="bg-gray-100 px-2 py-1 rounded">opensaas.config.ts</code> to define your schema</li>
270
+ </ol>
271
+ </div>
272
+ </main>
273
+ )
274
+ }
275
+ `
276
+ fs.writeFileSync(path.join(projectPath, 'app', 'page.tsx'), page)
277
+
278
+ // Create app/layout.tsx
279
+ const layout = `export const metadata = {
280
+ title: '${projectName}',
281
+ description: 'Built with OpenSaas',
282
+ }
283
+
284
+ export default function RootLayout({
285
+ children,
286
+ }: {
287
+ children: React.ReactNode
288
+ }) {
289
+ return (
290
+ <html lang="en">
291
+ <body>{children}</body>
292
+ </html>
293
+ )
294
+ }
295
+ `
296
+ fs.writeFileSync(path.join(projectPath, 'app', 'layout.tsx'), layout)
297
+
298
+ // Create README.md
299
+ const readme = `# ${projectName}
300
+
301
+ Built with [OpenSaas Stack](https://github.com/your-org/opensaas-stack)
302
+
303
+ ## Getting Started
304
+
305
+ 1. Install dependencies:
306
+ \`\`\`bash
307
+ npm install
308
+ # or
309
+ pnpm install
310
+ \`\`\`
311
+
312
+ 2. Generate Prisma schema and types:
313
+ \`\`\`bash
314
+ npm run generate
315
+ \`\`\`
316
+
317
+ 3. Push schema to database:
318
+ \`\`\`bash
319
+ npm run db:push
320
+ \`\`\`
321
+
322
+ 4. Run the development server:
323
+ \`\`\`bash
324
+ npm run dev
325
+ \`\`\`
326
+
327
+ Open [http://localhost:3000](http://localhost:3000) to see your app.
328
+
329
+ ## Project Structure
330
+
331
+ - \`opensaas.config.ts\` - Your schema definition with access control
332
+ - \`lib/context.ts\` - Database context with access control
333
+ - \`app/\` - Next.js app router pages
334
+ - \`prisma/\` - Generated Prisma schema
335
+ - \`.opensaas/\` - Generated TypeScript types
336
+
337
+ ## Learn More
338
+
339
+ - [OpenSaas Documentation](https://github.com/your-org/opensaas-stack)
340
+ - [Next.js Documentation](https://nextjs.org/docs)
341
+ - [Prisma Documentation](https://www.prisma.io/docs)
342
+ `
343
+ fs.writeFileSync(path.join(projectPath, 'README.md'), readme)
344
+
345
+ spinner.succeed(chalk.green('Project created successfully!'))
346
+
347
+ console.log(chalk.bold.green(`\n✨ Created ${projectName}\n`))
348
+ console.log(chalk.gray('Next steps:\n'))
349
+ console.log(chalk.cyan(` cd ${projectName}`))
350
+ console.log(chalk.cyan(' npm install'))
351
+ console.log(chalk.cyan(' npm run generate'))
352
+ console.log(chalk.cyan(' npm run db:push'))
353
+ console.log(chalk.cyan(' npm run dev'))
354
+ console.log()
355
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
356
+ } catch (error: any) {
357
+ spinner.fail(chalk.red('Failed to create project'))
358
+ console.error(chalk.red('\nāŒ Error:'), error.message)
359
+
360
+ // Cleanup on failure
361
+ if (fs.existsSync(projectPath)) {
362
+ fs.rmSync(projectPath, { recursive: true, force: true })
363
+ }
364
+
365
+ process.exit(1)
366
+ }
367
+ }
@@ -0,0 +1,75 @@
1
+ import type { OpenSaasConfig } from '@opensaas/stack-core'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+
5
+ /**
6
+ * Generate context factory that abstracts Prisma client from developers
7
+ *
8
+ * Creates a simple API with getContext() and getContextWithSession(session)
9
+ * that internally handles Prisma singleton and config imports.
10
+ */
11
+ export function generateContext(config: OpenSaasConfig): string {
12
+ // Check if custom Prisma client constructor is provided
13
+ const hasCustomConstructor = !!config.db.prismaClientConstructor
14
+
15
+ // Generate the Prisma client instantiation code
16
+ const prismaInstantiation = hasCustomConstructor
17
+ ? `config.db.prismaClientConstructor!(PrismaClient)`
18
+ : `new PrismaClient()`
19
+
20
+ return `/**
21
+ * Auto-generated context factory
22
+ *
23
+ * This module provides a simple API for creating OpenSaas contexts.
24
+ * It abstracts away Prisma client management and configuration.
25
+ *
26
+ * DO NOT EDIT - This file is automatically generated by 'pnpm generate'
27
+ */
28
+
29
+ import { getContext as getOpensaasContext } from '@opensaas/stack-core'
30
+ import { PrismaClient } from './prisma-client'
31
+ import config from '../opensaas.config'
32
+
33
+ // Internal Prisma singleton - managed automatically
34
+ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
35
+ const prisma = globalForPrisma.prisma ?? ${prismaInstantiation}
36
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
37
+
38
+ /**
39
+ * Get OpenSaas context with optional session
40
+ *
41
+ * @param session - Optional session object (structure defined by your application)
42
+ *
43
+ * @example
44
+ * \`\`\`typescript
45
+ * // Anonymous access
46
+ * const context = getContext()
47
+ * const posts = await context.db.post.findMany()
48
+ *
49
+ * // Authenticated access
50
+ * const context = getContext({ userId: 'user-123' })
51
+ * const myPosts = await context.db.post.findMany()
52
+ * \`\`\`
53
+ */
54
+ export function getContext(session?: { userId?: string; [key: string]: unknown } | null) {
55
+ return getOpensaasContext(config, prisma, session ?? null)
56
+ }
57
+
58
+ export const rawOpensaasContext = getContext()
59
+ `
60
+ }
61
+
62
+ /**
63
+ * Write context factory to file
64
+ */
65
+ export function writeContext(config: OpenSaasConfig, outputPath: string): void {
66
+ const content = generateContext(config)
67
+
68
+ // Ensure directory exists
69
+ const dir = path.dirname(outputPath)
70
+ if (!fs.existsSync(dir)) {
71
+ fs.mkdirSync(dir, { recursive: true })
72
+ }
73
+
74
+ fs.writeFileSync(outputPath, content, 'utf-8')
75
+ }
@@ -0,0 +1,4 @@
1
+ export { generatePrismaSchema, writePrismaSchema } from './prisma.js'
2
+ export { generateTypes, writeTypes } from './types.js'
3
+ export { patchPrismaTypes } from './type-patcher.js'
4
+ export { generateContext, writeContext } from './context.js'
@@ -0,0 +1,157 @@
1
+ import type { OpenSaasConfig, FieldConfig, RelationshipField } from '@opensaas/stack-core'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+
5
+ /**
6
+ * Map OpenSaas field types to Prisma field types
7
+ */
8
+ function mapFieldTypeToPrisma(fieldName: string, field: FieldConfig): string | null {
9
+ // Relationships are handled separately
10
+ if (field.type === 'relationship') {
11
+ return null
12
+ }
13
+
14
+ // Use field's own Prisma type generator if available
15
+ if (field.getPrismaType) {
16
+ const result = field.getPrismaType(fieldName)
17
+ return result.type
18
+ }
19
+
20
+ // Fallback for fields without generator methods
21
+ throw new Error(`Field type "${field.type}" does not implement getPrismaType method`)
22
+ }
23
+
24
+ /**
25
+ * Get field modifiers (?, @default, @unique, etc.)
26
+ */
27
+ function getFieldModifiers(fieldName: string, field: FieldConfig): string {
28
+ // Handle relationships separately
29
+ if (field.type === 'relationship') {
30
+ const relField = field as RelationshipField
31
+ if (relField.many) {
32
+ return '[]'
33
+ } else {
34
+ return '?'
35
+ }
36
+ }
37
+
38
+ // Use field's own Prisma type generator if available
39
+ if (field.getPrismaType) {
40
+ const result = field.getPrismaType(fieldName)
41
+ return result.modifiers || ''
42
+ }
43
+
44
+ // Fallback for fields without generator methods
45
+ return ''
46
+ }
47
+
48
+ /**
49
+ * Parse relationship ref to get target list and field
50
+ */
51
+ function parseRelationshipRef(ref: string): { list: string; field: string } {
52
+ const [list, field] = ref.split('.')
53
+ if (!list || !field) {
54
+ throw new Error(`Invalid relationship ref: ${ref}`)
55
+ }
56
+ return { list, field }
57
+ }
58
+
59
+ /**
60
+ * Generate Prisma schema from OpenSaas config
61
+ */
62
+ export function generatePrismaSchema(config: OpenSaasConfig): string {
63
+ const lines: string[] = []
64
+
65
+ const opensaasPath = config.opensaasPath || '.opensaas'
66
+
67
+ // Generator and datasource
68
+ lines.push('generator client {')
69
+ lines.push(' provider = "prisma-client-js"')
70
+ lines.push(` output = "../${opensaasPath}/prisma-client"`)
71
+ lines.push('}')
72
+ lines.push('')
73
+ lines.push('datasource db {')
74
+ lines.push(` provider = "${config.db.provider}"`)
75
+ lines.push(' url = env("DATABASE_URL")')
76
+ lines.push('}')
77
+ lines.push('')
78
+
79
+ // Generate models for each list
80
+ for (const [listName, listConfig] of Object.entries(config.lists)) {
81
+ lines.push(`model ${listName} {`)
82
+
83
+ // Always add id field
84
+ lines.push(' id String @id @default(cuid())')
85
+
86
+ // Track relationship fields for later processing
87
+ const relationshipFields: Array<{
88
+ name: string
89
+ field: RelationshipField
90
+ }> = []
91
+
92
+ // Add regular fields
93
+ for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
94
+ if (fieldConfig.type === 'relationship') {
95
+ relationshipFields.push({
96
+ name: fieldName,
97
+ field: fieldConfig as RelationshipField,
98
+ })
99
+ continue
100
+ }
101
+
102
+ const prismaType = mapFieldTypeToPrisma(fieldName, fieldConfig)
103
+ if (!prismaType) continue // Skip if no type returned
104
+
105
+ const modifiers = getFieldModifiers(fieldName, fieldConfig)
106
+
107
+ // Format with proper spacing
108
+ const paddedName = fieldName.padEnd(12)
109
+ lines.push(` ${paddedName} ${prismaType}${modifiers}`)
110
+ }
111
+
112
+ // Add relationship fields
113
+ for (const { name: fieldName, field: relField } of relationshipFields) {
114
+ const { list: targetList } = parseRelationshipRef(relField.ref)
115
+ const _modifiers = getFieldModifiers(fieldName, relField)
116
+ const paddedName = fieldName.padEnd(12)
117
+
118
+ if (relField.many) {
119
+ // One-to-many relationship
120
+ lines.push(` ${paddedName} ${targetList}[]`)
121
+ } else {
122
+ // Many-to-one relationship (add foreign key field)
123
+ const foreignKeyField = `${fieldName}Id`
124
+ const fkPaddedName = foreignKeyField.padEnd(12)
125
+
126
+ lines.push(` ${fkPaddedName} String?`)
127
+ lines.push(
128
+ ` ${paddedName} ${targetList}? @relation(fields: [${foreignKeyField}], references: [id])`,
129
+ )
130
+ }
131
+ }
132
+
133
+ // Always add timestamps
134
+ lines.push(' createdAt DateTime @default(now())')
135
+ lines.push(' updatedAt DateTime @updatedAt')
136
+
137
+ lines.push('}')
138
+ lines.push('')
139
+ }
140
+
141
+ return lines.join('\n')
142
+ }
143
+
144
+ /**
145
+ * Write Prisma schema to file
146
+ */
147
+ export function writePrismaSchema(config: OpenSaasConfig, outputPath: string): void {
148
+ const schema = generatePrismaSchema(config)
149
+
150
+ // Ensure directory exists
151
+ const dir = path.dirname(outputPath)
152
+ if (!fs.existsSync(dir)) {
153
+ fs.mkdirSync(dir, { recursive: true })
154
+ }
155
+
156
+ fs.writeFileSync(outputPath, schema, 'utf-8')
157
+ }
@@ -0,0 +1,93 @@
1
+ import type { OpenSaasConfig } from '@opensaas/stack-core'
2
+ import * as fs from 'fs'
3
+ import * as path from 'path'
4
+
5
+ /**
6
+ * Patches Prisma's generated types based on field-level type patch configurations
7
+ *
8
+ * This reads Prisma's generated index.d.ts and replaces field types according to
9
+ * the `typePatch` configuration on each field. Fields can specify custom result types
10
+ * that will replace the original Prisma types in query results.
11
+ *
12
+ * The patched types are written to `.opensaas/prisma-client.d.ts` so users can
13
+ * import from there to get the transformed types.
14
+ */
15
+ export function patchPrismaTypes(config: OpenSaasConfig, projectRoot: string): void {
16
+ const opensaasPath = config.opensaasPath || '.opensaas'
17
+
18
+ // Prisma generates to opensaasPath/prisma-client
19
+ const prismaClientDir = path.join(projectRoot, opensaasPath, 'prisma-client')
20
+ const prismaIndexPath = path.join(prismaClientDir, 'index.d.ts')
21
+
22
+ // Check if Prisma types exist
23
+ if (!fs.existsSync(prismaIndexPath)) {
24
+ console.warn(
25
+ 'āš ļø Prisma types not found. Run `npx prisma generate` first to generate Prisma Client.',
26
+ )
27
+ return
28
+ }
29
+
30
+ // Read original Prisma types
31
+ const originalTypes = fs.readFileSync(prismaIndexPath, 'utf-8')
32
+
33
+ // Collect all fields that need type patching
34
+ type FieldPatch = {
35
+ fieldName: string
36
+ resultType: string
37
+ patchScope: 'scalars-only' | 'all'
38
+ }
39
+
40
+ const fieldPatches: FieldPatch[] = []
41
+
42
+ for (const listConfig of Object.values(config.lists)) {
43
+ for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
44
+ if (fieldConfig.typePatch) {
45
+ fieldPatches.push({
46
+ fieldName,
47
+ resultType: fieldConfig.typePatch.resultType,
48
+ patchScope: fieldConfig.typePatch.patchScope || 'scalars-only',
49
+ })
50
+ }
51
+ }
52
+ }
53
+
54
+ if (fieldPatches.length === 0) {
55
+ // No fields need patching
56
+ return
57
+ }
58
+
59
+ // Patch the types
60
+ let patchedTypes = originalTypes
61
+
62
+ // For each field that needs patching, replace its type
63
+ for (const { fieldName, resultType, patchScope } of fieldPatches) {
64
+ if (patchScope === 'scalars-only') {
65
+ // Pattern matches: fieldName: <type> ONLY inside scalars: $Extensions.GetPayloadResult<{...}>
66
+ // This ensures we don't patch Input types (UserCreateInput, etc.)
67
+ // Example match:
68
+ // scalars: $Extensions.GetPayloadResult<{
69
+ // id: string
70
+ // password: string ← Replace this
71
+ // ...
72
+ const pattern = new RegExp(
73
+ `(scalars:\\s*\\$Extensions\\.GetPayloadResult<\\{[^}]*?\\b${fieldName}:\\s*)[^,\\n}]+`,
74
+ 'g',
75
+ )
76
+
77
+ patchedTypes = patchedTypes.replace(pattern, `$1${resultType}`)
78
+ } else {
79
+ // patchScope === 'all' - patch everywhere the field appears
80
+ // This is more aggressive and will patch input types too
81
+ const pattern = new RegExp(`(\\b${fieldName}:\\s*)[^,\\n}]+`, 'g')
82
+ patchedTypes = patchedTypes.replace(pattern, `$1${resultType}`)
83
+ }
84
+ }
85
+
86
+ // Write patched types back to Prisma's index.d.ts
87
+ // This directly modifies the Prisma-generated file with our type patches
88
+ fs.writeFileSync(prismaIndexPath, patchedTypes, 'utf-8')
89
+
90
+ console.log(
91
+ `āœ… Patched Prisma types (${fieldPatches.length} field${fieldPatches.length === 1 ? '' : 's'})`,
92
+ )
93
+ }