@opensaas/stack-cli 0.1.4 → 0.1.6

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.
@@ -1,5 +1,110 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
+ exports[`Context Generator > generateContext > should export rawOpensaasContext 1`] = `
4
+ "/**
5
+ * Auto-generated context factory
6
+ *
7
+ * This module provides a simple API for creating OpenSaas contexts.
8
+ * It abstracts away Prisma client management and configuration.
9
+ *
10
+ * DO NOT EDIT - This file is automatically generated by 'pnpm generate'
11
+ */
12
+
13
+ import { getContext as getOpensaasContext } from '@opensaas/stack-core'
14
+ import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
15
+ import { PrismaClient } from './prisma-client'
16
+ import type { Context } from './types'
17
+ import configOrPromise from '../opensaas.config'
18
+
19
+ // Resolve config if it's a Promise (when plugins are present)
20
+ const configPromise = Promise.resolve(configOrPromise)
21
+ let resolvedConfig: OpenSaasConfig | null = null
22
+
23
+ // Internal Prisma singleton - managed automatically
24
+ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
25
+ let prisma: PrismaClient | null = null
26
+
27
+ async function getPrisma() {
28
+ if (!prisma) {
29
+ if (!resolvedConfig) {
30
+ resolvedConfig = await configPromise
31
+ }
32
+ prisma = globalForPrisma.prisma ?? new PrismaClient()
33
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
34
+ }
35
+ return prisma
36
+ }
37
+
38
+ async function getConfig() {
39
+ if (!resolvedConfig) {
40
+ resolvedConfig = await configPromise
41
+ }
42
+ return resolvedConfig
43
+ }
44
+
45
+ /**
46
+ * Storage utilities (not configured)
47
+ */
48
+ const storage = {
49
+ uploadFile: async () => {
50
+ throw new Error('Storage is not configured. Add storage providers to your opensaas.config.ts')
51
+ },
52
+ uploadImage: async () => {
53
+ throw new Error('Storage is not configured. Add storage providers to your opensaas.config.ts')
54
+ },
55
+ deleteFile: async () => {
56
+ throw new Error('Storage is not configured. Add storage providers to your opensaas.config.ts')
57
+ },
58
+ deleteImage: async () => {
59
+ throw new Error('Storage is not configured. Add storage providers to your opensaas.config.ts')
60
+ },
61
+ }
62
+
63
+ /**
64
+ * Get OpenSaas context with optional session
65
+ *
66
+ * @param session - Optional session object (structure defined by your application)
67
+ *
68
+ * @example
69
+ * \`\`\`typescript
70
+ * // Anonymous access
71
+ * const context = await getContext()
72
+ * const posts = await context.db.post.findMany()
73
+ *
74
+ * // Authenticated access
75
+ * const context = await getContext({ userId: 'user-123' })
76
+ * const myPosts = await context.db.post.findMany()
77
+ *
78
+ * // With custom session type
79
+ * type CustomSession = { userId: string; email: string; role: string } | null
80
+ * const context = await getContext<CustomSession>({ userId: '123', email: 'user@example.com', role: 'admin' })
81
+ * // context.session is now typed as CustomSession
82
+ * \`\`\`
83
+ */
84
+ export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
85
+ const config = await getConfig()
86
+ const prismaClient = await getPrisma()
87
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
88
+ }
89
+
90
+ /**
91
+ * Raw context for synchronous initialization (e.g., Better-auth setup)
92
+ * This is only available after config is resolved, use with caution
93
+ */
94
+ export const rawOpensaasContext = (async () => {
95
+ const config = await getConfig()
96
+ const prismaClient = await getPrisma()
97
+ return getOpensaasContext(config, prismaClient, null, storage)
98
+ })()
99
+
100
+ /**
101
+ * Re-export resolved config for use in admin pages and server actions
102
+ * This is a promise that resolves to the config
103
+ */
104
+ export const config = getConfig()
105
+ "
106
+ `;
107
+
3
108
  exports[`Context Generator > generateContext > should generate context factory with custom Prisma client constructor 1`] = `
4
109
  "/**
5
110
  * Auto-generated context factory
@@ -11,15 +116,36 @@ exports[`Context Generator > generateContext > should generate context factory w
11
116
  */
