@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
package/src/generator/types.ts
CHANGED
|
@@ -41,24 +41,96 @@ function isFieldOptional(field: FieldConfig): boolean {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Generate
|
|
44
|
+
* Generate virtual fields type - only contains virtual fields
|
|
45
|
+
* This is intersected with Prisma's GetPayload to add virtual fields to query results
|
|
45
46
|
*/
|
|
46
|
-
function
|
|
47
|
+
function generateVirtualFieldsType(listName: string, fields: Record<string, FieldConfig>): string {
|
|
47
48
|
const lines: string[] = []
|
|
49
|
+
const virtualFields = Object.entries(fields).filter(([_, config]) => config.type === 'virtual')
|
|
48
50
|
|
|
49
|
-
lines.push(
|
|
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 = {`)
|
|
50
119
|
lines.push(' id: string')
|
|
51
120
|
|
|
52
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
|
+
|
|
53
125
|
if (fieldConfig.type === 'relationship') {
|
|
54
126
|
const relField = fieldConfig as RelationshipField
|
|
55
127
|
const [targetList] = relField.ref.split('.')
|
|
56
128
|
|
|
57
129
|
if (relField.many) {
|
|
58
|
-
lines.push(` ${fieldName}
|
|
130
|
+
lines.push(` ${fieldName}?: ${targetList}Output[]`) // Optional since only present with include
|
|
59
131
|
} else {
|
|
60
132
|
lines.push(` ${fieldName}Id: string | null`)
|
|
61
|
-
lines.push(` ${fieldName}
|
|
133
|
+
lines.push(` ${fieldName}?: ${targetList}Output | null`) // Optional since only present with include
|
|
62
134
|
}
|
|
63
135
|
} else {
|
|
64
136
|
const tsType = mapFieldTypeToTypeScript(fieldConfig)
|
|
@@ -72,11 +144,18 @@ function generateModelType(listName: string, fields: Record<string, FieldConfig>
|
|
|
72
144
|
|
|
73
145
|
lines.push(' createdAt: Date')
|
|
74
146
|
lines.push(' updatedAt: Date')
|
|
75
|
-
lines.push('}')
|
|
147
|
+
lines.push('} & ' + listName + 'VirtualFields') // Include virtual fields
|
|
76
148
|
|
|
77
149
|
return lines.join('\n')
|
|
78
150
|
}
|
|
79
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
|
+
|
|
80
159
|
/**
|
|
81
160
|
* Generate CreateInput type
|
|
82
161
|
*/
|
|
@@ -86,6 +165,12 @@ function generateCreateInputType(listName: string, fields: Record<string, FieldC
|
|
|
86
165
|
lines.push(`export type ${listName}CreateInput = {`)
|
|
87
166
|
|
|
88
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
|
+
|
|
89
174
|
if (fieldConfig.type === 'relationship') {
|
|
90
175
|
const relField = fieldConfig as RelationshipField
|
|
91
176
|
|
|
@@ -118,6 +203,12 @@ function generateUpdateInputType(listName: string, fields: Record<string, FieldC
|
|
|
118
203
|
lines.push(`export type ${listName}UpdateInput = {`)
|
|
119
204
|
|
|
120
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
|
+
|
|
121
212
|
if (fieldConfig.type === 'relationship') {
|
|
122
213
|
const relField = fieldConfig as RelationshipField
|
|
123
214
|
|
|
@@ -170,19 +261,153 @@ function generateWhereInputType(listName: string, fields: Record<string, FieldCo
|
|
|
170
261
|
}
|
|
171
262
|
|
|
172
263
|
/**
|
|
173
|
-
* Generate
|
|
264
|
+
* Generate hook types that reference Prisma input types
|
|
174
265
|
*/
|
|
175
|
-
function
|
|
266
|
+
function generateHookTypes(listName: string): string {
|
|
176
267
|
const lines: string[] = []
|
|
177
268
|
|
|
178
|
-
lines.push(
|
|
179
|
-
lines.push(
|
|
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')
|
|
180
408
|
lines.push(' session: TSession')
|
|
181
|
-
lines.push(' prisma: PrismaClient')
|
|
182
|
-
lines.push(' storage: StorageUtils')
|
|
183
409
|
lines.push(' serverAction: (props: ServerActionProps) => Promise<unknown>')
|
|
184
410
|
lines.push(' sudo: () => Context<TSession>')
|
|
185
|
-
lines.push(' _isSudo: boolean')
|
|
186
411
|
lines.push('}')
|
|
187
412
|
|
|
188
413
|
return lines.join('\n')
|
|
@@ -249,9 +474,10 @@ export function generateTypes(config: OpenSaasConfig): string {
|
|
|
249
474
|
// Add necessary imports
|
|
250
475
|
// Use alias for Session to avoid conflicts if user has a list named "Session"
|
|
251
476
|
lines.push(
|
|
252
|
-
"import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB } from '@opensaas/stack-core'",
|
|
477
|
+
"import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB, AccessContext } from '@opensaas/stack-core'",
|
|
253
478
|
)
|
|
254
|
-
lines.push("import type { PrismaClient } from './prisma-client'")
|
|
479
|
+
lines.push("import type { PrismaClient, Prisma } from './prisma-client/client'")
|
|
480
|
+
lines.push("import type { PluginServices } from './plugin-types'")
|
|
255
481
|
|
|
256
482
|
// Add field-specific imports
|
|
257
483
|
const fieldImports = collectFieldImports(config)
|
|
@@ -265,7 +491,15 @@ export function generateTypes(config: OpenSaasConfig): string {
|
|
|
265
491
|
|
|
266
492
|
// Generate types for each list
|
|
267
493
|
for (const [listName, listConfig] of Object.entries(config.lists)) {
|
|
268
|
-
|
|
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))
|
|
269
503
|
lines.push('')
|
|
270
504
|
lines.push(generateCreateInputType(listName, listConfig.fields))
|
|
271
505
|
lines.push('')
|
|
@@ -273,10 +507,16 @@ export function generateTypes(config: OpenSaasConfig): string {
|
|
|
273
507
|
lines.push('')
|
|
274
508
|
lines.push(generateWhereInputType(listName, listConfig.fields))
|
|
275
509
|
lines.push('')
|
|
510
|
+
lines.push(generateHookTypes(listName))
|
|
511
|
+
lines.push('')
|
|
276
512
|
}
|
|
277
513
|
|
|
514
|
+
// Generate CustomDB interface
|
|
515
|
+
lines.push(generateCustomDBType(config))
|
|
516
|
+
lines.push('')
|
|
517
|
+
|
|
278
518
|
// Generate Context type
|
|
279
|
-
lines.push(generateContextType())
|
|
519
|
+
lines.push(generateContextType(config))
|
|
280
520
|
|
|
281
521
|
return lines.join('\n')
|
|
282
522
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from 'commander'
|
|
|
4
4
|
import { generateCommand } from './commands/generate.js'
|
|
5
5
|
import { initCommand } from './commands/init.js'
|
|
6
6
|
import { devCommand } from './commands/dev.js'
|
|
7
|
+
import { createMCPCommand } from './commands/mcp.js'
|
|
7
8
|
|
|
8
9
|
const program = new Command()
|
|
9
10
|
|
|
@@ -35,4 +36,7 @@ program
|
|
|
35
36
|
await devCommand()
|
|
36
37
|
})
|
|
37
38
|
|
|
39
|
+
// Add MCP command group
|
|
40
|
+
program.addCommand(createMCPCommand())
|
|
41
|
+
|
|
38
42
|
program.parse()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation provider - Fetches documentation from the hosted docs site
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DocumentationLookup } from './types.js'
|
|
6
|
+
|
|
7
|
+
interface SearchResult {
|
|
8
|
+
content: string
|
|
9
|
+
metadata: {
|
|
10
|
+
title?: string
|
|
11
|
+
slug?: string
|
|
12
|
+
section?: string
|
|
13
|
+
}
|
|
14
|
+
score: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SearchResponse {
|
|
18
|
+
results: SearchResult[]
|
|
19
|
+
query: string
|
|
20
|
+
count: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class OpenSaasDocumentationProvider {
|
|
24
|
+
private readonly DOCS_API = 'https://stack.opensaas.au/api/search'
|
|
25
|
+
private cache = new Map<string, { data: DocumentationLookup; timestamp: number }>()
|
|
26
|
+
private readonly CACHE_TTL = 1000 * 60 * 30 // 30 minutes
|
|
27
|
+
|
|
28
|
+
// Topic mappings for user-friendly queries
|
|
29
|
+
private topicMappings: Record<string, string> = {
|
|
30
|
+
fields: 'field-types',
|
|
31
|
+
'field types': 'field-types',
|
|
32
|
+
'field type': 'field-types',
|
|
33
|
+
access: 'access-control',
|
|
34
|
+
'access control': 'access-control',
|
|
35
|
+
permissions: 'access-control',
|
|
36
|
+
auth: 'authentication',
|
|
37
|
+
authentication: 'authentication',
|
|
38
|
+
login: 'authentication',
|
|
39
|
+
'sign in': 'authentication',
|
|
40
|
+
hooks: 'hooks',
|
|
41
|
+
hook: 'hooks',
|
|
42
|
+
lifecycle: 'hooks',
|
|
43
|
+
plugins: 'plugin-system',
|
|
44
|
+
plugin: 'plugin-system',
|
|
45
|
+
rag: 'rag',
|
|
46
|
+
search: 'semantic-search',
|
|
47
|
+
'semantic search': 'semantic-search',
|
|
48
|
+
storage: 'file-storage',
|
|
49
|
+
files: 'file-storage',
|
|
50
|
+
upload: 'file-storage',
|
|
51
|
+
config: 'configuration',
|
|
52
|
+
configuration: 'configuration',
|
|
53
|
+
prisma: 'prisma-integration',
|
|
54
|
+
database: 'database-setup',
|
|
55
|
+
deployment: 'deployment',
|
|
56
|
+
deploy: 'deployment',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Search documentation by query
|
|
61
|
+
*/
|
|
62
|
+
async searchDocs(query: string, limit = 5, minScore = 0.7): Promise<DocumentationLookup> {
|
|
63
|
+
const cacheKey = `search:${query}:${limit}:${minScore}`
|
|
64
|
+
|
|
65
|
+
// Check cache
|
|
66
|
+
const cached = this.cache.get(cacheKey)
|
|
67
|
+
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
|
68
|
+
return cached.data
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(this.DOCS_API, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({ query, limit, minScore }),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Docs API error: ${response.statusText}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = (await response.json()) as SearchResponse
|
|
85
|
+
|
|
86
|
+
const docLookup: DocumentationLookup = {
|
|
87
|
+
topic: query,
|
|
88
|
+
content: this.formatSearchResults(data.results),
|
|
89
|
+
url: 'https://stack.opensaas.au/',
|
|
90
|
+
codeExamples: this.extractCodeExamples(data.results),
|
|
91
|
+
relatedTopics: this.extractRelatedTopics(data.results),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Cache the result
|
|
95
|
+
this.cache.set(cacheKey, { data: docLookup, timestamp: Date.now() })
|
|
96
|
+
|
|
97
|
+
return docLookup
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Error fetching documentation:', error)
|
|
100
|
+
return this.getFallbackDocs(query)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get documentation for a specific topic
|
|
106
|
+
*/
|
|
107
|
+
async getTopicDocs(topic: string): Promise<DocumentationLookup> {
|
|
108
|
+
// Normalize topic using mappings
|
|
109
|
+
const normalizedTopic = this.topicMappings[topic.toLowerCase()] || topic
|
|
110
|
+
|
|
111
|
+
return this.searchDocs(normalizedTopic, 3, 0.8)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format search results into readable content
|
|
116
|
+
*/
|
|
117
|
+
private formatSearchResults(results: SearchResult[]): string {
|
|
118
|
+
if (results.length === 0) {
|
|
119
|
+
return 'No documentation found for this query.'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return results
|
|
123
|
+
.map((result, index) => {
|
|
124
|
+
const title = result.metadata.title || `Section ${index + 1}`
|
|
125
|
+
const section = result.metadata.section || ''
|
|
126
|
+
const score = (result.score * 100).toFixed(0)
|
|
127
|
+
|
|
128
|
+
return `### ${title}${section ? ` (${section})` : ''} [Relevance: ${score}%]\n\n${result.content}\n`
|
|
129
|
+
})
|
|
130
|
+
.join('\n---\n\n')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract code examples from search results
|
|
135
|
+
*/
|
|
136
|
+
private extractCodeExamples(results: SearchResult[]): string[] {
|
|
137
|
+
const codeExamples: string[] = []
|
|
138
|
+
const codeBlockRegex = /```[\s\S]*?```/g
|
|
139
|
+
|
|
140
|
+
for (const result of results) {
|
|
141
|
+
const matches = result.content.match(codeBlockRegex)
|
|
142
|
+
if (matches) {
|
|
143
|
+
codeExamples.push(...matches)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return codeExamples
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Extract related topics from search results
|
|
152
|
+
*/
|
|
153
|
+
private extractRelatedTopics(results: SearchResult[]): string[] {
|
|
154
|
+
const topics = new Set<string>()
|
|
155
|
+
|
|
156
|
+
for (const result of results) {
|
|
157
|
+
if (result.metadata.section) {
|
|
158
|
+
topics.add(result.metadata.section)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Array.from(topics)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Fallback documentation when API is unavailable
|
|
167
|
+
*/
|
|
168
|
+
private getFallbackDocs(query: string): DocumentationLookup {
|
|
169
|
+
return {
|
|
170
|
+
topic: query,
|
|
171
|
+
content: `Unable to fetch documentation from the docs site at this time.
|
|
172
|
+
|
|
173
|
+
Please visit the OpenSaaS Stack documentation directly:
|
|
174
|
+
https://stack.opensaas.au/
|
|
175
|
+
|
|
176
|
+
For ${query}, you can also check:
|
|
177
|
+
- GitHub repository: https://github.com/OpenSaasAU/stack
|
|
178
|
+
- Example projects in the examples/ directory`,
|
|
179
|
+
url: 'https://stack.opensaas.au/',
|
|
180
|
+
codeExamples: [],
|
|
181
|
+
relatedTopics: [],
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Clear expired cache entries
|
|
187
|
+
*/
|
|
188
|
+
clearExpiredCache(): void {
|
|
189
|
+
const now = Date.now()
|
|
190
|
+
for (const [key, value] of this.cache.entries()) {
|
|
191
|
+
if (now - value.timestamp >= this.CACHE_TTL) {
|
|
192
|
+
this.cache.delete(key)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clear all cache
|
|
199
|
+
*/
|
|
200
|
+
clearCache(): void {
|
|
201
|
+
this.cache.clear()
|
|
202
|
+
}
|
|
203
|
+
}
|