@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.
- package/.turbo/turbo-build.log +4 -0
- package/README.md +328 -0
- package/bin/opensaas.js +3 -0
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +40 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +90 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +343 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/generator/context.d.ts +13 -0
- package/dist/generator/context.d.ts.map +1 -0
- package/dist/generator/context.js +69 -0
- package/dist/generator/context.js.map +1 -0
- package/dist/generator/index.d.ts +5 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +5 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/prisma.d.ts +10 -0
- package/dist/generator/prisma.d.ts.map +1 -0
- package/dist/generator/prisma.js +129 -0
- package/dist/generator/prisma.js.map +1 -0
- package/dist/generator/type-patcher.d.ts +13 -0
- package/dist/generator/type-patcher.d.ts.map +1 -0
- package/dist/generator/type-patcher.js +68 -0
- package/dist/generator/type-patcher.js.map +1 -0
- package/dist/generator/types.d.ts +10 -0
- package/dist/generator/types.d.ts.map +1 -0
- package/dist/generator/types.js +225 -0
- package/dist/generator/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/commands/dev.ts +48 -0
- package/src/commands/generate.ts +103 -0
- package/src/commands/init.ts +367 -0
- package/src/generator/context.ts +75 -0
- package/src/generator/index.ts +4 -0
- package/src/generator/prisma.ts +157 -0
- package/src/generator/type-patcher.ts +93 -0
- package/src/generator/types.ts +263 -0
- package/src/index.ts +34 -0
- package/tsconfig.json +13 -0
- 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,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
|
+
}
|