@opensaas/stack-core 0.1.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 (95) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/README.md +447 -0
  3. package/dist/access/engine.d.ts +73 -0
  4. package/dist/access/engine.d.ts.map +1 -0
  5. package/dist/access/engine.js +244 -0
  6. package/dist/access/engine.js.map +1 -0
  7. package/dist/access/field-transforms.d.ts +47 -0
  8. package/dist/access/field-transforms.d.ts.map +1 -0
  9. package/dist/access/field-transforms.js +2 -0
  10. package/dist/access/field-transforms.js.map +1 -0
  11. package/dist/access/index.d.ts +3 -0
  12. package/dist/access/index.d.ts.map +1 -0
  13. package/dist/access/index.js +2 -0
  14. package/dist/access/index.js.map +1 -0
  15. package/dist/access/types.d.ts +83 -0
  16. package/dist/access/types.d.ts.map +1 -0
  17. package/dist/access/types.js +2 -0
  18. package/dist/access/types.js.map +1 -0
  19. package/dist/config/index.d.ts +39 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +38 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/types.d.ts +413 -0
  24. package/dist/config/types.d.ts.map +1 -0
  25. package/dist/config/types.js +2 -0
  26. package/dist/config/types.js.map +1 -0
  27. package/dist/context/index.d.ts +31 -0
  28. package/dist/context/index.d.ts.map +1 -0
  29. package/dist/context/index.js +524 -0
  30. package/dist/context/index.js.map +1 -0
  31. package/dist/context/nested-operations.d.ts +10 -0
  32. package/dist/context/nested-operations.d.ts.map +1 -0
  33. package/dist/context/nested-operations.js +261 -0
  34. package/dist/context/nested-operations.js.map +1 -0
  35. package/dist/fields/index.d.ts +78 -0
  36. package/dist/fields/index.d.ts.map +1 -0
  37. package/dist/fields/index.js +381 -0
  38. package/dist/fields/index.js.map +1 -0
  39. package/dist/hooks/index.d.ts +58 -0
  40. package/dist/hooks/index.d.ts.map +1 -0
  41. package/dist/hooks/index.js +79 -0
  42. package/dist/hooks/index.js.map +1 -0
  43. package/dist/index.d.ts +11 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +12 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/lib/case-utils.d.ts +49 -0
  48. package/dist/lib/case-utils.d.ts.map +1 -0
  49. package/dist/lib/case-utils.js +68 -0
  50. package/dist/lib/case-utils.js.map +1 -0
  51. package/dist/lib/case-utils.test.d.ts +2 -0
  52. package/dist/lib/case-utils.test.d.ts.map +1 -0
  53. package/dist/lib/case-utils.test.js +101 -0
  54. package/dist/lib/case-utils.test.js.map +1 -0
  55. package/dist/utils/password.d.ts +81 -0
  56. package/dist/utils/password.d.ts.map +1 -0
  57. package/dist/utils/password.js +132 -0
  58. package/dist/utils/password.js.map +1 -0
  59. package/dist/validation/schema.d.ts +17 -0
  60. package/dist/validation/schema.d.ts.map +1 -0
  61. package/dist/validation/schema.js +42 -0
  62. package/dist/validation/schema.js.map +1 -0
  63. package/dist/validation/schema.test.d.ts +2 -0
  64. package/dist/validation/schema.test.d.ts.map +1 -0
  65. package/dist/validation/schema.test.js +143 -0
  66. package/dist/validation/schema.test.js.map +1 -0
  67. package/docs/type-distribution-fix.md +136 -0
  68. package/package.json +48 -0
  69. package/src/access/engine.ts +360 -0
  70. package/src/access/field-transforms.ts +99 -0
  71. package/src/access/index.ts +20 -0
  72. package/src/access/types.ts +103 -0
  73. package/src/config/index.ts +71 -0
  74. package/src/config/types.ts +478 -0
  75. package/src/context/index.ts +814 -0
  76. package/src/context/nested-operations.ts +412 -0
  77. package/src/fields/index.ts +438 -0
  78. package/src/hooks/index.ts +132 -0
  79. package/src/index.ts +62 -0
  80. package/src/lib/case-utils.test.ts +127 -0
  81. package/src/lib/case-utils.ts +74 -0
  82. package/src/utils/password.ts +147 -0
  83. package/src/validation/schema.test.ts +171 -0
  84. package/src/validation/schema.ts +59 -0
  85. package/tests/access-relationships.test.ts +613 -0
  86. package/tests/access.test.ts +499 -0
  87. package/tests/config.test.ts +195 -0
  88. package/tests/context.test.ts +248 -0
  89. package/tests/hooks.test.ts +417 -0
  90. package/tests/password-type-distribution.test.ts +155 -0
  91. package/tests/password-types.test.ts +147 -0
  92. package/tests/password.test.ts +249 -0
  93. package/tsconfig.json +12 -0
  94. package/tsconfig.tsbuildinfo +1 -0
  95. package/vitest.config.ts +27 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Session type - can be extended by users
