@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.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +348 -0
  3. package/CLAUDE.md +60 -12
  4. package/dist/commands/generate.d.ts.map +1 -1
  5. package/dist/commands/generate.js +13 -13
  6. package/dist/commands/generate.js.map +1 -1
  7. package/dist/commands/mcp.d.ts +6 -0
  8. package/dist/commands/mcp.d.ts.map +1 -0
  9. package/dist/commands/mcp.js +116 -0
  10. package/dist/commands/mcp.js.map +1 -0
  11. package/dist/generator/context.d.ts.map +1 -1
  12. package/dist/generator/context.js +40 -7
  13. package/dist/generator/context.js.map +1 -1
  14. package/dist/generator/index.d.ts +4 -1
  15. package/dist/generator/index.d.ts.map +1 -1
  16. package/dist/generator/index.js +4 -1
  17. package/dist/generator/index.js.map +1 -1
  18. package/dist/generator/lists.d.ts +31 -0
  19. package/dist/generator/lists.d.ts.map +1 -0
  20. package/dist/generator/lists.js +123 -0
  21. package/dist/generator/lists.js.map +1 -0
  22. package/dist/generator/plugin-types.d.ts +10 -0
  23. package/dist/generator/plugin-types.d.ts.map +1 -0
  24. package/dist/generator/plugin-types.js +122 -0
  25. package/dist/generator/plugin-types.js.map +1 -0
  26. package/dist/generator/prisma-config.d.ts +17 -0
  27. package/dist/generator/prisma-config.d.ts.map +1 -0
  28. package/dist/generator/prisma-config.js +40 -0
  29. package/dist/generator/prisma-config.js.map +1 -0
  30. package/dist/generator/prisma-extensions.d.ts +11 -0
  31. package/dist/generator/prisma-extensions.d.ts.map +1 -0
  32. package/dist/generator/prisma-extensions.js +134 -0
  33. package/dist/generator/prisma-extensions.js.map +1 -0
  34. package/dist/generator/prisma.d.ts.map +1 -1
  35. package/dist/generator/prisma.js +5 -2
  36. package/dist/generator/prisma.js.map +1 -1
  37. package/dist/generator/types.d.ts.map +1 -1
  38. package/dist/generator/types.js +201 -17
  39. package/dist/generator/types.js.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/mcp/lib/documentation-provider.d.ts +43 -0
  43. package/dist/mcp/lib/documentation-provider.d.ts.map +1 -0
  44. package/dist/mcp/lib/documentation-provider.js +163 -0
  45. package/dist/mcp/lib/documentation-provider.js.map +1 -0
  46. package/dist/mcp/lib/features/catalog.d.ts +26 -0
  47. package/dist/mcp/lib/features/catalog.d.ts.map +1 -0
  48. package/dist/mcp/lib/features/catalog.js +291 -0
  49. package/dist/mcp/lib/features/catalog.js.map +1 -0
  50. package/dist/mcp/lib/generators/feature-generator.d.ts +35 -0
  51. package/dist/mcp/lib/generators/feature-generator.d.ts.map +1 -0
  52. package/dist/mcp/lib/generators/feature-generator.js +546 -0
  53. package/dist/mcp/lib/generators/feature-generator.js.map +1 -0
  54. package/dist/mcp/lib/types.d.ts +80 -0
  55. package/dist/mcp/lib/types.d.ts.map +1 -0
  56. package/dist/mcp/lib/types.js +5 -0
  57. package/dist/mcp/lib/types.js.map +1 -0
  58. package/dist/mcp/lib/wizards/wizard-engine.d.ts +71 -0
  59. package/dist/mcp/lib/wizards/wizard-engine.d.ts.map +1 -0
  60. package/dist/mcp/lib/wizards/wizard-engine.js +356 -0
  61. package/dist/mcp/lib/wizards/wizard-engine.js.map +1 -0
  62. package/dist/mcp/server/index.d.ts +8 -0
  63. package/dist/mcp/server/index.d.ts.map +1 -0
  64. package/dist/mcp/server/index.js +202 -0
  65. package/dist/mcp/server/index.js.map +1 -0
  66. package/dist/mcp/server/stack-mcp-server.d.ts +92 -0
  67. package/dist/mcp/server/stack-mcp-server.d.ts.map +1 -0
  68. package/dist/mcp/server/stack-mcp-server.js +265 -0
  69. package/dist/mcp/server/stack-mcp-server.js.map +1 -0
  70. package/package.json +10 -8
  71. package/src/commands/__snapshots__/generate.test.ts.snap +145 -38
  72. package/src/commands/dev.test.ts +0 -1
  73. package/src/commands/generate.test.ts +18 -8
  74. package/src/commands/generate.ts +20 -19
  75. package/src/commands/mcp.ts +135 -0
  76. package/src/generator/__snapshots__/context.test.ts.snap +63 -18
  77. package/src/generator/__snapshots__/prisma.test.ts.snap +8 -16
  78. package/src/generator/__snapshots__/types.test.ts.snap +1267 -95
  79. package/src/generator/context.test.ts +15 -8
  80. package/src/generator/context.ts +40 -7
  81. package/src/generator/index.ts +4 -1
  82. package/src/generator/lists.test.ts +335 -0
  83. package/src/generator/lists.ts +140 -0
  84. package/src/generator/plugin-types.ts +147 -0
  85. package/src/generator/prisma-config.ts +46 -0
  86. package/src/generator/prisma-extensions.ts +159 -0
  87. package/src/generator/prisma.test.ts +0 -10
  88. package/src/generator/prisma.ts +6 -2
  89. package/src/generator/types.test.ts +0 -12
  90. package/src/generator/types.ts +257 -17
  91. package/src/index.ts +4 -0
  92. package/src/mcp/lib/documentation-provider.ts +203 -0
  93. package/src/mcp/lib/features/catalog.ts +301 -0
  94. package/src/mcp/lib/generators/feature-generator.ts +598 -0
  95. package/src/mcp/lib/types.ts +89 -0
  96. package/src/mcp/lib/wizards/wizard-engine.ts +427 -0
  97. package/src/mcp/server/index.ts +240 -0
  98. package/src/mcp/server/stack-mcp-server.ts +301 -0
  99. package/tsconfig.tsbuildinfo +1 -1
  100. package/dist/generator/type-patcher.d.ts +0 -13
  101. package/dist/generator/type-patcher.d.ts.map +0 -1
  102. package/dist/generator/type-patcher.js +0 -68
  103. package/dist/generator/type-patcher.js.map +0 -1
  104. 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
- url: 'file:./dev.db',
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
- url: 'file:./dev.db',
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('globalThis as unknown as { prisma: PrismaClient | undefined }')
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
- url: 'file:./dev.db',
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
- url: 'file:./dev.db',
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
- url: 'file:./dev.db',
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
- url: 'file:./dev.db',
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ prismaClientConstructor: (() => null) as any,
122
129
  },
123
130
  lists: {},
124
131
  }
@@ -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
- : `new PrismaClient()`
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: PrismaClient | undefined }
116
- let prisma: PrismaClient | null = null
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
- prisma = globalForPrisma.prisma ?? ${prismaInstantiation}
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
  /**
@@ -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 { patchPrismaTypes } from './type-patcher.js'
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
+ }