12
117
 
13
118
  import { getContext as getOpensaasContext } from '@opensaas/stack-core'
14
- import type { Session as OpensaasSession } from '@opensaas/stack-core'
119
+ import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
15
120
  import { PrismaClient } from './prisma-client'
16
121
  import type { Context } from './types'
17
- import config from '../opensaas.config'
122
+ import configOrPromise from '../opensaas.config'
123
+
124
+ // Resolve config if it's a Promise (when plugins are present)
125
+ const configPromise = Promise.resolve(configOrPromise)
126
+ let resolvedConfig: OpenSaasConfig | null = null
18
127
 
19
128
  // Internal Prisma singleton - managed automatically
20
129
  const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
21
- const prisma = globalForPrisma.prisma ?? config.db.prismaClientConstructor!(PrismaClient)
22
- if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
130
+ let prisma: PrismaClient | null = null
131
+
132
+ async function getPrisma() {
133
+ if (!prisma) {
134
+ if (!resolvedConfig) {
135
+ resolvedConfig = await configPromise
136
+ }
137
+ prisma = globalForPrisma.prisma ?? resolvedConfig.db.prismaClientConstructor!(PrismaClient)
138
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
139
+ }
140
+ return prisma
141
+ }
142
+
143
+ async function getConfig() {
144
+ if (!resolvedConfig) {
145
+ resolvedConfig = await configPromise
146
+ }
147
+ return resolvedConfig
148
+ }
23
149
 
24
150
  /**
25
151
  * Storage utilities (not configured)
@@ -47,24 +173,40 @@ const storage = {
47
173
  * @example
48
174
  * \`\`\`typescript
49
175
  * // Anonymous access
50
- * const context = getContext()
176
+ * const context = await getContext()
51
177
  * const posts = await context.db.post.findMany()
52
178
  *
53
179
  * // Authenticated access
54
- * const context = getContext({ userId: 'user-123' })
180
+ * const context = await getContext({ userId: 'user-123' })
55
181
  * const myPosts = await context.db.post.findMany()
56
182
  *
57
183
  * // With custom session type
58
184
  * type CustomSession = { userId: string; email: string; role: string } | null
59
- * const context = getContext<CustomSession>({ userId: '123', email: 'user@example.com', role: 'admin' })
185
+ * const context = await getContext<CustomSession>({ userId: '123', email: 'user@example.com', role: 'admin' })
60
186
  * // context.session is now typed as CustomSession
61
187
  * \`\`\`
62
188
  */
63
- export function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Context<TSession> {
64
- return getOpensaasContext(config, prisma, session ?? null, storage) as Context<TSession>
189
+ export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
190
+ const config = await getConfig()
191
+ const prismaClient = await getPrisma()
192
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
65
193
  }
66
194
 
67
- export const rawOpensaasContext = getContext()
195
+ /**
196
+ * Raw context for synchronous initialization (e.g., Better-auth setup)
197
+ * This is only available after config is resolved, use with caution
198
+ */
199
+ export const rawOpensaasContext = (async () => {
200
+ const config = await getConfig()
201
+ const prismaClient = await getPrisma()
202
+ return getOpensaasContext(config, prismaClient, null, storage)
203
+ })()
204
+
205
+ /**
206
+ * Re-export resolved config for use in admin pages and server actions
207
+ * This is a promise that resolves to the config
208
+ */
209
+ export const config = getConfig()
68
210
  "
69
211
  `;
70
212
 
@@ -79,15 +221,36 @@ exports[`Context Generator > generateContext > should generate context factory w
79
221
  */
80
222
 
81
223
  import { getContext as getOpensaasContext } from '@opensaas/stack-core'
82
- import type { Session as OpensaasSession } from '@opensaas/stack-core'
224
+ import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
83
225
  import { PrismaClient } from './prisma-client'
84
226
  import type { Context } from './types'
85
- import config from '../opensaas.config'
227
+ import configOrPromise from '../opensaas.config'
228
+
229
+ // Resolve config if it's a Promise (when plugins are present)
230
+ const configPromise = Promise.resolve(configOrPromise)
231
+ let resolvedConfig: OpenSaasConfig | null = null
86
232
 