3
+ */
4
+ export type Session = {
5
+ userId?: string
6
+ [key: string]: unknown
7
+ } | null
8
+
9
+ /**
10
+ * Generic Prisma model delegate type
11
+ */
12
+ export type PrismaModelDelegate = {
13
+ findUnique: (args: unknown) => Promise<unknown>
14
+ findFirst: (args: unknown) => Promise<unknown>
15
+ findMany: (args: unknown) => Promise<unknown[]>
16
+ create: (args: unknown) => Promise<unknown>
17
+ update: (args: unknown) => Promise<unknown>
18
+ delete: (args: unknown) => Promise<unknown>
19
+ count: (args?: unknown) => Promise<number>
20
+ }
21
+
22
+ /**
23
+ * Generic Prisma client type
24
+ * This is intentionally permissive to allow actual PrismaClient types
25
+ * Uses `any` because Prisma generates highly complex client types that are difficult to constrain
26
+ * This type is used as a generic constraint and the actual type safety comes from TPrisma parameter
27
+ */
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ export type PrismaClientLike = any
30
+
31
+ /**
32
+ * Map Prisma client to access-controlled database context
33
+ * Preserves Prisma's type information for each model
34
+ */
35
+ export type AccessControlledDB<TPrisma extends PrismaClientLike> = {
36
+ [K in keyof TPrisma]: TPrisma[K] extends {
37
+ // Uses `any` in conditional type checks to verify Prisma model shape
38
+ // This is a standard TypeScript pattern for checking if a property exists with any signature
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ findUnique: any
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ findMany: any
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ create: any
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ update: any
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ delete: any
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ count: any
51
+ }
52
+ ? {
53
+ findUnique: TPrisma[K]['findUnique']
54
+ findMany: TPrisma[K]['findMany']
55
+ create: TPrisma[K]['create']
56
+ update: TPrisma[K]['update']
57
+ delete: TPrisma[K]['delete']
58
+ count: TPrisma[K]['count']
59
+ }
60
+ : never
61
+ } & {
62
+ // Add index signature for runtime string access (e.g., db[getDbKey(listName)])
63
+ // Uses `any` because models can have any shape from Prisma schema
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ [key: string]: any
66
+ }
67
+
68
+ /**
69
+ * Context type (simplified for access control)
70
+ */
71
+ export type AccessContext<TPrisma extends PrismaClientLike = PrismaClientLike> = {
72
+ session: Session
73
+ prisma: TPrisma
74
+ db: AccessControlledDB<TPrisma>
75
+ [key: string]: unknown
76
+ }
77
+
78
+ /**
79
+ * Prisma filter type - represents a where clause
80
+ * Uses Partial to allow filtering by any subset of fields
81
+ */
82
+ export type PrismaFilter<T = Record<string, unknown>> = Partial<Record<keyof T, unknown>>
83
+
84
+ /**
85
+ * Access control function type
86
+ * Can return:
87
+ * - boolean: true = allow, false = deny
88
+ * - PrismaFilter: Prisma where clause to filter results
89
+ */
90
+ export type AccessControl<T = Record<string, unknown>> = (args: {
91
+ session: Session
92
+ item?: T // Present for update/delete operations
93
+ context: AccessContext
94
+ }) => boolean | PrismaFilter<T> | Promise<boolean | PrismaFilter<T>>
95
+
96
+ /**
97
+ * Field-level access control
98
+ */
99
+ export type FieldAccess = {
100
+ read?: AccessControl
101
+ create?: AccessControl
102
+ update?: AccessControl
103
+ }
@@ -0,0 +1,71 @@
1
+ import type { OpenSaasConfig, ListConfig, FieldConfig, OperationAccess, Hooks } from './types.js'
2
+
3
+ /**
4
+ * Helper function to define configuration with type safety
5
+ */
6
+ export function config(config: OpenSaasConfig): OpenSaasConfig {
7
+ return config
8
+ }
9
+
10
+ /**
11
+ * Helper function to define a list with type safety
12
+ *
13
+ * Accepts raw field configs and transforms them to inject the item type T
14
+ * This enables proper typing in field hooks where item: T
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import type { User } from './.opensaas/types'
19
+ *
20
+ * User: list<User>({
21
+ * fields: {
22
+ * password: password({
23
+ * hooks: {
24
+ * resolveInput: async ({ inputValue, item }) => {
25
+ * // item is typed as User | undefined
26
+ * // inputValue is typed as string | undefined
27
+ * return hashPassword(inputValue)
28
+ * }
29
+ * }
30
+ * })
31
+ * }
32
+ * })
33
+ * ```
34
+ */
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ export function list<T = any>(config: {
37
+ fields: Record<string, FieldConfig>
38
+ access?: {
39
+ operation?: OperationAccess<T>
40
+ }
41
+ hooks?: Hooks<T>
42
+ }): ListConfig<T> {
43
+ // At runtime, field configs are unchanged
44
+ // At type level, they're transformed to inject T as the item type
45
+ return config as ListConfig<T>
46
+ }
47
+
48
+ // Re-export all types
49
+ export type {
50
+ OpenSaasConfig,
51
+ ListConfig,
52
+ FieldConfig,
53
+ BaseFieldConfig,
54
+ TextField,
55
+ IntegerField,
56
+ CheckboxField,
57
+ TimestampField,
58
+ PasswordField,
59
+ SelectField,
60
+ RelationshipField,
61
+ OperationAccess,
62
+ Hooks,
63
+ FieldHooks,
64
+ FieldsWithItemType,
65
+ DatabaseConfig,
66
+ SessionConfig,
67
+ UIConfig,
68
+ ThemeConfig,
69
+ ThemePreset,
70
+ ThemeColors,
71
+ } from './types.js'
@@ -0,0 +1,478 @@
1
+ import type { AccessControl, FieldAccess } from '../access/types.js'
2
+ import type { z } from 'zod'
3
+
4
+ /**
5
+ * Field configuration types
6
+ */
7
+ export type FieldType =
8
+ | 'text'
9
+ | 'integer'
10
+ | 'checkbox'
11
+ | 'timestamp'
12
+ | 'password'
13
+ | 'select'
14
+ | 'relationship'
15
+ | string // Allow custom field types from third-party packages
16
+
17
+ /**
18
+ * Field-level hooks for data transformation and side effects
19
+ * Allows field types to define custom behavior during operations
20
+ *
21
+ * @template TInput - Type of the input value (what goes into the database)
22
+ * @template TOutput - Type of the output value (what comes out of the database)
23
+ * @template TItem - Type of the parent item/record
24
+ */
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ export type FieldHooks<TInput = any, TOutput = TInput, TItem = any> = {
27
+ /**
28
+ * Transform field value before database write
29
+ * Called during create/update operations after list-level resolveInput but before validation
30
+ * This is where you should transform input data (e.g., hash passwords, normalize values)
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * resolveInput: async ({ inputValue, operation }) => {
35
+ * if (typeof inputValue === 'string' && !isHashedPassword(inputValue)) {
36
+ * return await hashPassword(inputValue)
37
+ * }
38
+ * return inputValue
39
+ * }
40
+ * ```
41
+ */
42
+ resolveInput?: (args: {
43
+ operation: 'create' | 'update'
44
+ inputValue: TInput | undefined
45
+ item?: TItem
46
+ listKey: string
47
+ fieldName: string
48
+ context: import('../access/types.js').AccessContext
49
+ }) => Promise<TInput | undefined> | TInput | undefined
50
+
51
+ /**
52
+ * Perform side effects before database write
53
+ * Called during create/update/delete operations after validation and access control
54
+ * This should ONLY contain side effects (logging, notifications, etc.), not data transformation
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * beforeOperation: async ({ resolvedValue, operation, item }) => {
59
+ * console.log(`About to ${operation} field with value:`, resolvedValue)
60
+ * await sendAuditLog({ operation, item })
61
+ * }
62
+ * ```
63
+ */
64
+ beforeOperation?: (args: {
65
+ operation: 'create' | 'update' | 'delete'
66
+ resolvedValue: TInput | undefined
67
+ item?: TItem
68
+ listKey: string
69
+ fieldName: string
70
+ context: import('../access/types.js').AccessContext
71
+ }) => Promise<void> | void
72
+
73
+ /**
74
+ * Perform side effects after database operation
75
+ * Called after any database operation (create/update/delete/query)
76
+ * This should ONLY contain side effects (logging, cache invalidation, etc.), not data transformation
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * afterOperation: async ({ operation, value, item }) => {
81
+ * await invalidateCache({ listKey, itemId: item.id })
82
+ * await sendWebhook({ operation, item })
83
+ * }
84
+ * ```
85
+ */
86
+ afterOperation?: (
87
+ args:
88
+ | {
89
+ operation: 'create' | 'update' | 'delete'
90
+ value: TInput | undefined
91
+ item: TItem
92
+ listKey: string
93
+ fieldName: string
94
+ context: import('../access/types.js').AccessContext
95
+ }
96
+ | {
97
+ operation: 'query'
98
+ value: TOutput | undefined
99
+ item: TItem
100
+ listKey: string
101
+ fieldName: string
102
+ context: import('../access/types.js').AccessContext
103
+ },
104
+ ) => Promise<void> | void
105
+
106
+ /**
107
+ * Transform field value after database read
108
+ * Called when returning results from query operations
109
+ * This is where you should transform output data (e.g., wrap passwords, format values)
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * resolveOutput: ({ value }) => {
114
+ * if (typeof value === 'string' && value.length > 0) {
115
+ * return new HashedPassword(value)
116
+ * }
117
+ * return value
118
+ * }
119
+ * ```
120
+ */
121
+ resolveOutput?: (args: {
122
+ operation: 'query'
123
+ value: TInput | undefined
124
+ item: TItem
125
+ listKey: string
126
+ fieldName: string
127
+ context: import('../access/types.js').AccessContext
128
+ }) => TOutput | undefined
129
+ }
130
+
131
+ /**
132
+ * Configuration for patching Prisma-generated types
133
+ * Allows fields to transform their types in query results
134
+ */
135
+ export type TypePatchConfig = {
136
+ /**
137
+ * The TypeScript type to use in Prisma result types (e.g., Payload scalars)
138
+ * This is an import statement like: "import('@opensaas/stack-core').HashedPassword"
139
+ */
140
+ resultType: string
141
+ /**
142
+ * Optional: Where to apply the patch
143
+ * - 'scalars-only': Only patch in Payload scalars (default, safest)
144
+ * - 'all': Patch everywhere the field appears (including inputs)
145
+ */
146
+ patchScope?: 'scalars-only' | 'all'
147
+ }
148
+
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ export type BaseFieldConfig<TInput = any, TOutput = TInput> = {
151
+ type: string
152
+ access?: FieldAccess
153
+ defaultValue?: unknown
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ hooks?: FieldHooks<TInput, TOutput, any>
156
+ /**
157
+ * Type patching configuration for Prisma-generated types
158
+ * When specified, the generator will patch Prisma's types to use
159
+ * the specified type in query results instead of the original type
160
+ */
161
+ typePatch?: TypePatchConfig
162
+ ui?: {
163
+ /**
164
+ * Custom React component to render this field
165
+ * Overrides the default component for this field type
166
+ * Uses `any` to accept any React component type without overly complex generics
167
+ */
168
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
+ component?: any
170
+ /**
171
+ * Custom field type name to use from the global registry
172
+ * e.g., "color" to use a globally registered ColorPickerField
173
+ */
174
+ fieldType?: string
175
+ /**
176
+ * Transform field value before sending to client (browser)
177
+ * Useful for sensitive fields (e.g., passwords) or complex data structures
178
+ * that shouldn't be serialized in their raw form
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * // Password field: send only whether it's set, not the hash
183
+ * valueForClientSerialization: ({ value }) => ({ isSet: !!value })
184
+ * ```
185
+ */
186
+ valueForClientSerialization?: (args: { value: unknown }) => unknown
187
+ /**
188
+ * Additional UI-specific configuration
189
+ */
190
+ [key: string]: unknown
191
+ }
192
+ /**
193
+ * Generate Zod schema for this field
194
+ * @param fieldName - The name of the field (for error messages)
195
+ * @param operation - Whether this is a create or update operation
196
+ */
197
+ getZodSchema?: (fieldName: string, operation: 'create' | 'update') => z.ZodTypeAny
198
+ /**
199
+ * Get Prisma type and modifiers for schema generation
200
+ * @param fieldName - The name of the field (for generating modifiers)
201
+ * @returns Prisma type string and optional modifiers
202
+ */
203
+ getPrismaType?: (fieldName: string) => {
204
+ type: string
205
+ modifiers?: string
206
+ }
207
+ /**
208
+ * Get TypeScript type information for type generation
209
+ * @returns TypeScript type string and optionality
210
+ */
211
+ getTypeScriptType?: () => {
212
+ type: string
213
+ optional: boolean
214
+ }
215
+ }
216
+
217
+ export type TextField = BaseFieldConfig<string, string> & {
218
+ type: 'text'
219
+ validation?: {
220
+ isRequired?: boolean
221
+ length?: {
222
+ min?: number
223
+ max?: number
224
+ }
225
+ }
226
+ isIndexed?: boolean | 'unique'
227
+ ui?: {
228
+ displayMode?: 'input' | 'textarea'
229
+ }
230
+ }
231
+
232
+ export type IntegerField = BaseFieldConfig<number, number> & {
233
+ type: 'integer'
234
+ validation?: {
235
+ isRequired?: boolean
236
+ min?: number
237
+ max?: number
238
+ }
239
+ }
240
+
241
+ export type CheckboxField = BaseFieldConfig<boolean, boolean> & {
242
+ type: 'checkbox'
243
+ }
244
+
245
+ export type TimestampField = BaseFieldConfig<Date, Date> & {
246
+ type: 'timestamp'
247
+ defaultValue?: { kind: 'now' } | Date
248
+ }
249
+
250
+ export type PasswordField = BaseFieldConfig<
251
+ string,
252
+ import('../utils/password.js').HashedPassword
253
+ > & {
254
+ type: 'password'
255
+ validation?: {
256
+ isRequired?: boolean
257
+ }
258
+ }
259
+
260
+ export type SelectField = BaseFieldConfig<string, string> & {
261
+ type: 'select'
262
+ options: Array<{ label: string; value: string }>
263
+ validation?: {
264
+ isRequired?: boolean
265
+ }
266
+ ui?: {
267
+ displayMode?: 'select' | 'segmented-control' | 'radio'
268
+ }
269
+ }
270
+
271
+ export type RelationshipField = BaseFieldConfig<string | string[], string | string[]> & {
272
+ type: 'relationship'
273
+ ref: string // Format: 'ListName.fieldName'
274
+ many?: boolean
275
+ ui?: {
276
+ displayMode?: 'select' | 'cards'
277
+ }
278
+ }
279
+
280
+ export type FieldConfig =
281
+ | TextField
282
+ | IntegerField
283
+ | CheckboxField
284
+ | TimestampField
285
+ | PasswordField
286
+ | SelectField
287
+ | RelationshipField
288
+ | BaseFieldConfig // Allow any field extending BaseFieldConfig (for third-party fields)
289
+
290
+ /**
291
+ * List configuration types
292
+ */
293
+
294
+ /**
295
+ * Utility type to inject item type into a single field config
296
+ * Extracts TInput and TOutput from BaseFieldConfig<TInput, TOutput> and reconstructs with new hooks type
297
+ */
298
+ type WithItemType<TField extends FieldConfig, TItem> =
299
+ TField extends BaseFieldConfig<infer TInput, infer TOutput>
300
+ ? Omit<TField, 'hooks'> & {
301
+ hooks?: FieldHooks<TInput, TOutput, TItem>
302
+ }
303
+ : TField
304
+
305
+ /**
306
+ * Utility type to transform all fields in a record to inject item type
307
+ * Maps over each field and applies WithItemType transformation
308
+ */
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ export type FieldsWithItemType<TFields extends Record<string, FieldConfig>, TItem = any> = {
311
+ [K in keyof TFields]: WithItemType<TFields[K], TItem>
312
+ }
313
+
314
+ // Generic `any` default allows OperationAccess to work with any list item type
315
+ // This is needed because the item type varies per list and is inferred from Prisma models
316
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
+ export type OperationAccess<T = any> = {
318
+ query?: AccessControl<T>
319
+ create?: AccessControl<T>
320
+ update?: AccessControl<T>
321
+ delete?: AccessControl<T>
322
+ }
323
+
324
+ export type HookArgs<T = Record<string, unknown>> = {
325
+ operation: 'create' | 'update' | 'delete'
326
+ resolvedData?: Partial<T>
327
+ item?: T
328
+ context: import('../access/types.js').AccessContext
329
+ }
330
+
331
+ export type Hooks<T = Record<string, unknown>> = {
332
+ resolveInput?: (args: HookArgs<T> & { operation: 'create' | 'update' }) => Promise<Partial<T>>
333
+ validateInput?: (
334
+ args: HookArgs<T> & {
335
+ operation: 'create' | 'update'
336
+ addValidationError: (msg: string) => void
337
+ },
338
+ ) => Promise<void>
339
+ beforeOperation?: (args: HookArgs<T>) => Promise<void>
340
+ afterOperation?: (args: HookArgs<T>) => Promise<void>
341
+ }
342
+
343
+ // Generic `any` default allows ListConfig to work with any list item type
344
+ // This is needed because the item type varies per list and is inferred from Prisma models
345
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346
+ export type ListConfig<T = any> = {
347
+ // Field configs are automatically transformed to inject the item type T
348
+ // This enables proper typing in field hooks where item: TItem
349
+ fields: FieldsWithItemType<Record<string, FieldConfig>, T>
350
+ access?: {
351
+ operation?: OperationAccess<T>
352
+ }
353
+ hooks?: Hooks<T>
354
+ }
355
+
356
+ /**
357
+ * Database configuration
358
+ */
359
+ export type DatabaseConfig = {
360
+ provider: 'postgresql' | 'mysql' | 'sqlite'
361
+ url: string
362
+ /**
363
+ * Optional factory function to create a custom Prisma client instance
364
+ * Receives the PrismaClient class and returns a configured instance
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * import { PrismaNeon } from '@prisma/adapter-neon'
369
+ * import { neonConfig } from '@neondatabase/serverless'
370
+ * import ws from 'ws'
371
+ *
372
+ * prismaClientConstructor: (PrismaClient) => {
373
+ * neonConfig.webSocketConstructor = ws
374
+ * const adapter = new PrismaNeon({
375
+ * connectionString: process.env.DATABASE_URL
376
+ * })
377
+ * return new PrismaClient({ adapter })
378
+ * }
379
+ * ```
380
+ */
381
+ // Uses `any` for maximum flexibility with Prisma client constructors and adapters
382
+ // Different database adapters have varying type signatures that are hard to unify
383
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
384
+ prismaClientConstructor?: (PrismaClientClass: any) => any
385
+ }
386
+
387
+ /**
388
+ * Session configuration
389
+ */
390
+ export type SessionConfig = {
391
+ // Uses `any` return type because session structure is user-defined and varies per application
392
+ // The stack doesn't enforce a specific session shape - users can use NextAuth, Clerk, etc.
393
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
394
+ getSession: () => Promise<any>
395
+ }
396
+
397
+ /**
398
+ * Theme preset options
399
+ */
400
+ export type ThemePreset = 'modern' | 'classic' | 'neon'
401
+
402
+ /**
403
+ * Custom theme colors (HSL values without hsl() wrapper)
404
+ * Format: "220 20% 97%" (hue saturation lightness)
405
+ */
406
+ export type ThemeColors = {
407
+ background?: string
408
+ foreground?: string
409
+ card?: string
410
+ cardForeground?: string
411
+ popover?: string
412
+ popoverForeground?: string
413
+ primary?: string
414
+ primaryForeground?: string
415
+ secondary?: string
416
+ secondaryForeground?: string
417
+ muted?: string
418
+ mutedForeground?: string
419
+ accent?: string
420
+ accentForeground?: string
421
+ destructive?: string
422
+ destructiveForeground?: string
423
+ border?: string
424
+ input?: string
425
+ ring?: string
426
+ gradientFrom?: string
427
+ gradientTo?: string
428
+ }
429
+
430
+ /**
431
+ * Theme configuration
432
+ */
433
+ export type ThemeConfig = {
434
+ /**
435
+ * Preset theme to use
436
+ * @default "modern"
437
+ */
438
+ preset?: ThemePreset
439
+ /**
440
+ * Custom color overrides for light mode
441
+ */
442
+ colors?: ThemeColors
443
+ /**
444
+ * Custom color overrides for dark mode
445
+ */
446
+ darkColors?: ThemeColors
447
+ /**
448
+ * Border radius in rem
449
+ * @default 0.75
450
+ */
451
+ radius?: number
452
+ }
453
+
454
+ /**
455
+ * UI configuration
456
+ */
457
+ export type UIConfig = {
458
+ basePath?: string
459
+ /**
460
+ * Theme configuration for the admin UI
461
+ */
462
+ theme?: ThemeConfig
463
+ }
464
+
465
+ /**
466
+ * Main configuration type
467
+ */
468
+ export type OpenSaasConfig = {
469
+ db: DatabaseConfig
470
+ lists: Record<string, ListConfig>
471
+ session?: SessionConfig
472
+ ui?: UIConfig
473
+ /**
474
+ * Path where OpenSaas generates files (context, types, patched Prisma client)
475
+ * @default ".opensaas"
476
+ */
477
+ opensaasPath?: string
478
+ }