@opensaas/stack-cli 0.5.0 → 0.6.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/README.md +76 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +91 -265
- package/dist/commands/migrate.js.map +1 -1
- package/package.json +7 -2
- package/plugin/.claude-plugin/plugin.json +15 -0
- package/plugin/README.md +112 -0
- package/plugin/agents/migration-assistant.md +150 -0
- package/plugin/commands/analyze-schema.md +34 -0
- package/plugin/commands/generate-config.md +33 -0
- package/plugin/commands/validate-migration.md +34 -0
- package/plugin/skills/opensaas-migration/SKILL.md +192 -0
- 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/generator/types.ts
DELETED
|
@@ -1,537 +0,0 @@
|
|
|
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 TypeScript types
|
|
7
|
-
*/
|
|
8
|
-
function mapFieldTypeToTypeScript(field: FieldConfig): string | null {
|
|
9
|
-
// Relationships are handled separately
|
|
10
|
-
if (field.type === 'relationship') {
|
|
11
|
-
return null
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Use field's own TypeScript type generator if available
|
|
15
|
-
if (field.getTypeScriptType) {
|
|
16
|
-
const result = field.getTypeScriptType()
|
|
17
|
-
return result.type
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Fallback for fields without generator methods
|
|
21
|
-
throw new Error(`Field type "${field.type}" does not implement getTypeScriptType method`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if a field is optional in the type
|
|
26
|
-
*/
|
|
27
|
-
function isFieldOptional(field: FieldConfig): boolean {
|
|
28
|
-
// Relationships are always nullable
|
|
29
|
-
if (field.type === 'relationship') {
|
|
30
|
-
return true
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Use field's own TypeScript type generator if available
|
|
34
|
-
if (field.getTypeScriptType) {
|
|
35
|
-
const result = field.getTypeScriptType()
|
|
36
|
-
return result.optional
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Fallback: assume optional
|
|
40
|
-
return true
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Generate virtual fields type - only contains virtual fields
|
|
45
|
-
* This is intersected with Prisma's GetPayload to add virtual fields to query results
|
|
46
|
-
*/
|
|
47
|
-
function generateVirtualFieldsType(listName: string, fields: Record<string, FieldConfig>): string {
|
|
48
|
-
const lines: string[] = []
|
|
49
|
-
const virtualFields = Object.entries(fields).filter(([_, config]) => config.type === 'virtual')
|
|
50
|
-
|
|
51
|
-
lines.push(`/**`)
|
|
52
|
-
lines.push(` * Virtual fields for ${listName} - computed fields not in database`)
|
|
53
|
-
lines.push(` * These are added to query results via resolveOutput hooks`)
|
|
54
|
-
lines.push(` */`)
|
|
55
|
-
lines.push(`export type ${listName}VirtualFields = {`)
|
|
56
|
-
|
|
57
|
-
for (const [fieldName, fieldConfig] of virtualFields) {
|
|
58
|
-
const tsType = mapFieldTypeToTypeScript(fieldConfig)
|
|
59
|
-
if (!tsType) continue
|
|
60
|
-
|
|
61
|
-
const optional = isFieldOptional(fieldConfig)
|
|
62
|
-
const nullability = optional ? ' | null' : ''
|
|
63
|
-
lines.push(` ${fieldName}: ${tsType}${nullability}`)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// If no virtual fields, make it an empty object
|
|
67
|
-
if (virtualFields.length === 0) {
|
|
68
|
-
lines.push(' // No virtual fields defined')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
lines.push('}')
|
|
72
|
-
|
|
73
|
-
return lines.join('\n')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Generate transformed fields type - fields with resultExtension transformations
|
|
78
|
-
* This replaces Prisma's base types with transformed types (e.g., string -> HashedPassword)
|
|
79
|
-
*/
|
|
80
|
-
function generateTransformedFieldsType(
|
|
81
|
-
listName: string,
|
|
82
|
-
fields: Record<string, FieldConfig>,
|
|
83
|
-
): string {
|
|
84
|
-
const lines: string[] = []
|
|
85
|
-
const transformedFields = Object.entries(fields).filter(([_, config]) => config.resultExtension)
|
|
86
|
-
|
|
87
|
-
lines.push(`/**`)
|
|
88
|
-
lines.push(` * Transformed fields for ${listName} - fields with resultExtension transformations`)
|
|
89
|
-
lines.push(` * These override Prisma's base types with transformed types via result extensions`)
|
|
90
|
-
lines.push(` */`)
|
|
91
|
-
lines.push(`export type ${listName}TransformedFields = {`)
|
|
92
|
-
|
|
93
|
-
for (const [fieldName, fieldConfig] of transformedFields) {
|
|
94
|
-
if (fieldConfig.resultExtension) {
|
|
95
|
-
const optional = isFieldOptional(fieldConfig)
|
|
96
|
-
const nullability = optional ? ' | undefined' : ''
|
|
97
|
-
lines.push(` ${fieldName}: ${fieldConfig.resultExtension.outputType}${nullability}`)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// If no transformed fields, make it an empty object
|
|
102
|
-
if (transformedFields.length === 0) {
|
|
103
|
-
lines.push(' // No transformed fields defined')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
lines.push('}')
|
|
107
|
-
|
|
108
|
-
return lines.join('\n')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Generate TypeScript Output type for a model (includes virtual fields)
|
|
113
|
-
* This is kept for backwards compatibility but CustomDB uses Prisma's GetPayload + VirtualFields
|
|
114
|
-
*/
|
|
115
|
-
function generateModelOutputType(listName: string, fields: Record<string, FieldConfig>): string {
|
|
116
|
-
const lines: string[] = []
|
|
117
|
-
|
|
118
|
-
lines.push(`export type ${listName}Output = {`)
|
|
119
|
-
lines.push(' id: string')
|
|
120
|
-
|
|
121
|
-
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
122
|
-
// Skip virtual fields - they're in VirtualFields type
|
|
123
|
-
if (fieldConfig.type === 'virtual') continue
|
|
124
|
-
|
|
125
|
-
if (fieldConfig.type === 'relationship') {
|
|
126
|
-
const relField = fieldConfig as RelationshipField
|
|
127
|
-
const [targetList] = relField.ref.split('.')
|
|
128
|
-
|
|
129
|
-
if (relField.many) {
|
|
130
|
-
lines.push(` ${fieldName}?: ${targetList}Output[]`) // Optional since only present with include
|
|
131
|
-
} else {
|
|
132
|
-
lines.push(` ${fieldName}Id: string | null`)
|
|
133
|
-
lines.push(` ${fieldName}?: ${targetList}Output | null`) // Optional since only present with include
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
const tsType = mapFieldTypeToTypeScript(fieldConfig)
|
|
137
|
-
if (!tsType) continue // Skip if no type returned
|
|
138
|
-
|
|
139
|
-
const optional = isFieldOptional(fieldConfig)
|
|
140
|
-
const nullability = optional ? ' | null' : ''
|
|
141
|
-
lines.push(` ${fieldName}: ${tsType}${nullability}`)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
lines.push(' createdAt: Date')
|
|
146
|
-
lines.push(' updatedAt: Date')
|
|
147
|
-
lines.push('} & ' + listName + 'VirtualFields') // Include virtual fields
|
|
148
|
-
|
|
149
|
-
return lines.join('\n')
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Generate convenience type alias (List = ListOutput)
|
|
154
|
-
*/
|
|
155
|
-
function generateModelTypeAlias(listName: string): string {
|
|
156
|
-
return `export type ${listName} = ${listName}Output`
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Generate CreateInput type
|
|
161
|
-
*/
|
|
162
|
-
function generateCreateInputType(listName: string, fields: Record<string, FieldConfig>): string {
|
|
163
|
-
const lines: string[] = []
|
|
164
|
-
|
|
165
|
-
lines.push(`export type ${listName}CreateInput = {`)
|
|
166
|
-
|
|
167
|
-
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
168
|
-
// Skip virtual fields - they don't accept input in create operations
|
|
169
|
-
// Virtual fields with resolveInput hooks handle side effects but don't store data
|
|
170
|
-
if (fieldConfig.virtual) {
|
|
171
|
-
continue
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (fieldConfig.type === 'relationship') {
|
|
175
|
-
const relField = fieldConfig as RelationshipField
|
|
176
|
-
|
|
177
|
-
if (relField.many) {
|
|
178
|
-
lines.push(` ${fieldName}?: { connect: Array<{ id: string }> }`)
|
|
179
|
-
} else {
|
|
180
|
-
lines.push(` ${fieldName}?: { connect: { id: string } }`)
|
|
181
|
-
}
|
|
182
|
-
} else {
|
|
183
|
-
const tsType = mapFieldTypeToTypeScript(fieldConfig)
|
|
184
|
-
if (!tsType) continue // Skip if no type returned
|
|
185
|
-
|
|
186
|
-
const required = !isFieldOptional(fieldConfig) && !fieldConfig.defaultValue
|
|
187
|
-
const optional = required ? '' : '?'
|
|
188
|
-
lines.push(` ${fieldName}${optional}: ${tsType}`)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
lines.push('}')
|
|
193
|
-
|
|
194
|
-
return lines.join('\n')
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Generate UpdateInput type
|
|
199
|
-
*/
|
|
200
|
-
function generateUpdateInputType(listName: string, fields: Record<string, FieldConfig>): string {
|
|
201
|
-
const lines: string[] = []
|
|
202
|
-
|
|
203
|
-
lines.push(`export type ${listName}UpdateInput = {`)
|
|
204
|
-
|
|
205
|
-
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
206
|
-
// Virtual fields with resolveInput hooks can accept input for side effects
|
|
207
|
-
// but we still skip them in the input type since they don't store data
|
|
208
|
-
if (fieldConfig.virtual) {
|
|
209
|
-
continue
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (fieldConfig.type === 'relationship') {
|
|
213
|
-
const relField = fieldConfig as RelationshipField
|
|
214
|
-
|
|
215
|
-
if (relField.many) {
|
|
216
|
-
lines.push(
|
|
217
|
-
` ${fieldName}?: { connect: Array<{ id: string }>, disconnect: Array<{ id: string }> }`,
|
|
218
|
-
)
|
|
219
|
-
} else {
|
|
220
|
-
lines.push(` ${fieldName}?: { connect: { id: string } } | { disconnect: true }`)
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
const tsType = mapFieldTypeToTypeScript(fieldConfig)
|
|
224
|
-
if (!tsType) continue // Skip if no type returned
|
|
225
|
-
|
|
226
|
-
lines.push(` ${fieldName}?: ${tsType}`)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
lines.push('}')
|
|
231
|
-
|
|
232
|
-
return lines.join('\n')
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Generate WhereInput type (simplified)
|
|
237
|
-
*/
|
|
238
|
-
function generateWhereInputType(listName: string, fields: Record<string, FieldConfig>): string {
|
|
239
|
-
const lines: string[] = []
|
|
240
|
-
|
|
241
|
-
lines.push(`export type ${listName}WhereInput = {`)
|
|
242
|
-
lines.push(' id?: string')
|
|
243
|
-
lines.push(' AND?: Array<' + listName + 'WhereInput>')
|
|
244
|
-
lines.push(' OR?: Array<' + listName + 'WhereInput>')
|
|
245
|
-
lines.push(' NOT?: ' + listName + 'WhereInput')
|
|
246
|
-
|
|
247
|
-
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
248
|
-
if (fieldConfig.type === 'relationship') {
|
|
249
|
-
continue // Skip for now
|
|
250
|
-
} else {
|
|
251
|
-
const tsType = mapFieldTypeToTypeScript(fieldConfig)
|
|
252
|
-
if (!tsType) continue // Skip if no type returned
|
|
253
|
-
|
|
254
|
-
lines.push(` ${fieldName}?: { equals?: ${tsType}, not?: ${tsType} }`)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
lines.push('}')
|
|
259
|
-
|
|
260
|
-
return lines.join('\n')
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Generate hook types that reference Prisma input types
|
|
265
|
-
*/
|
|
266
|
-
function generateHookTypes(listName: string): string {
|
|
267
|
-
const lines: string[] = []
|
|
268
|
-
|
|
269
|
-
lines.push(`/**`)
|
|
270
|
-
lines.push(` * Hook types for ${listName} list`)
|
|
271
|
-
lines.push(` * Properly typed to use Prisma's generated input types`)
|
|
272
|
-
lines.push(` */`)
|
|
273
|
-
lines.push(`export type ${listName}Hooks = {`)
|
|
274
|
-
lines.push(` resolveInput?: (args:`)
|
|
275
|
-
lines.push(` | {`)
|
|
276
|
-
lines.push(` operation: 'create'`)
|
|
277
|
-
lines.push(` resolvedData: Prisma.${listName}CreateInput`)
|
|
278
|
-
lines.push(` item: undefined`)
|
|
279
|
-
lines.push(` context: import('@opensaas/stack-core').AccessContext`)
|
|
280
|
-
lines.push(` }`)
|
|
281
|
-
lines.push(` | {`)
|
|
282
|
-
lines.push(` operation: 'update'`)
|
|
283
|
-
lines.push(` resolvedData: Prisma.${listName}UpdateInput`)
|
|
284
|
-
lines.push(` item: ${listName}`)
|
|
285
|
-
lines.push(` context: import('@opensaas/stack-core').AccessContext`)
|
|
286
|
-
lines.push(` }`)
|
|
287
|
-
lines.push(` ) => Promise<Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput>`)
|
|
288
|
-
lines.push(` validateInput?: (args: {`)
|
|
289
|
-
lines.push(` operation: 'create' | 'update'`)
|
|
290
|
-
lines.push(` resolvedData: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
|
|
291
|
-
lines.push(` item?: ${listName}`)
|
|
292
|
-
lines.push(` context: import('@opensaas/stack-core').AccessContext`)
|
|
293
|
-
lines.push(` addValidationError: (msg: string) => void`)
|
|
294
|
-
lines.push(` }) => Promise<void>`)
|
|
295
|
-
lines.push(` beforeOperation?: (args: {`)
|
|
296
|
-
lines.push(` operation: 'create' | 'update' | 'delete'`)
|
|
297
|
-
lines.push(` resolvedData?: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
|
|
298
|
-
lines.push(` item?: ${listName}`)
|
|
299
|
-
lines.push(` context: import('@opensaas/stack-core').AccessContext`)
|
|
300
|
-
lines.push(` }) => Promise<void>`)
|
|
301
|
-
lines.push(` afterOperation?: (args: {`)
|
|
302
|
-
lines.push(` operation: 'create' | 'update' | 'delete'`)
|
|
303
|
-
lines.push(` resolvedData?: Prisma.${listName}CreateInput | Prisma.${listName}UpdateInput`)
|
|
304
|
-
lines.push(` item?: ${listName}`)
|
|
305
|
-
lines.push(` context: import('@opensaas/stack-core').AccessContext`)
|
|
306
|
-
lines.push(` }) => Promise<void>`)
|
|
307
|
-
lines.push(`}`)
|
|
308
|
-
|
|
309
|
-
return lines.join('\n')
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Generate custom DB interface that uses Prisma's conditional types with virtual and transformed fields
|
|
314
|
-
* This leverages Prisma's GetPayload utility to get correct types based on select/include
|
|
315
|
-
*/
|
|
316
|
-
function generateCustomDBType(config: OpenSaasConfig): string {
|
|
317
|
-
const lines: string[] = []
|
|
318
|
-
|
|
319
|
-
// Generate list of db keys to omit from AccessControlledDB
|
|
320
|
-
const dbKeys = Object.keys(config.lists).map((listName) => {
|
|
321
|
-
const dbKey = listName.charAt(0).toLowerCase() + listName.slice(1)
|
|
322
|
-
return `'${dbKey}'`
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
lines.push('/**')
|
|
326
|
-
lines.push(
|
|
327
|
-
" * Custom DB type that uses Prisma's conditional types with virtual and transformed field support",
|
|
328
|
-
)
|
|
329
|
-
lines.push(
|
|
330
|
-
' * Types change based on select/include - relationships only present when explicitly included',
|
|
331
|
-
)
|
|
332
|
-
lines.push(' * Virtual fields and transformed fields are added to the base model type')
|
|
333
|
-
lines.push(' */')
|
|
334
|
-
lines.push('export type CustomDB = Omit<AccessControlledDB<PrismaClient>, ')
|
|
335
|
-
lines.push(` ${dbKeys.join(' | ')}`)
|
|
336
|
-
lines.push('> & {')
|
|
337
|
-
|
|
338
|
-
// For each list, create strongly-typed methods using Prisma's conditional types
|
|
339
|
-
for (const listName of Object.keys(config.lists)) {
|
|
340
|
-
const dbKey = listName.charAt(0).toLowerCase() + listName.slice(1) // camelCase
|
|
341
|
-
|
|
342
|
-
lines.push(` ${dbKey}: {`)
|
|
343
|
-
|
|
344
|
-
// findUnique - generic to preserve Prisma's conditional return type
|
|
345
|
-
lines.push(` findUnique: <T extends Prisma.${listName}FindUniqueArgs>(`)
|
|
346
|
-
lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}FindUniqueArgs>`)
|
|
347
|
-
lines.push(
|
|
348
|
-
` ) => Promise<(Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields) | null>`,
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
// findMany - generic to preserve Prisma's conditional return type
|
|
352
|
-
lines.push(` findMany: <T extends Prisma.${listName}FindManyArgs>(`)
|
|
353
|
-
lines.push(` args?: Prisma.SelectSubset<T, Prisma.${listName}FindManyArgs>`)
|
|
354
|
-
lines.push(
|
|
355
|
-
` ) => Promise<Array<Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields>>`,
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
// create - generic to preserve Prisma's conditional return type
|
|
359
|
-
lines.push(` create: <T extends Prisma.${listName}CreateArgs>(`)
|
|
360
|
-
lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}CreateArgs>`)
|
|
361
|
-
lines.push(
|
|
362
|
-
` ) => Promise<Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields>`,
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
// update - generic to preserve Prisma's conditional return type
|
|
366
|
-
lines.push(` update: <T extends Prisma.${listName}UpdateArgs>(`)
|
|
367
|
-
lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}UpdateArgs>`)
|
|
368
|
-
lines.push(
|
|
369
|
-
` ) => Promise<(Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields) | null>`,
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
// delete - generic to preserve Prisma's conditional return type
|
|
373
|
-
lines.push(` delete: <T extends Prisma.${listName}DeleteArgs>(`)
|
|
374
|
-
lines.push(` args: Prisma.SelectSubset<T, Prisma.${listName}DeleteArgs>`)
|
|
375
|
-
lines.push(
|
|
376
|
-
` ) => Promise<(Omit<Prisma.${listName}GetPayload<T>, keyof ${listName}TransformedFields> & ${listName}TransformedFields & ${listName}VirtualFields) | null>`,
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
// count - no changes to return type
|
|
380
|
-
lines.push(` count: (args?: Prisma.${listName}CountArgs) => Promise<number>`)
|
|
381
|
-
|
|
382
|
-
lines.push(` }`)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
lines.push('}')
|
|
386
|
-
|
|
387
|
-
return lines.join('\n')
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Generate Context type that is compatible with AccessContext
|
|
392
|
-
*/
|
|
393
|
-
function generateContextType(_config: OpenSaasConfig): string {
|
|
394
|
-
const lines: string[] = []
|
|
395
|
-
|
|
396
|
-
lines.push('/**')
|
|
397
|
-
lines.push(
|
|
398
|
-
' * Context type compatible with AccessContext but with CustomDB for virtual field typing',
|
|
399
|
-
)
|
|
400
|
-
lines.push(
|
|
401
|
-
' * Extends AccessContext and overrides db property to include virtual fields in output types',
|
|
402
|
-
)
|
|
403
|
-
lines.push(' */')
|
|
404
|
-
lines.push(
|
|
405
|
-
"export type Context<TSession extends OpensaasSession = OpensaasSession> = Omit<AccessContext<PrismaClient>, 'db' | 'session'> & {",
|
|
406
|
-
)
|
|
407
|
-
lines.push(' db: CustomDB')
|
|
408
|
-
lines.push(' session: TSession')
|
|
409
|
-
lines.push(' serverAction: (props: ServerActionProps) => Promise<unknown>')
|
|
410
|
-
lines.push(' sudo: () => Context<TSession>')
|
|
411
|
-
lines.push('}')
|
|
412
|
-
|
|
413
|
-
return lines.join('\n')
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Collect TypeScript imports from field configurations
|
|
418
|
-
*/
|
|
419
|
-
function collectFieldImports(config: OpenSaasConfig): Array<{
|
|
420
|
-
names: string[]
|
|
421
|
-
from: string
|
|
422
|
-
typeOnly: boolean
|
|
423
|
-
}> {
|
|
424
|
-
const importsMap = new Map<string, { names: Set<string>; typeOnly: boolean }>()
|
|
425
|
-
|
|
426
|
-
// Iterate through all lists and fields
|
|
427
|
-
for (const listConfig of Object.values(config.lists)) {
|
|
428
|
-
for (const fieldConfig of Object.values(listConfig.fields)) {
|
|
429
|
-
// Check if field provides imports
|
|
430
|
-
if (fieldConfig.getTypeScriptImports) {
|
|
431
|
-
const imports = fieldConfig.getTypeScriptImports()
|
|
432
|
-
for (const imp of imports) {
|
|
433
|
-
const existing = importsMap.get(imp.from)
|
|
434
|
-
if (existing) {
|
|
435
|
-
// Merge names into existing import
|
|
436
|
-
imp.names.forEach((name) => existing.names.add(name))
|
|
437
|
-
// If either import is not type-only, make the merged import not type-only
|
|
438
|
-
if (imp.typeOnly === false) {
|
|
439
|
-
existing.typeOnly = false
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
// Add new import
|
|
443
|
-
importsMap.set(imp.from, {
|
|
444
|
-
names: new Set(imp.names),
|
|
445
|
-
typeOnly: imp.typeOnly ?? true,
|
|
446
|
-
})
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Convert map to array
|
|
454
|
-
return Array.from(importsMap.entries()).map(([from, { names, typeOnly }]) => ({
|
|
455
|
-
names: Array.from(names).sort(),
|
|
456
|
-
from,
|
|
457
|
-
typeOnly,
|
|
458
|
-
}))
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Generate all TypeScript types from config
|
|
463
|
-
*/
|
|
464
|
-
export function generateTypes(config: OpenSaasConfig): string {
|
|
465
|
-
const lines: string[] = []
|
|
466
|
-
|
|
467
|
-
// Add header comment
|
|
468
|
-
lines.push('/**')
|
|
469
|
-
lines.push(' * Generated types from OpenSaas configuration')
|
|
470
|
-
lines.push(' * DO NOT EDIT - This file is automatically generated')
|
|
471
|
-
lines.push(' */')
|
|
472
|
-
lines.push('')
|
|
473
|
-
|
|
474
|
-
// Add necessary imports
|
|
475
|
-
// Use alias for Session to avoid conflicts if user has a list named "Session"
|
|
476
|
-
lines.push(
|
|
477
|
-
"import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB, AccessContext } from '@opensaas/stack-core'",
|
|
478
|
-
)
|
|
479
|
-
lines.push("import type { PrismaClient, Prisma } from './prisma-client/client'")
|
|
480
|
-
lines.push("import type { PluginServices } from './plugin-types'")
|
|
481
|
-
|
|
482
|
-
// Add field-specific imports
|
|
483
|
-
const fieldImports = collectFieldImports(config)
|
|
484
|
-
for (const imp of fieldImports) {
|
|
485
|
-
const typePrefix = imp.typeOnly ? 'type ' : ''
|
|
486
|
-
const names = imp.names.join(', ')
|
|
487
|
-
lines.push(`import ${typePrefix}{ ${names} } from '${imp.from}'`)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
lines.push('')
|
|
491
|
-
|
|
492
|
-
// Generate types for each list
|
|
493
|
-
for (const [listName, listConfig] of Object.entries(config.lists)) {
|
|
494
|
-
// Generate VirtualFields type first (needed by Output type and CustomDB)
|
|
495
|
-
lines.push(generateVirtualFieldsType(listName, listConfig.fields))
|
|
496
|
-
lines.push('')
|
|
497
|
-
// Generate TransformedFields type (needed by CustomDB)
|
|
498
|
-
lines.push(generateTransformedFieldsType(listName, listConfig.fields))
|
|
499
|
-
lines.push('')
|
|
500
|
-
lines.push(generateModelOutputType(listName, listConfig.fields))
|
|
501
|
-
lines.push('')
|
|
502
|
-
lines.push(generateModelTypeAlias(listName))
|
|
503
|
-
lines.push('')
|
|
504
|
-
lines.push(generateCreateInputType(listName, listConfig.fields))
|
|
505
|
-
lines.push('')
|
|
506
|
-
lines.push(generateUpdateInputType(listName, listConfig.fields))
|
|
507
|
-
lines.push('')
|
|
508
|
-
lines.push(generateWhereInputType(listName, listConfig.fields))
|
|
509
|
-
lines.push('')
|
|
510
|
-
lines.push(generateHookTypes(listName))
|
|
511
|
-
lines.push('')
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Generate CustomDB interface
|
|
515
|
-
lines.push(generateCustomDBType(config))
|
|
516
|
-
lines.push('')
|
|
517
|
-
|
|
518
|
-
// Generate Context type
|
|
519
|
-
lines.push(generateContextType(config))
|
|
520
|
-
|
|
521
|
-
return lines.join('\n')
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Write TypeScript types to file
|
|
526
|
-
*/
|
|
527
|
-
export function writeTypes(config: OpenSaasConfig, outputPath: string): void {
|
|
528
|
-
const types = generateTypes(config)
|
|
529
|
-
|
|
530
|
-
// Ensure directory exists
|
|
531
|
-
const dir = path.dirname(outputPath)
|
|
532
|
-
if (!fs.existsSync(dir)) {
|
|
533
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
fs.writeFileSync(outputPath, types, 'utf-8')
|
|
537
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander'
|
|
4
|
-
import { generateCommand } from './commands/generate.js'
|
|
5
|
-
import { initCommand } from './commands/init.js'
|
|
6
|
-
import { devCommand } from './commands/dev.js'
|
|
7
|
-
import { createMCPCommand } from './commands/mcp.js'
|
|
8
|
-
import { createMigrateCommand } from './commands/migrate.js'
|
|
9
|
-
|
|
10
|
-
const program = new Command()
|
|
11
|
-
|
|
12
|
-
program.name('opensaas').description('OpenSaas Stack CLI').version('0.1.0')
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.command('generate')
|
|
16
|
-
.description('Generate Prisma schema and TypeScript types from opensaas.config.ts')
|
|
17
|
-
.action(async () => {
|
|
18
|
-
await generateCommand()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
program
|
|
22
|
-
.command('init [project-name]')
|
|
23
|
-
.description('Create a new OpenSaas project (delegates to create-opensaas-app)')
|
|
24
|
-
.option('--with-auth', 'Include authentication (Better-auth)')
|
|
25
|
-
.allowUnknownOption() // Allow passing through other options
|
|
26
|
-
.action(async (projectName, options) => {
|
|
27
|
-
const args = []
|
|
28
|
-
if (projectName) args.push(projectName)
|
|
29
|
-
if (options.withAuth) args.push('--with-auth')
|
|
30
|
-
await initCommand(args)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
program
|
|
34
|
-
.command('dev')
|
|
35
|
-
.description('Watch opensaas.config.ts and regenerate on changes')
|
|
36
|
-
.action(async () => {
|
|
37
|
-
await devCommand()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
// Add MCP command group
|
|
41
|
-
program.addCommand(createMCPCommand())
|
|
42
|
-
|
|
43
|
-
// Add migrate command
|
|
44
|
-
program.addCommand(createMigrateCommand())
|
|
45
|
-
|
|
46
|
-
program.parse()
|