87
233
  // Internal Prisma singleton - managed automatically
88
234
  const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
89
- const prisma = globalForPrisma.prisma ?? new PrismaClient()
90
- if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
235
+ let prisma: PrismaClient | null = null
236
+
237
+ async function getPrisma() {
238
+ if (!prisma) {
239
+ if (!resolvedConfig) {
240
+ resolvedConfig = await configPromise
241
+ }
242
+ prisma = globalForPrisma.prisma ?? new PrismaClient()
243
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
244
+ }
245
+ return prisma
246
+ }
247
+
248
+ async function getConfig() {
249
+ if (!resolvedConfig) {
250
+ resolvedConfig = await configPromise
251
+ }
252
+ return resolvedConfig
253
+ }
91
254
 
92
255
  /**
93
256
  * Storage utilities (not configured)
@@ -115,23 +278,39 @@ const storage = {
115
278
  * @example
116
279
  * \`\`\`typescript
117
280
  * // Anonymous access
118
- * const context = getContext()
281
+ * const context = await getContext()
119
282
  * const posts = await context.db.post.findMany()
120
283
  *
121
284
  * // Authenticated access
122
- * const context = getContext({ userId: 'user-123' })
285
+ * const context = await getContext({ userId: 'user-123' })
123
286
  * const myPosts = await context.db.post.findMany()
124
287
  *
125
288
  * // With custom session type
126
289
  * type CustomSession = { userId: string; email: string; role: string } | null
127
- * const context = getContext<CustomSession>({ userId: '123', email: 'user@example.com', role: 'admin' })
290
+ * const context = await getContext<CustomSession>({ userId: '123', email: 'user@example.com', role: 'admin' })
128
291
  * // context.session is now typed as CustomSession
129
292
  * \`\`\`
130
293
  */
131
- export function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Context<TSession> {
132
- return getOpensaasContext(config, prisma, session ?? null, storage) as Context<TSession>
294
+ export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
295
+ const config = await getConfig()
296
+ const prismaClient = await getPrisma()
297
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
133
298
  }
134
299
 
135
- export const rawOpensaasContext = getContext()
300
+ /**
301
+ * Raw context for synchronous initialization (e.g., Better-auth setup)
302
+ * This is only available after config is resolved, use with caution
303
+ */
304
+ export const rawOpensaasContext = (async () => {
305
+ const config = await getConfig()
306
+ const prismaClient = await getPrisma()
307
+ return getOpensaasContext(config, prismaClient, null, storage)
308
+ })()
309
+
310
+ /**
311
+ * Re-export resolved config for use in admin pages and server actions
312
+ * This is a promise that resolves to the config
313
+ */
314
+ export const config = getConfig()
136
315
  "
