@opensaas/stack-cli 0.1.7 → 0.4.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 +1 -1
- package/CHANGELOG.md +348 -0
- package/CLAUDE.md +60 -12
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +13 -13
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +116 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/generator/context.d.ts.map +1 -1
- package/dist/generator/context.js +40 -7
- package/dist/generator/context.js.map +1 -1
- package/dist/generator/index.d.ts +4 -1
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +4 -1
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/lists.d.ts +31 -0
- package/dist/generator/lists.d.ts.map +1 -0
- package/dist/generator/lists.js +123 -0
- package/dist/generator/lists.js.map +1 -0
- package/dist/generator/plugin-types.d.ts +10 -0
- package/dist/generator/plugin-types.d.ts.map +1 -0
- package/dist/generator/plugin-types.js +122 -0
- package/dist/generator/plugin-types.js.map +1 -0
- package/dist/generator/prisma-config.d.ts +17 -0
- package/dist/generator/prisma-config.d.ts.map +1 -0
- package/dist/generator/prisma-config.js +40 -0
- package/dist/generator/prisma-config.js.map +1 -0
- package/dist/generator/prisma-extensions.d.ts +11 -0
- package/dist/generator/prisma-extensions.d.ts.map +1 -0
- package/dist/generator/prisma-extensions.js +134 -0
- package/dist/generator/prisma-extensions.js.map +1 -0
- package/dist/generator/prisma.d.ts.map +1 -1
- package/dist/generator/prisma.js +5 -2
- package/dist/generator/prisma.js.map +1 -1
- package/dist/generator/types.d.ts.map +1 -1
- package/dist/generator/types.js +201 -17
- package/dist/generator/types.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/lib/documentation-provider.d.ts +43 -0
- package/dist/mcp/lib/documentation-provider.d.ts.map +1 -0
- package/dist/mcp/lib/documentation-provider.js +163 -0
- package/dist/mcp/lib/documentation-provider.js.map +1 -0
- package/dist/mcp/lib/features/catalog.d.ts +26 -0
- package/dist/mcp/lib/features/catalog.d.ts.map +1 -0
- package/dist/mcp/lib/features/catalog.js +291 -0
- package/dist/mcp/lib/features/catalog.js.map +1 -0
- package/dist/mcp/lib/generators/feature-generator.d.ts +35 -0
- package/dist/mcp/lib/generators/feature-generator.d.ts.map +1 -0
- package/dist/mcp/lib/generators/feature-generator.js +546 -0
- package/dist/mcp/lib/generators/feature-generator.js.map +1 -0
- package/dist/mcp/lib/types.d.ts +80 -0
- package/dist/mcp/lib/types.d.ts.map +1 -0
- package/dist/mcp/lib/types.js +5 -0
- package/dist/mcp/lib/types.js.map +1 -0
- package/dist/mcp/lib/wizards/wizard-engine.d.ts +71 -0
- package/dist/mcp/lib/wizards/wizard-engine.d.ts.map +1 -0
- package/dist/mcp/lib/wizards/wizard-engine.js +356 -0
- package/dist/mcp/lib/wizards/wizard-engine.js.map +1 -0
- package/dist/mcp/server/index.d.ts +8 -0
- package/dist/mcp/server/index.d.ts.map +1 -0
- package/dist/mcp/server/index.js +202 -0
- package/dist/mcp/server/index.js.map +1 -0
- package/dist/mcp/server/stack-mcp-server.d.ts +92 -0
- package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -0
- package/dist/mcp/server/stack-mcp-server.js +265 -0
- package/dist/mcp/server/stack-mcp-server.js.map +1 -0
- package/package.json +10 -8
- package/src/commands/__snapshots__/generate.test.ts.snap +145 -38
- package/src/commands/dev.test.ts +0 -1
- package/src/commands/generate.test.ts +18 -8
- package/src/commands/generate.ts +20 -19
- package/src/commands/mcp.ts +135 -0
- package/src/generator/__snapshots__/context.test.ts.snap +63 -18
- package/src/generator/__snapshots__/prisma.test.ts.snap +8 -16
- package/src/generator/__snapshots__/types.test.ts.snap +1267 -95
- package/src/generator/context.test.ts +15 -8
- package/src/generator/context.ts +40 -7
- package/src/generator/index.ts +4 -1
- package/src/generator/lists.test.ts +335 -0
- package/src/generator/lists.ts +140 -0
- package/src/generator/plugin-types.ts +147 -0
- package/src/generator/prisma-config.ts +46 -0
- package/src/generator/prisma-extensions.ts +159 -0
- package/src/generator/prisma.test.ts +0 -10
- package/src/generator/prisma.ts +6 -2
- package/src/generator/types.test.ts +0 -12
- package/src/generator/types.ts +257 -17
- package/src/index.ts +4 -0
- package/src/mcp/lib/documentation-provider.ts +203 -0
- package/src/mcp/lib/features/catalog.ts +301 -0
- package/src/mcp/lib/generators/feature-generator.ts +598 -0
- package/src/mcp/lib/types.ts +89 -0
- package/src/mcp/lib/wizards/wizard-engine.ts +427 -0
- package/src/mcp/server/index.ts +240 -0
- package/src/mcp/server/stack-mcp-server.ts +301 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/generator/type-patcher.d.ts +0 -13
- package/dist/generator/type-patcher.d.ts.map +0 -1
- package/dist/generator/type-patcher.js +0 -68
- package/dist/generator/type-patcher.js.map +0 -1
- package/src/generator/type-patcher.ts +0 -93
|
@@ -9,7 +9,8 @@ describe('Context Generator', () => {
|
|
|
9
9
|
const config: OpenSaasConfig = {
|
|
10
10
|
db: {
|
|
11
11
|
provider: 'sqlite',
|
|
12
|
-
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
prismaClientConstructor: (() => null) as any,
|
|
13
14
|
},
|
|
14
15
|
lists: {
|
|
15
16
|
User: {
|
|
@@ -29,7 +30,6 @@ describe('Context Generator', () => {
|
|
|
29
30
|
const config: OpenSaasConfig = {
|
|
30
31
|
db: {
|
|
31
32
|
provider: 'postgresql',
|
|
32
|
-
url: process.env.DATABASE_URL || 'postgresql://localhost:5432/db',
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
34
|
prismaClientConstructor: ((PrismaClient: any) => new PrismaClient()) as any,
|
|
35
35
|
},
|
|
@@ -51,7 +51,8 @@ describe('Context Generator', () => {
|
|
|
51
51
|
const config: OpenSaasConfig = {
|
|
52
52
|
db: {
|
|
53
53
|
provider: 'sqlite',
|
|
54
|
-
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
prismaClientConstructor: (() => null) as any,
|
|
55
56
|
},
|
|
56
57
|
lists: {},
|
|
57
58
|
}
|
|
@@ -59,7 +60,9 @@ describe('Context Generator', () => {
|
|
|
59
60
|
const context = generateContext(config)
|
|
60
61
|
|
|
61
62
|
expect(context).toContain('const globalForPrisma')
|
|
62
|
-
expect(context).toContain(
|
|
63
|
+
expect(context).toContain(
|
|
64
|
+
'globalThis as unknown as { prisma: ReturnType<typeof createExtendedPrisma> | null }',
|
|
65
|
+
)
|
|
63
66
|
expect(context).toContain('globalForPrisma.prisma')
|
|
64
67
|
expect(context).toContain("if (process.env.NODE_ENV !== 'production')")
|
|
65
68
|
})
|
|
@@ -68,7 +71,8 @@ describe('Context Generator', () => {
|
|
|
68
71
|
const config: OpenSaasConfig = {
|
|
69
72
|
db: {
|
|
70
73
|
provider: 'sqlite',
|
|
71
|
-
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
prismaClientConstructor: (() => null) as any,
|
|
72
76
|
},
|
|
73
77
|
lists: {},
|
|
74
78
|
}
|
|
@@ -87,7 +91,8 @@ describe('Context Generator', () => {
|
|
|
87
91
|
const config: OpenSaasConfig = {
|
|
88
92
|
db: {
|
|
89
93
|
provider: 'sqlite',
|
|
90
|
-
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
prismaClientConstructor: (() => null) as any,
|
|
91
96
|
},
|
|
92
97
|
lists: {},
|
|
93
98
|
}
|
|
@@ -104,7 +109,8 @@ describe('Context Generator', () => {
|
|
|
104
109
|
const config: OpenSaasConfig = {
|
|
105
110
|
db: {
|
|
106
111
|
provider: 'sqlite',
|
|
107
|
-
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
prismaClientConstructor: (() => null) as any,
|
|
108
114
|
},
|
|
109
115
|
lists: {},
|
|
110
116
|
}
|
|
@@ -118,7 +124,8 @@ describe('Context Generator', () => {
|
|
|
118
124
|
const config: OpenSaasConfig = {
|
|
119
125
|
db: {
|
|
120
126
|
provider: 'sqlite',
|
|
121
|
-
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
prismaClientConstructor: (() => null) as any,
|
|
122
129
|
},
|
|
123
130
|
lists: {},
|
|
124
131
|
}
|
package/src/generator/context.ts
CHANGED
|
@@ -16,9 +16,27 @@ export function generateContext(config: OpenSaasConfig): string {
|
|
|
16
16
|
const hasStorage = !!config.storage && Object.keys(config.storage).length > 0
|
|
17
17
|
|
|
18
18
|
// Generate the Prisma client instantiation code
|
|
19
|
+
// Prisma 7 requires adapters, so prismaClientConstructor must be provided
|
|
19
20
|
const prismaInstantiation = hasCustomConstructor
|
|
20
21
|
? `resolvedConfig.db.prismaClientConstructor!(PrismaClient)`
|
|
21
|
-
: `
|
|
22
|
+
: `(() => {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'Prisma 7 requires a database adapter. Please add prismaClientConstructor to your opensaas.config.ts db configuration.\\n\\n' +
|
|
25
|
+
'Example for SQLite:\\n' +
|
|
26
|
+
'import { PrismaBetterSQLite3 } from \\'@prisma/adapter-better-sqlite3\\'\\n' +
|
|
27
|
+
'import Database from \\'better-sqlite3\\'\\n\\n' +
|
|
28
|
+
'db: {\\n' +
|
|
29
|
+
' provider: \\'sqlite\\',\\n' +
|
|
30
|
+
' url: process.env.DATABASE_URL || \\'file:./dev.db\\',\\n' +
|
|
31
|
+
' prismaClientConstructor: (PrismaClient) => {\\n' +
|
|
32
|
+
' const db = new Database(process.env.DATABASE_URL || \\'./dev.db\\')\\n' +
|
|
33
|
+
' const adapter = new PrismaBetterSQLite3(db)\\n' +
|
|
34
|
+
' return new PrismaClient({ adapter })\\n' +
|
|
35
|
+
' }\\n' +
|
|
36
|
+
'}\\n\\n' +
|
|
37
|
+
'See https://www.prisma.io/docs/orm/overview/databases/database-drivers for more information.'
|
|
38
|
+
)
|
|
39
|
+
})()`
|
|
22
40
|
|
|
23
41
|
// Generate storage utilities if storage is configured
|
|
24
42
|
const storageUtilities = hasStorage
|
|
@@ -103,8 +121,9 @@ const storage = {
|
|
|
103
121
|
|
|
104
122
|
import { getContext as getOpensaasContext } from '@opensaas/stack-core'
|
|
105
123
|
import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
|
|
106
|
-
import { PrismaClient } from './prisma-client'
|
|
124
|
+
import { PrismaClient } from './prisma-client/client'
|
|
107
125
|
import type { Context } from './types'
|
|
126
|
+
import { prismaExtensions } from './prisma-extensions'
|
|
108
127
|
import configOrPromise from '../opensaas.config'
|
|
109
128
|
|
|
110
129
|
// Resolve config if it's a Promise (when plugins are present)
|
|
@@ -112,15 +131,29 @@ const configPromise = Promise.resolve(configOrPromise)
|
|
|
112
131
|
let resolvedConfig: OpenSaasConfig | null = null
|
|
113
132
|
|
|
114
133
|
// Internal Prisma singleton - managed automatically
|
|
115
|
-
const globalForPrisma = globalThis as unknown as { prisma:
|
|
116
|
-
let prisma:
|
|
134
|
+
const globalForPrisma = globalThis as unknown as { prisma: ReturnType<typeof createExtendedPrisma> | null }
|
|
135
|
+
let prisma: ReturnType<typeof createExtendedPrisma> | null = null
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create Prisma client with result extensions
|
|
139
|
+
*/
|
|
140
|
+
function createExtendedPrisma(basePrisma: PrismaClient) {
|
|
141
|
+
// Check if there are any extensions to apply
|
|
142
|
+
if (Object.keys(prismaExtensions).length === 0) {
|
|
143
|
+
return basePrisma
|
|
144
|
+
}
|
|
145
|
+
// Apply result extensions
|
|
146
|
+
return basePrisma.$extends(prismaExtensions)
|
|
147
|
+
}
|
|
117
148
|
|
|
118
149
|
async function getPrisma() {
|
|
119
150
|
if (!prisma) {
|
|
120
151
|
if (!resolvedConfig) {
|
|
121
152
|
resolvedConfig = await configPromise
|
|
122
153
|
}
|
|
123
|
-
|
|
154
|
+
const basePrisma = ${prismaInstantiation}
|
|
155
|
+
const extendedPrisma = createExtendedPrisma(basePrisma)
|
|
156
|
+
prisma = globalForPrisma.prisma ?? extendedPrisma
|
|
124
157
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
|
125
158
|
}
|
|
126
159
|
return prisma
|
|
@@ -157,7 +190,7 @@ ${storageUtilities}
|
|
|
157
190
|
export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
|
|
158
191
|
const config = await getConfig()
|
|
159
192
|
const prismaClient = await getPrisma()
|
|
160
|
-
return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
|
|
193
|
+
return getOpensaasContext(config, prismaClient, session ?? null, storage) as unknown as Context<TSession>
|
|
161
194
|
}
|
|
162
195
|
|
|
163
196
|
/**
|
|
@@ -167,7 +200,7 @@ export async function getContext<TSession extends OpensaasSession = OpensaasSess
|
|
|
167
200
|
export const rawOpensaasContext = (async () => {
|
|
168
201
|
const config = await getConfig()
|
|
169
202
|
const prismaClient = await getPrisma()
|
|
170
|
-
return getOpensaasContext(config, prismaClient, null, storage)
|
|
203
|
+
return getOpensaasContext(config, prismaClient, null, storage) as unknown as Context
|
|
171
204
|
})()
|
|
172
205
|
|
|
173
206
|
/**
|
package/src/generator/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { generatePrismaSchema, writePrismaSchema } from './prisma.js'
|
|
2
|
+
export { generatePrismaConfig, writePrismaConfig } from './prisma-config.js'
|
|
2
3
|
export { generateTypes, writeTypes } from './types.js'
|
|
3
|
-
export {
|
|
4
|
+
export { generateListsNamespace, writeLists } from './lists.js'
|
|
4
5
|
export { generateContext, writeContext } from './context.js'
|
|
6
|
+
export { generatePluginTypes, writePluginTypes } from './plugin-types.js'
|
|
7
|
+
export { generatePrismaExtensions, writePrismaExtensions } from './prisma-extensions.js'
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { generateListsNamespace } from './lists.js'
|
|
3
|
+
import type { OpenSaasConfig } from '@opensaas/stack-core'
|
|
4
|
+
import { text, integer, relationship, checkbox } from '@opensaas/stack-core/fields'
|
|
5
|
+
|
|
6
|
+
describe('Lists Namespace Generator', () => {
|
|
7
|
+
describe('generateListsNamespace', () => {
|
|
8
|
+
it('should generate Lists namespace for single list', () => {
|
|
9
|
+
const config: OpenSaasConfig = {
|
|
10
|
+
db: {
|
|
11
|
+
provider: 'sqlite',
|
|
12
|
+
},
|
|
13
|
+
lists: {
|
|
14
|
+
Post: {
|
|
15
|
+
fields: {
|
|
16
|
+
title: text({ validation: { isRequired: true } }),
|
|
17
|
+
content: text(),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lists = generateListsNamespace(config)
|
|
24
|
+
|
|
25
|
+
expect(lists).toContain('export declare namespace Lists {')
|
|
26
|
+
expect(lists).toContain('export type Post')
|
|
27
|
+
expect(lists).toContain('namespace Post {')
|
|
28
|
+
expect(lists).toContain("export type Item = import('./types').Post")
|
|
29
|
+
expect(lists).toContain('export type TypeInfo')
|
|
30
|
+
expect(lists).toContain("key: 'Post'")
|
|
31
|
+
expect(lists).toContain('item: Item')
|
|
32
|
+
expect(lists).toContain('inputs: {')
|
|
33
|
+
expect(lists).toContain("create: import('./prisma-client/client').Prisma.PostCreateInput")
|
|
34
|
+
expect(lists).toContain("update: import('./prisma-client/client').Prisma.PostUpdateInput")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should generate Lists namespace for multiple lists', () => {
|
|
38
|
+
const config: OpenSaasConfig = {
|
|
39
|
+
db: {
|
|
40
|
+
provider: 'sqlite',
|
|
41
|
+
},
|
|
42
|
+
lists: {
|
|
43
|
+
User: {
|
|
44
|
+
fields: {
|
|
45
|
+
name: text(),
|
|
46
|
+
email: text(),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
Post: {
|
|
50
|
+
fields: {
|
|
51
|
+
title: text(),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
Comment: {
|
|
55
|
+
fields: {
|
|
56
|
+
content: text(),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lists = generateListsNamespace(config)
|
|
63
|
+
|
|
64
|
+
// Check all three lists are present
|
|
65
|
+
expect(lists).toContain('export type User')
|
|
66
|
+
expect(lists).toContain('export type Post')
|
|
67
|
+
expect(lists).toContain('export type Comment')
|
|
68
|
+
|
|
69
|
+
// Check all three namespaces
|
|
70
|
+
expect(lists).toContain('namespace User {')
|
|
71
|
+
expect(lists).toContain('namespace Post {')
|
|
72
|
+
expect(lists).toContain('namespace Comment {')
|
|
73
|
+
|
|
74
|
+
// Check TypeInfo for each
|
|
75
|
+
expect(lists).toContain("key: 'User'")
|
|
76
|
+
expect(lists).toContain("key: 'Post'")
|
|
77
|
+
expect(lists).toContain("key: 'Comment'")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should include header comment with usage examples', () => {
|
|
81
|
+
const config: OpenSaasConfig = {
|
|
82
|
+
db: {
|
|
83
|
+
provider: 'sqlite',
|
|
84
|
+
},
|
|
85
|
+
lists: {
|
|
86
|
+
Post: {
|
|
87
|
+
fields: {
|
|
88
|
+
title: text(),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lists = generateListsNamespace(config)
|
|
95
|
+
|
|
96
|
+
expect(lists).toContain('/**')
|
|
97
|
+
expect(lists).toContain('Generated Lists namespace from OpenSaas configuration')
|
|
98
|
+
expect(lists).toContain('DO NOT EDIT')
|
|
99
|
+
expect(lists).toContain('@example')
|
|
100
|
+
expect(lists).toContain('import type { Lists }')
|
|
101
|
+
expect(lists).toContain('list<Lists.Post.TypeInfo>')
|
|
102
|
+
expect(lists).toContain('const Post: Lists.Post = list')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should reference correct import paths', () => {
|
|
106
|
+
const config: OpenSaasConfig = {
|
|
107
|
+
db: {
|
|
108
|
+
provider: 'sqlite',
|
|
109
|
+
},
|
|
110
|
+
lists: {
|
|
111
|
+
User: {
|
|
112
|
+
fields: {
|
|
113
|
+
name: text(),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const lists = generateListsNamespace(config)
|
|
120
|
+
|
|
121
|
+
// Check ListConfig import
|
|
122
|
+
expect(lists).toContain("import('@opensaas/stack-core').ListConfig")
|
|
123
|
+
|
|
124
|
+
// Check Item import
|
|
125
|
+
expect(lists).toContain("import('./types').User")
|
|
126
|
+
|
|
127
|
+
// Check Prisma imports
|
|
128
|
+
expect(lists).toContain("import('./prisma-client/client').Prisma.UserCreateInput")
|
|
129
|
+
expect(lists).toContain("import('./prisma-client/client').Prisma.UserUpdateInput")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should generate TypeInfo structure correctly', () => {
|
|
133
|
+
const config: OpenSaasConfig = {
|
|
134
|
+
db: {
|
|
135
|
+
provider: 'sqlite',
|
|
136
|
+
},
|
|
137
|
+
lists: {
|
|
138
|
+
Post: {
|
|
139
|
+
fields: {
|
|
140
|
+
title: text(),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const lists = generateListsNamespace(config)
|
|
147
|
+
|
|
148
|
+
// Verify TypeInfo structure
|
|
149
|
+
expect(lists).toContain('export type TypeInfo = {')
|
|
150
|
+
expect(lists).toContain("key: 'Post'")
|
|
151
|
+
expect(lists).toContain('item: Item')
|
|
152
|
+
expect(lists).toContain('inputs: {')
|
|
153
|
+
expect(lists).toContain('create:')
|
|
154
|
+
expect(lists).toContain('update:')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should handle lists with relationships', () => {
|
|
158
|
+
const config: OpenSaasConfig = {
|
|
159
|
+
db: {
|
|
160
|
+
provider: 'sqlite',
|
|
161
|
+
},
|
|
162
|
+
lists: {
|
|
163
|
+
User: {
|
|
164
|
+
fields: {
|
|
165
|
+
name: text(),
|
|
166
|
+
posts: relationship({ ref: 'Post.author', many: true }),
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
Post: {
|
|
170
|
+
fields: {
|
|
171
|
+
title: text(),
|
|
172
|
+
author: relationship({ ref: 'User.posts' }),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lists = generateListsNamespace(config)
|
|
179
|
+
|
|
180
|
+
// Both lists should be generated
|
|
181
|
+
expect(lists).toContain('export type User')
|
|
182
|
+
expect(lists).toContain('export type Post')
|
|
183
|
+
|
|
184
|
+
// Prisma input types should still reference correct types
|
|
185
|
+
expect(lists).toContain('Prisma.UserCreateInput')
|
|
186
|
+
expect(lists).toContain('Prisma.PostCreateInput')
|
|
187
|
+
expect(lists).toContain('Prisma.UserUpdateInput')
|
|
188
|
+
expect(lists).toContain('Prisma.PostUpdateInput')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should handle lists with various field types', () => {
|
|
192
|
+
const config: OpenSaasConfig = {
|
|
193
|
+
db: {
|
|
194
|
+
provider: 'sqlite',
|
|
195
|
+
},
|
|
196
|
+
lists: {
|
|
197
|
+
Product: {
|
|
198
|
+
fields: {
|
|
199
|
+
name: text(),
|
|
200
|
+
price: integer(),
|
|
201
|
+
isAvailable: checkbox(),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const lists = generateListsNamespace(config)
|
|
208
|
+
|
|
209
|
+
// TypeInfo should be generated regardless of field types
|
|
210
|
+
expect(lists).toContain('export type Product')
|
|
211
|
+
expect(lists).toContain('namespace Product {')
|
|
212
|
+
expect(lists).toContain('export type TypeInfo')
|
|
213
|
+
expect(lists).toContain('Prisma.ProductCreateInput')
|
|
214
|
+
expect(lists).toContain('Prisma.ProductUpdateInput')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should close namespace properly', () => {
|
|
218
|
+
const config: OpenSaasConfig = {
|
|
219
|
+
db: {
|
|
220
|
+
provider: 'sqlite',
|
|
221
|
+
},
|
|
222
|
+
lists: {
|
|
223
|
+
User: {
|
|
224
|
+
fields: {
|
|
225
|
+
name: text(),
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const lists = generateListsNamespace(config)
|
|
232
|
+
|
|
233
|
+
// Should have closing brace for namespace
|
|
234
|
+
expect(lists).toMatch(/}\s*$/)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('should generate for empty lists object', () => {
|
|
238
|
+
const config: OpenSaasConfig = {
|
|
239
|
+
db: {
|
|
240
|
+
provider: 'sqlite',
|
|
241
|
+
},
|
|
242
|
+
lists: {},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const lists = generateListsNamespace(config)
|
|
246
|
+
|
|
247
|
+
// Should still have namespace declaration
|
|
248
|
+
expect(lists).toContain('export declare namespace Lists {')
|
|
249
|
+
expect(lists).toContain('}')
|
|
250
|
+
expect(lists).toContain('/**')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should maintain consistent formatting', () => {
|
|
254
|
+
const config: OpenSaasConfig = {
|
|
255
|
+
db: {
|
|
256
|
+
provider: 'sqlite',
|
|
257
|
+
},
|
|
258
|
+
lists: {
|
|
259
|
+
Post: {
|
|
260
|
+
fields: {
|
|
261
|
+
title: text(),
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const lists = generateListsNamespace(config)
|
|
268
|
+
|
|
269
|
+
// Check indentation consistency
|
|
270
|
+
expect(lists).toContain(' export type Post')
|
|
271
|
+
expect(lists).toContain(' namespace Post {')
|
|
272
|
+
expect(lists).toContain(' export type Item')
|
|
273
|
+
expect(lists).toContain(' export type TypeInfo')
|
|
274
|
+
expect(lists).toContain(' key:')
|
|
275
|
+
expect(lists).toContain(' item:')
|
|
276
|
+
expect(lists).toContain(' inputs: {')
|
|
277
|
+
expect(lists).toContain(' create:')
|
|
278
|
+
expect(lists).toContain(' update:')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('should handle list names with special casing', () => {
|
|
282
|
+
const config: OpenSaasConfig = {
|
|
283
|
+
db: {
|
|
284
|
+
provider: 'sqlite',
|
|
285
|
+
},
|
|
286
|
+
lists: {
|
|
287
|
+
BlogPost: {
|
|
288
|
+
fields: {
|
|
289
|
+
title: text(),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
APIKey: {
|
|
293
|
+
fields: {
|
|
294
|
+
key: text(),
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const lists = generateListsNamespace(config)
|
|
301
|
+
|
|
302
|
+
// Should preserve exact casing from config
|
|
303
|
+
expect(lists).toContain('export type BlogPost')
|
|
304
|
+
expect(lists).toContain('export type APIKey')
|
|
305
|
+
expect(lists).toContain('namespace BlogPost {')
|
|
306
|
+
expect(lists).toContain('namespace APIKey {')
|
|
307
|
+
expect(lists).toContain("key: 'BlogPost'")
|
|
308
|
+
expect(lists).toContain("key: 'APIKey'")
|
|
309
|
+
expect(lists).toContain('Prisma.BlogPostCreateInput')
|
|
310
|
+
expect(lists).toContain('Prisma.APIKeyCreateInput')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should connect List type to TypeInfo via ListConfig generic', () => {
|
|
314
|
+
const config: OpenSaasConfig = {
|
|
315
|
+
db: {
|
|
316
|
+
provider: 'sqlite',
|
|
317
|
+
},
|
|
318
|
+
lists: {
|
|
319
|
+
Post: {
|
|
320
|
+
fields: {
|
|
321
|
+
title: text(),
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const lists = generateListsNamespace(config)
|
|
328
|
+
|
|
329
|
+
// Verify the List type uses ListConfig with TypeInfo
|
|
330
|
+
expect(lists).toContain(
|
|
331
|
+
"export type Post = import('@opensaas/stack-core').ListConfig<Lists.Post.TypeInfo>",
|
|
332
|
+
)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
}
|