@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/generator/lists.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Map field type string to TypeScript field type name
|
|
7
|
-
*/
|
|
8
|
-
function getFieldTypeName(fieldType: string): string {
|
|
9
|
-
const typeMap: Record<string, string> = {
|
|
10
|
-
text: 'TextField',
|
|
11
|
-
integer: 'IntegerField',
|
|
12
|
-
checkbox: 'CheckboxField',
|
|
13
|
-
timestamp: 'TimestampField',
|
|
14
|
-
password: 'PasswordField',
|
|
15
|
-
select: 'SelectField',
|
|
16
|
-
relationship: 'RelationshipField',
|
|
17
|
-
json: 'JsonField',
|
|
18
|
-
virtual: 'VirtualField',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return typeMap[fieldType] || 'BaseFieldConfig'
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Generate Lists namespace with TypeInfo for each list
|
|
26
|
-
* This provides strongly-typed hooks with Prisma input types
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```typescript
|
|
30
|
-
* // Generated output:
|
|
31
|
-
* export declare namespace Lists {
|
|
32
|
-
* export type Post = import('@opensaas/stack-core').ListConfig<Lists.Post.TypeInfo>
|
|
33
|
-
*
|
|
34
|
-
* namespace Post {
|
|
35
|
-
* export type Item = import('./types').Post
|
|
36
|
-
* export type TypeInfo = {
|
|
37
|
-
* key: 'Post'
|
|
38
|
-
* item: Item
|
|
39
|
-
* inputs: {
|
|
40
|
-
* create: import('./prisma-client/client').Prisma.PostCreateInput
|
|
41
|
-
* update: import('./prisma-client/client').Prisma.PostUpdateInput
|
|
42
|
-
* }
|
|
43
|
-
* }
|
|
44
|
-
* }
|
|
45
|
-
* }
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
export function generateListsNamespace(config: OpenSaasConfig): string {
|
|
49
|
-
const lines: string[] = []
|
|
50
|
-
|
|
51
|
-
// Add header comment
|
|
52
|
-
lines.push('/**')
|
|
53
|
-
lines.push(' * Generated Lists namespace from OpenSaas configuration')
|
|
54
|
-
lines.push(' * DO NOT EDIT - This file is automatically generated')
|
|
55
|
-
lines.push(' *')
|
|
56
|
-
lines.push(' * This file provides TypeInfo for each list, enabling strong typing')
|
|
57
|
-
lines.push(' * for hooks with Prisma input types.')
|
|
58
|
-
lines.push(' *')
|
|
59
|
-
lines.push(' * @example')
|
|
60
|
-
lines.push(' * ```typescript')
|
|
61
|
-
lines.push(" * import type { Lists } from './.opensaas/lists'")
|
|
62
|
-
lines.push(' *')
|
|
63
|
-
lines.push(' * // Use TypeInfo as generic parameter')
|
|
64
|
-
lines.push(' * Post: list<Lists.Post.TypeInfo>({')
|
|
65
|
-
lines.push(' * hooks: {')
|
|
66
|
-
lines.push(' * resolveInput: async ({ operation, resolvedData }) => {')
|
|
67
|
-
lines.push(' * // resolvedData is Prisma.PostCreateInput or Prisma.PostUpdateInput')
|
|
68
|
-
lines.push(' * return resolvedData')
|
|
69
|
-
lines.push(' * }')
|
|
70
|
-
lines.push(' * }')
|
|
71
|
-
lines.push(' * })')
|
|
72
|
-
lines.push(' *')
|
|
73
|
-
lines.push(' * // Or use as typed constant')
|
|
74
|
-
lines.push(' * const Post: Lists.Post = list({ ... })')
|
|
75
|
-
lines.push(' * ```')
|
|
76
|
-
lines.push(' */')
|
|
77
|
-
lines.push('')
|
|
78
|
-
|
|
79
|
-
// Start Lists namespace
|
|
80
|
-
lines.push('export declare namespace Lists {')
|
|
81
|
-
|
|
82
|
-
// Generate type for each list
|
|
83
|
-
for (const [listName, listConfig] of Object.entries(config.lists)) {
|
|
84
|
-
lines.push(
|
|
85
|
-
` export type ${listName} = import('@opensaas/stack-core').ListConfig<Lists.${listName}.TypeInfo>`,
|
|
86
|
-
)
|
|
87
|
-
lines.push('')
|
|
88
|
-
lines.push(` namespace ${listName} {`)
|
|
89
|
-
lines.push(` export type Item = import('./types').${listName}`)
|
|
90
|
-
lines.push('')
|
|
91
|
-
|
|
92
|
-
// Generate Fields type
|
|
93
|
-
lines.push(` /**`)
|
|
94
|
-
lines.push(` * Field configurations for ${listName}`)
|
|
95
|
-
lines.push(` * Maps field names to their field config types`)
|
|
96
|
-
lines.push(` */`)
|
|
97
|
-
lines.push(` export type Fields = {`)
|
|
98
|
-
for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
|
|
99
|
-
const fieldTypeName = getFieldTypeName(fieldConfig.type)
|
|
100
|
-
lines.push(
|
|
101
|
-
` ${fieldName}: import('@opensaas/stack-core').${fieldTypeName}<Lists.${listName}.TypeInfo>`,
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
lines.push(` }`)
|
|
105
|
-
lines.push('')
|
|
106
|
-
|
|
107
|
-
// Generate TypeInfo with fields property
|
|
108
|
-
lines.push(` export type TypeInfo = {`)
|
|
109
|
-
lines.push(` key: '${listName}'`)
|
|
110
|
-
lines.push(` fields: Fields`)
|
|
111
|
-
lines.push(` item: Item`)
|
|
112
|
-
lines.push(` inputs: {`)
|
|
113
|
-
lines.push(` create: import('./prisma-client/client').Prisma.${listName}CreateInput`)
|
|
114
|
-
lines.push(` update: import('./prisma-client/client').Prisma.${listName}UpdateInput`)
|
|
115
|
-
lines.push(` }`)
|
|
116
|
-
lines.push(` }`)
|
|
117
|
-
lines.push(` }`)
|
|
118
|
-
lines.push('')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Close Lists namespace
|
|
122
|
-
lines.push('}')
|
|
123
|
-
|
|
124
|
-
return lines.join('\n')
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Write Lists namespace to file
|
|
129
|
-
*/
|
|
130
|
-
export function writeLists(config: OpenSaasConfig, outputPath: string): void {
|
|
131
|
-
const lists = generateListsNamespace(config)
|
|
132
|
-
|
|
133
|
-
// Ensure directory exists
|
|
134
|
-
const dir = path.dirname(outputPath)
|
|
135
|
-
if (!fs.existsSync(dir)) {
|
|
136
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
fs.writeFileSync(outputPath, lists, 'utf-8')
|
|
140
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Generate TypeScript declaration for plugin data types
|
|
7
|
-
* Creates type-safe access to config._pluginData
|
|
8
|
-
*/
|
|
9
|
-
function generatePluginDataInterface(config: OpenSaasConfig): string {
|
|
10
|
-
const lines: string[] = []
|
|
11
|
-
|
|
12
|
-
lines.push('/**')
|
|
13
|
-
lines.push(' * Plugin data storage types')
|
|
14
|
-
lines.push(' * Provides type-safe access to config._pluginData')
|
|
15
|
-
lines.push(' */')
|
|
16
|
-
lines.push('export interface PluginData {')
|
|
17
|
-
|
|
18
|
-
// Check if we have plugin data types to generate
|
|
19
|
-
if (config._pluginData && Object.keys(config._pluginData).length > 0) {
|
|
20
|
-
for (const pluginName of Object.keys(config._pluginData)) {
|
|
21
|
-
// Skip internal keys
|
|
22
|
-
if (pluginName.startsWith('__')) continue
|
|
23
|
-
|
|
24
|
-
// For each plugin, we'll add a Record<string, unknown> entry
|
|
25
|
-
// TODO: In the future, plugins could export their data types for proper typing
|
|
26
|
-
lines.push(` ${pluginName}?: Record<string, unknown>`)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
lines.push('}')
|
|
31
|
-
|
|
32
|
-
return lines.join('\n')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Generate TypeScript declaration for plugin runtime services
|
|
37
|
-
* Creates type-safe access to context.plugins
|
|
38
|
-
*/
|
|
39
|
-
function generatePluginServicesInterface(config: OpenSaasConfig): string {
|
|
40
|
-
const lines: string[] = []
|
|
41
|
-
const imports: string[] = []
|
|
42
|
-
|
|
43
|
-
// Check if we have plugins with runtime functions
|
|
44
|
-
const pluginsWithRuntime = (config._plugins || config.plugins || []).filter((p) => p.runtime)
|
|
45
|
-
|
|
46
|
-
// Collect imports from plugins that provide runtime service types
|
|
47
|
-
if (pluginsWithRuntime.length > 0) {
|
|
48
|
-
for (const plugin of pluginsWithRuntime) {
|
|
49
|
-
if (plugin.runtimeServiceTypes) {
|
|
50
|
-
imports.push(plugin.runtimeServiceTypes.import)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Add imports at the top if any exist
|
|
56
|
-
if (imports.length > 0) {
|
|
57
|
-
lines.push(...imports)
|
|
58
|
-
lines.push('')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
lines.push('/**')
|
|
62
|
-
lines.push(' * Plugin runtime services')
|
|
63
|
-
lines.push(' * Provides type-safe access to context.plugins')
|
|
64
|
-
lines.push(' * Extends Record to allow compatibility with base AccessContext type')
|
|
65
|
-
lines.push(' */')
|
|
66
|
-
lines.push(
|
|
67
|
-
'export interface PluginServices extends Record<string, Record<string, any> | undefined> {',
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
if (pluginsWithRuntime.length > 0) {
|
|
71
|
-
for (const plugin of pluginsWithRuntime) {
|
|
72
|
-
if (plugin.runtimeServiceTypes) {
|
|
73
|
-
// Use typed runtime service from plugin
|
|
74
|
-
lines.push(` ${plugin.name}?: ${plugin.runtimeServiceTypes.typeName}`)
|
|
75
|
-
} else {
|
|
76
|
-
// Fallback to Record<string, any> for plugins without type metadata
|
|
77
|
-
lines.push(` ${plugin.name}?: Record<string, any>`)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
lines.push('}')
|
|
83
|
-
|
|
84
|
-
return lines.join('\n')
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Generate module augmentation for OpenSaaS core types
|
|
89
|
-
* Note: We cannot augment _pluginData or plugins properties directly due to type constraints
|
|
90
|
-
* Instead, users should cast to PluginServices when accessing context.plugins
|
|
91
|
-
*/
|
|
92
|
-
function generateModuleAugmentation(): string {
|
|
93
|
-
const lines: string[] = []
|
|
94
|
-
|
|
95
|
-
lines.push('')
|
|
96
|
-
lines.push('/**')
|
|
97
|
-
lines.push(' * Declare this module to make interfaces available globally')
|
|
98
|
-
lines.push(' * Import this file to get typed plugin access')
|
|
99
|
-
lines.push(' */')
|
|
100
|
-
lines.push('declare global {')
|
|
101
|
-
lines.push(' // Plugin types are available as PluginServices interface')
|
|
102
|
-
lines.push('}')
|
|
103
|
-
|
|
104
|
-
return lines.join('\n')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Generate complete plugin types file
|
|
109
|
-
*/
|
|
110
|
-
export function generatePluginTypes(config: OpenSaasConfig): string {
|
|
111
|
-
const lines: string[] = []
|
|
112
|
-
|
|
113
|
-
// Add header comment
|
|
114
|
-
lines.push('/**')
|
|
115
|
-
lines.push(' * Generated plugin types from OpenSaas configuration')
|
|
116
|
-
lines.push(' * DO NOT EDIT - This file is automatically generated')
|
|
117
|
-
lines.push(' *')
|
|
118
|
-
lines.push(' * This file provides type-safe access to:')
|
|
119
|
-
lines.push(' * - config._pluginData - Plugin configuration data')
|
|
120
|
-
lines.push(' * - context.plugins - Plugin runtime services')
|
|
121
|
-
lines.push(' */')
|
|
122
|
-
lines.push('')
|
|
123
|
-
|
|
124
|
-
// Generate interfaces
|
|
125
|
-
lines.push(generatePluginDataInterface(config))
|
|
126
|
-
lines.push(generatePluginServicesInterface(config))
|
|
127
|
-
|
|
128
|
-
// Generate module augmentation
|
|
129
|
-
lines.push(generateModuleAugmentation())
|
|
130
|
-
|
|
131
|
-
return lines.join('\n')
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Write plugin types to file
|
|
136
|
-
*/
|
|
137
|
-
export function writePluginTypes(config: OpenSaasConfig, outputPath: string): void {
|
|
138
|
-
const pluginTypes = generatePluginTypes(config)
|
|
139
|
-
|
|
140
|
-
// Ensure directory exists
|
|
141
|
-
const dir = path.dirname(outputPath)
|
|
142
|
-
if (!fs.existsSync(dir)) {
|
|
143
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
fs.writeFileSync(outputPath, pluginTypes, 'utf-8')
|
|
147
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Generate Prisma config file for CLI commands
|
|
7
|
-
*
|
|
8
|
-
* Prisma 7 requires a prisma.config.ts file at the project root for CLI commands
|
|
9
|
-
* like `prisma db push` and `prisma migrate dev`. This is separate from the
|
|
10
|
-
* runtime configuration (which uses adapters in opensaas.config.ts).
|
|
11
|
-
*
|
|
12
|
-
* The CLI config provides the database URL for schema operations, while the
|
|
13
|
-
* runtime config provides adapters for actual query execution.
|
|
14
|
-
*/
|
|
15
|
-
export function generatePrismaConfig(_config: OpenSaasConfig): string {
|
|
16
|
-
const lines: string[] = []
|
|
17
|
-
|
|
18
|
-
// Import dotenv for environment variable loading
|
|
19
|
-
lines.push("import 'dotenv/config'")
|
|
20
|
-
lines.push("import { defineConfig, env } from 'prisma/config'")
|
|
21
|
-
lines.push('')
|
|
22
|
-
lines.push('export default defineConfig({')
|
|
23
|
-
lines.push(" schema: 'prisma/schema.prisma',")
|
|
24
|
-
lines.push(' datasource: {')
|
|
25
|
-
lines.push(" url: env('DATABASE_URL'),")
|
|
26
|
-
lines.push(' },')
|
|
27
|
-
lines.push('})')
|
|
28
|
-
lines.push('')
|
|
29
|
-
|
|
30
|
-
return lines.join('\n')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Write Prisma config to file
|
|
35
|
-
*/
|
|
36
|
-
export function writePrismaConfig(config: OpenSaasConfig, outputPath: string): void {
|
|
37
|
-
const prismaConfig = generatePrismaConfig(config)
|
|
38
|
-
|
|
39
|
-
// Ensure directory exists
|
|
40
|
-
const dir = path.dirname(outputPath)
|
|
41
|
-
if (!fs.existsSync(dir)) {
|
|
42
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
fs.writeFileSync(outputPath, prismaConfig, 'utf-8')
|
|
46
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import type { OpenSaasConfig, FieldConfig } from '@opensaas/stack-core'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Generate Prisma result extensions configuration
|
|
7
|
-
* This creates a Prisma client extension that calls field resolveOutput hooks
|
|
8
|
-
*/
|
|
9
|
-
export function generatePrismaExtensions(config: OpenSaasConfig): string {
|
|
10
|
-
const lines: string[] = []
|
|
11
|
-
|
|
12
|
-
// Add header comment
|
|
13
|
-
lines.push('/**')
|
|
14
|
-
lines.push(' * Generated Prisma result extensions from OpenSaas configuration')
|
|
15
|
-
lines.push(' * DO NOT EDIT - This file is automatically generated')
|
|
16
|
-
lines.push(' */')
|
|
17
|
-
lines.push('')
|
|
18
|
-
|
|
19
|
-
// Add imports
|
|
20
|
-
lines.push("import { Prisma } from './prisma-client/client'")
|
|
21
|
-
lines.push("import configOrPromise from '../opensaas.config'")
|
|
22
|
-
lines.push('')
|
|
23
|
-
|
|
24
|
-
// Resolve config synchronously if possible (will be resolved by context.ts anyway)
|
|
25
|
-
lines.push('// Resolve config - may be a promise if plugins are present')
|
|
26
|
-
lines.push('let resolvedConfig: any = null')
|
|
27
|
-
lines.push('const configPromise = Promise.resolve(configOrPromise)')
|
|
28
|
-
lines.push('configPromise.then(cfg => { resolvedConfig = cfg })')
|
|
29
|
-
lines.push('')
|
|
30
|
-
|
|
31
|
-
// Check if any fields have result extensions or are virtual
|
|
32
|
-
let hasExtensions = false
|
|
33
|
-
for (const listConfig of Object.values(config.lists)) {
|
|
34
|
-
for (const fieldConfig of Object.values(listConfig.fields)) {
|
|
35
|
-
if (fieldConfig.resultExtension || fieldConfig.virtual) {
|
|
36
|
-
hasExtensions = true
|
|
37
|
-
break
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (hasExtensions) break
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!hasExtensions) {
|
|
44
|
-
// No extensions needed - export a no-op
|
|
45
|
-
lines.push('/**')
|
|
46
|
-
lines.push(' * No result extensions configured')
|
|
47
|
-
lines.push(' */')
|
|
48
|
-
lines.push('export const prismaExtensions = {}')
|
|
49
|
-
lines.push('')
|
|
50
|
-
return lines.join('\n')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Generate result extensions
|
|
54
|
-
lines.push('/**')
|
|
55
|
-
lines.push(' * Prisma result extensions for field transformations and virtual fields')
|
|
56
|
-
lines.push(' * Delegates to field resolveOutput hooks from config for runtime transformations')
|
|
57
|
-
lines.push(' */')
|
|
58
|
-
lines.push('export const prismaExtensions = Prisma.defineExtension({')
|
|
59
|
-
lines.push(' result: {')
|
|
60
|
-
|
|
61
|
-
// Generate extensions for each list
|
|
62
|
-
for (const [listName, listConfig] of Object.entries(config.lists)) {
|
|
63
|
-
// Include both fields with resultExtension AND virtual fields
|
|
64
|
-
const fieldsWithExtensions: Array<[string, FieldConfig]> = Object.entries(
|
|
65
|
-
listConfig.fields,
|
|
66
|
-
).filter(([_, config]) => config.resultExtension || config.virtual)
|
|
67
|
-
|
|
68
|
-
if (fieldsWithExtensions.length === 0) continue
|
|
69
|
-
|
|
70
|
-
const modelKey = listName.charAt(0).toLowerCase() + listName.slice(1) // camelCase
|
|
71
|
-
|
|
72
|
-
lines.push(` ${modelKey}: {`)
|
|
73
|
-
|
|
74
|
-
for (const [fieldName, fieldConfig] of fieldsWithExtensions) {
|
|
75
|
-
const isVirtual = fieldConfig.virtual
|
|
76
|
-
|
|
77
|
-
lines.push(` ${fieldName}: {`)
|
|
78
|
-
|
|
79
|
-
if (isVirtual) {
|
|
80
|
-
// Virtual fields don't need database fields - they compute from the full item
|
|
81
|
-
lines.push(` needs: {},`)
|
|
82
|
-
} else {
|
|
83
|
-
// Non-virtual fields need their database value
|
|
84
|
-
lines.push(` needs: { ${fieldName}: true },`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
lines.push(` compute: (${modelKey}) => {`)
|
|
88
|
-
|
|
89
|
-
if (!isVirtual) {
|
|
90
|
-
// For non-virtual fields, get the database value and check nullability
|
|
91
|
-
lines.push(` const value = ${modelKey}.${fieldName}`)
|
|
92
|
-
lines.push(` if (value === null || value === undefined) {`)
|
|
93
|
-
lines.push(` return undefined`)
|
|
94
|
-
lines.push(` }`)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
lines.push(` // Call field's resolveOutput hook if available (synchronously)`)
|
|
98
|
-
lines.push(` if (!resolvedConfig) {`)
|
|
99
|
-
lines.push(
|
|
100
|
-
` // Config not yet resolved - return undefined for virtual, value for regular`,
|
|
101
|
-
)
|
|
102
|
-
if (isVirtual) {
|
|
103
|
-
lines.push(` return undefined`)
|
|
104
|
-
} else {
|
|
105
|
-
lines.push(` return value`)
|
|
106
|
-
}
|
|
107
|
-
lines.push(` }`)
|
|
108
|
-
lines.push(
|
|
109
|
-
` const fieldConfig = resolvedConfig.lists['${listName}'].fields['${fieldName}']`,
|
|
110
|
-
)
|
|
111
|
-
lines.push(` if (fieldConfig.hooks?.resolveOutput) {`)
|
|
112
|
-
lines.push(` return fieldConfig.hooks.resolveOutput({`)
|
|
113
|
-
lines.push(` operation: 'query',`)
|
|
114
|
-
if (isVirtual) {
|
|
115
|
-
lines.push(` value: undefined, // Virtual fields have no stored value`)
|
|
116
|
-
} else {
|
|
117
|
-
lines.push(` value,`)
|
|
118
|
-
}
|
|
119
|
-
lines.push(` item: ${modelKey},`)
|
|
120
|
-
lines.push(` listKey: '${listName}',`)
|
|
121
|
-
lines.push(` fieldName: '${fieldName}',`)
|
|
122
|
-
lines.push(
|
|
123
|
-
` context: null as any, // Extension context doesn't have full context`,
|
|
124
|
-
)
|
|
125
|
-
lines.push(` })`)
|
|
126
|
-
lines.push(` }`)
|
|
127
|
-
if (isVirtual) {
|
|
128
|
-
lines.push(` return undefined`)
|
|
129
|
-
} else {
|
|
130
|
-
lines.push(` return value`)
|
|
131
|
-
}
|
|
132
|
-
lines.push(` },`)
|
|
133
|
-
lines.push(` },`)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
lines.push(` },`)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
lines.push(' },')
|
|
140
|
-
lines.push('})')
|
|
141
|
-
lines.push('')
|
|
142
|
-
|
|
143
|
-
return lines.join('\n')
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Write Prisma extensions configuration to file
|
|
148
|
-
*/
|
|
149
|
-
export function writePrismaExtensions(config: OpenSaasConfig, outputPath: string): void {
|
|
150
|
-
const extensions = generatePrismaExtensions(config)
|
|
151
|
-
|
|
152
|
-
// Ensure directory exists
|
|
153
|
-
const dir = path.dirname(outputPath)
|
|
154
|
-
if (!fs.existsSync(dir)) {
|
|
155
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
fs.writeFileSync(outputPath, extensions, 'utf-8')
|
|
159
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { generatePrismaSchema } from './prisma.js'
|
|
3
|
-
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
4
|
-
import { text, integer, relationship, checkbox, timestamp } from '@opensaas/stack-core/fields'
|
|
5
|
-
|
|
6
|
-
describe('Prisma Schema Generator', () => {
|
|
7
|
-
describe('generatePrismaSchema', () => {
|
|
8
|
-
it('should generate basic schema with datasource and generator', () => {
|
|
9
|
-
const config: OpenSaasConfig = {
|
|
10
|
-
db: {
|
|
11
|
-
provider: 'sqlite',
|
|
12
|
-
},
|
|
13
|
-
lists: {},
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const schema = generatePrismaSchema(config)
|
|
17
|
-
|
|
18
|
-
expect(schema).toMatchSnapshot()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('should use custom opensaasPath for generator output', () => {
|
|
22
|
-
const config: OpenSaasConfig = {
|
|
23
|
-
db: {
|
|
24
|
-
provider: 'sqlite',
|
|
25
|
-
},
|
|
26
|
-
opensaasPath: '.custom-path',
|
|
27
|
-
lists: {},
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const schema = generatePrismaSchema(config)
|
|
31
|
-
|
|
32
|
-
expect(schema).toMatchSnapshot()
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('should generate model with basic fields', () => {
|
|
36
|
-
const config: OpenSaasConfig = {
|
|
37
|
-
db: {
|
|
38
|
-
provider: 'sqlite',
|
|
39
|
-
},
|
|
40
|
-
lists: {
|
|
41
|
-
User: {
|
|
42
|
-
fields: {
|
|
43
|
-
name: text({ validation: { isRequired: true } }),
|
|
44
|
-
email: text({ validation: { isRequired: true } }),
|
|
45
|
-
age: integer(),
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const schema = generatePrismaSchema(config)
|
|
52
|
-
|
|
53
|
-
expect(schema).toMatchSnapshot()
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('should generate model with checkbox field', () => {
|
|
57
|
-
const config: OpenSaasConfig = {
|
|
58
|
-
db: {
|
|
59
|
-
provider: 'sqlite',
|
|
60
|
-
},
|
|
61
|
-
lists: {
|
|
62
|
-
Post: {
|
|
63
|
-
fields: {
|
|
64
|
-
title: text(),
|
|
65
|
-
isPublished: checkbox({ defaultValue: false }),
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const schema = generatePrismaSchema(config)
|
|
72
|
-
|
|
73
|
-
expect(schema).toMatchSnapshot()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('should generate model with timestamp field', () => {
|
|
77
|
-
const config: OpenSaasConfig = {
|
|
78
|
-
db: {
|
|
79
|
-
provider: 'sqlite',
|
|
80
|
-
},
|
|
81
|
-
lists: {
|
|
82
|
-
Post: {
|
|
83
|
-
fields: {
|
|
84
|
-
title: text(),
|
|
85
|
-
publishedAt: timestamp(),
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const schema = generatePrismaSchema(config)
|
|
92
|
-
|
|
93
|
-
expect(schema).toMatchSnapshot()
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('should generate many-to-one relationship', () => {
|
|
97
|
-
const config: OpenSaasConfig = {
|
|
98
|
-
db: {
|
|
99
|
-
provider: 'sqlite',
|
|
100
|
-
},
|
|
101
|
-
lists: {
|
|
102
|
-
User: {
|
|
103
|
-
fields: {
|
|
104
|
-
name: text(),
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
Post: {
|
|
108
|
-
fields: {
|
|
109
|
-
title: text(),
|
|
110
|
-
author: relationship({ ref: 'User.posts' }),
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const schema = generatePrismaSchema(config)
|
|
117
|
-
|
|
118
|
-
expect(schema).toMatchSnapshot()
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('should generate one-to-many relationship', () => {
|
|
122
|
-
const config: OpenSaasConfig = {
|
|
123
|
-
db: {
|
|
124
|
-
provider: 'sqlite',
|
|
125
|
-
},
|
|
126
|
-
lists: {
|
|
127
|
-
User: {
|
|
128
|
-
fields: {
|
|
129
|
-
name: text(),
|
|
130
|
-
posts: relationship({ ref: 'Post.author', many: true }),
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
Post: {
|
|
134
|
-
fields: {
|
|
135
|
-
title: text(),
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const schema = generatePrismaSchema(config)
|
|
142
|
-
|
|
143
|
-
expect(schema).toMatchSnapshot()
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('should generate multiple models', () => {
|
|
147
|
-
const config: OpenSaasConfig = {
|
|
148
|
-
db: {
|
|
149
|
-
provider: 'postgresql',
|
|
150
|
-
},
|
|
151
|
-
lists: {
|
|
152
|
-
User: {
|
|
153
|
-
fields: {
|
|
154
|
-
name: text(),
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
Post: {
|
|
158
|
-
fields: {
|
|
159
|
-
title: text(),
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
Comment: {
|
|
163
|
-
fields: {
|
|
164
|
-
content: text(),
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const schema = generatePrismaSchema(config)
|
|
171
|
-
|
|
172
|
-
expect(schema).toMatchSnapshot()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('should always include system fields', () => {
|
|
176
|
-
const config: OpenSaasConfig = {
|
|
177
|
-
db: {
|
|
178
|
-
provider: 'sqlite',
|
|
179
|
-
},
|
|
180
|
-
lists: {
|
|
181
|
-
User: {
|
|
182
|
-
fields: {
|
|
183
|
-
name: text(),
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const schema = generatePrismaSchema(config)
|
|
190
|
-
|
|
191
|
-
expect(schema).toContain('id String @id @default(cuid())')
|
|
192
|
-
expect(schema).toContain('createdAt DateTime @default(now())')
|
|
193
|
-
expect(schema).toContain('updatedAt DateTime @updatedAt')
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('should handle empty lists config', () => {
|
|
197
|
-
const config: OpenSaasConfig = {
|
|
198
|
-
db: {
|
|
199
|
-
provider: 'sqlite',
|
|
200
|
-
},
|
|
201
|
-
lists: {},
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const schema = generatePrismaSchema(config)
|
|
205
|
-
|
|
206
|
-
expect(schema).toContain('generator client {')
|
|
207
|
-
expect(schema).toContain('datasource db {')
|
|
208
|
-
expect(schema).not.toContain('model')
|
|
209
|
-
})
|
|
210
|
-
})
|
|
211
|
-
})
|