137
316
  `;
@@ -95,9 +95,9 @@ describe('Context Generator', () => {
95
95
  const context = generateContext(config)
96
96
 
97
97
  expect(context).toContain('// Anonymous access')
98
- expect(context).toContain('const context = getContext()')
98
+ expect(context).toContain('const context = await getContext()')
99
99
  expect(context).toContain('// Authenticated access')
100
- expect(context).toContain("const context = getContext({ userId: 'user-123' })")
100
+ expect(context).toContain("const context = await getContext({ userId: 'user-123' })")
101
101
  })
102
102
 
103
103
  it('should export rawOpensaasContext', () => {
@@ -111,7 +111,7 @@ describe('Context Generator', () => {
111
111
 
112
112
  const context = generateContext(config)
113
113
 
114
- expect(context).toContain('export const rawOpensaasContext = getContext()')
114
+ expect(context).toMatchSnapshot()
115
115
  })
116
116
 
117
117
  it('should type session parameter correctly', () => {
@@ -128,18 +128,5 @@ describe('Context Generator', () => {
128
128
  expect(context).toContain('session?: TSession')
129
129
  expect(context).toContain('<TSession extends OpensaasSession = OpensaasSession>')
130
130
  })
131
-
132
- it('should call getOpensaasContext with correct arguments', () => {
133
- const config: OpenSaasConfig = {
134
- db: {
135
- provider: 'sqlite',
136
- url: 'file:./dev.db',
137
- },
138
- lists: {},
139
- }
140
-
141
- const context = generateContext(config)
142
- expect(context).toContain('getOpensaasContext(config, prisma, session ?? null, storage)')
143
- })
144
131
  })
145
132
  })
@@ -17,7 +17,7 @@ export function generateContext(config: OpenSaasConfig): string {
17
17
 
18
18
  // Generate the Prisma client instantiation code
19
19
  const prismaInstantiation = hasCustomConstructor
20
- ? `config.db.prismaClientConstructor!(PrismaClient)`
20
+ ? `resolvedConfig.db.prismaClientConstructor!(PrismaClient)`
21
21
  : `new PrismaClient()`
22
22
 
23
23
  // Generate storage utilities if storage is configured
@@ -47,21 +47,25 @@ async function getStorageRuntime() {
47
47
  */
48
48
  const storage = {
49
49
  uploadFile: async (providerName: string, file: File, buffer: Buffer, options?: unknown) => {
50
+ const config = await getConfig()
50
51
  const runtime = await getStorageRuntime()
51
52
  return runtime.uploadFile(config, providerName, { file, buffer }, options as any)
52
53
  },
53
54
 
54
55
  uploadImage: async (providerName: string, file: File, buffer: Buffer, options?: unknown) => {
56
+ const config = await getConfig()
55
57
  const runtime = await getStorageRuntime()
56
58
  return runtime.uploadImage(config, providerName, { file, buffer }, options as any)
57
59
  },
58
60
 
59
61
  deleteFile: async (providerName: string, filename: string) => {
62
+ const config = await getConfig()
60
63
  const runtime = await getStorageRuntime()
61
64
  return runtime.deleteFile(config, providerName, filename)
62
65
  },
63
66
 
64
67
  deleteImage: async (metadata: unknown) => {
68
+ const config = await getConfig()
65
69
  const runtime = await getStorageRuntime()
66
70
  return runtime.deleteImage(config, metadata as any)
67
71
  },
@@ -87,6 +91,7 @@ const storage = {
87
91
  }
88
92
  `
89
93
 
94
+ // Always use async version for consistency
90
95
  return `/**
91
96
  * Auto-generated context factory
92
97
  *
@@ -97,15 +102,36 @@ const storage = {
97
102
  */
98
103
 
99
104
  import { getContext as getOpensaasContext } from '@opensaas/stack-core'
100
- import type { Session as OpensaasSession } from '@opensaas/stack-core'
105
+ import type { Session as OpensaasSession, OpenSaasConfig } from '@opensaas/stack-core'
101
106
  import { PrismaClient } from './prisma-client'
102
107
  import type { Context } from './types'
103
- import config from '../opensaas.config'
108
+ import configOrPromise from '../opensaas.config'
109
+
110
+ // Resolve config if it's a Promise (when plugins are present)
111
+ const configPromise = Promise.resolve(configOrPromise)
112
+ let resolvedConfig: OpenSaasConfig | null = null
104
113
 
105
114
  // Internal Prisma singleton - managed automatically
106
115
  const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
107
- const prisma = globalForPrisma.prisma ?? ${prismaInstantiation}
108
- if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
116
+ let prisma: PrismaClient | null = null
117
+
118
+ async function getPrisma() {
119
+ if (!prisma) {
120
+ if (!resolvedConfig) {
121
+ resolvedConfig = await configPromise
122
+ }
123
+ prisma = globalForPrisma.prisma ?? ${prismaInstantiation}
124
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
125
+ }
126
+ return prisma
127
+ }
128
+
129
+ async function getConfig() {
130
+ if (!resolvedConfig) {
131
+ resolvedConfig = await configPromise
132
+ }
133
+ return resolvedConfig
134
+ }
109
135
  ${storageUtilities}
110
136
  /**
111
137
  * Get OpenSaas context with optional session
@@ -115,24 +141,40 @@ ${storageUtilities}
115
141
  * @example
116
142
  * \`\`\`typescript
117
143
  * // Anonymous access
118
- * const context = getContext()
144
+ * const context = await getContext()
119
145
  * const posts = await context.db.post.findMany()
120
146
  *
121
147
  * // Authenticated access
122
- * const context = getContext({ userId: 'user-123' })
148
+ * const context = await getContext({ userId: 'user-123' })
123
149
  * const myPosts = await context.db.post.findMany()
124
150
  *
125
151
  * // With custom session type
126
152
  * type CustomSession = { userId: string; email: string; role: string } | null
127
- * const context = getContext<CustomSession>({ userId: '123', email: 'user@example.com', role: 'admin' })
153
+ * const context = await getContext<CustomSession>({ userId: '123', email: 'user@example.com', role: 'admin' })
128
154
  * // context.session is now typed as CustomSession
129
155
  * \`\`\`
130
156
  */
131
- export function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Context<TSession> {
132
- return getOpensaasContext(config, prisma, session ?? null, storage) as Context<TSession>
157
+ export async function getContext<TSession extends OpensaasSession = OpensaasSession>(session?: TSession): Promise<Context<TSession>> {
158
+ const config = await getConfig()
159
+ const prismaClient = await getPrisma()
160
+ return getOpensaasContext(config, prismaClient, session ?? null, storage) as Context<TSession>
133
161
  }
134
162
 
135
- export const rawOpensaasContext = getContext()
163
+ /**
164
+ * Raw context for synchronous initialization (e.g., Better-auth setup)
165
+ * This is only available after config is resolved, use with caution
166
+ */
167
+ export const rawOpensaasContext = (async () => {
168
+ const config = await getConfig()
169
+ const prismaClient = await getPrisma()
170
+ return getOpensaasContext(config, prismaClient, null, storage)
171
+ })()
172
+
173
+ /**
174
+ * Re-export resolved config for use in admin pages and server actions
175
+ * This is a promise that resolves to the config
176
+ */
177
+ export const config = getConfig()
136
178
  `
137
179
  }
138
180
 
@@ -186,6 +186,51 @@ function generateContextType(): string {
186
186
  return lines.join('\n')
187
187
  }
188
188
 
189
+ /**
190
+ * Collect TypeScript imports from field configurations
191
+ */
192
+ function collectFieldImports(config: OpenSaasConfig): Array<{
193
+ names: string[]
194
+ from: string
195
+ typeOnly: boolean
196
+ }> {
197
+ const importsMap = new Map<string, { names: Set<string>; typeOnly: boolean }>()
198
+
199
+ // Iterate through all lists and fields
200
+ for (const listConfig of Object.values(config.lists)) {
201
+ for (const fieldConfig of Object.values(listConfig.fields)) {
202
+ // Check if field provides imports
203
+ if (fieldConfig.getTypeScriptImports) {
204
+ const imports = fieldConfig.getTypeScriptImports()
205
+ for (const imp of imports) {
206
+ const existing = importsMap.get(imp.from)
207
+ if (existing) {
208
+ // Merge names into existing import
209
+ imp.names.forEach((name) => existing.names.add(name))
210
+ // If either import is not type-only, make the merged import not type-only
211
+ if (imp.typeOnly === false) {
212
+ existing.typeOnly = false
213
+ }
214
+ } else {
215
+ // Add new import
216
+ importsMap.set(imp.from, {
217
+ names: new Set(imp.names),
218
+ typeOnly: imp.typeOnly ?? true,
219
+ })
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ // Convert map to array
227
+ return Array.from(importsMap.entries()).map(([from, { names, typeOnly }]) => ({
228
+ names: Array.from(names).sort(),
229
+ from,
230
+ typeOnly,
231
+ }))
232
+ }
233
+
189
234
  /**
190
235
  * Generate all TypeScript types from config
191
236
  */
@@ -205,6 +250,15 @@ export function generateTypes(config: OpenSaasConfig): string {
205
250
  "import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB } from '@opensaas/stack-core'",
206
251
  )
207
252
  lines.push("import type { PrismaClient } from './prisma-client'")
253
+
254
+ // Add field-specific imports
255
+ const fieldImports = collectFieldImports(config)
256
+ for (const imp of fieldImports) {
257
+ const typePrefix = imp.typeOnly ? 'type ' : ''
258
+ const names = imp.names.join(', ')
259
+ lines.push(`import ${typePrefix}{ ${names} } from '${imp.from}'`)
260
+ }
261
+
208
262
  lines.push('')
209
263
 
210
264
  // Generate types for each list