@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.
- package/.turbo/turbo-build.log +4 -0
- package/README.md +447 -0
- package/dist/access/engine.d.ts +73 -0
- package/dist/access/engine.d.ts.map +1 -0
- package/dist/access/engine.js +244 -0
- package/dist/access/engine.js.map +1 -0
- package/dist/access/field-transforms.d.ts +47 -0
- package/dist/access/field-transforms.d.ts.map +1 -0
- package/dist/access/field-transforms.js +2 -0
- package/dist/access/field-transforms.js.map +1 -0
- package/dist/access/index.d.ts +3 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +2 -0
- package/dist/access/index.js.map +1 -0
- package/dist/access/types.d.ts +83 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access/types.js +2 -0
- package/dist/access/types.js.map +1 -0
- package/dist/config/index.d.ts +39 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +413 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/context/index.d.ts +31 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +524 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/nested-operations.d.ts +10 -0
- package/dist/context/nested-operations.d.ts.map +1 -0
- package/dist/context/nested-operations.js +261 -0
- package/dist/context/nested-operations.js.map +1 -0
- package/dist/fields/index.d.ts +78 -0
- package/dist/fields/index.d.ts.map +1 -0
- package/dist/fields/index.js +381 -0
- package/dist/fields/index.js.map +1 -0
- package/dist/hooks/index.d.ts +58 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +79 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/case-utils.d.ts +49 -0
- package/dist/lib/case-utils.d.ts.map +1 -0
- package/dist/lib/case-utils.js +68 -0
- package/dist/lib/case-utils.js.map +1 -0
- package/dist/lib/case-utils.test.d.ts +2 -0
- package/dist/lib/case-utils.test.d.ts.map +1 -0
- package/dist/lib/case-utils.test.js +101 -0
- package/dist/lib/case-utils.test.js.map +1 -0
- package/dist/utils/password.d.ts +81 -0
- package/dist/utils/password.d.ts.map +1 -0
- package/dist/utils/password.js +132 -0
- package/dist/utils/password.js.map +1 -0
- package/dist/validation/schema.d.ts +17 -0
- package/dist/validation/schema.d.ts.map +1 -0
- package/dist/validation/schema.js +42 -0
- package/dist/validation/schema.js.map +1 -0
- package/dist/validation/schema.test.d.ts +2 -0
- package/dist/validation/schema.test.d.ts.map +1 -0
- package/dist/validation/schema.test.js +143 -0
- package/dist/validation/schema.test.js.map +1 -0
- package/docs/type-distribution-fix.md +136 -0
- package/package.json +48 -0
- package/src/access/engine.ts +360 -0
- package/src/access/field-transforms.ts +99 -0
- package/src/access/index.ts +20 -0
- package/src/access/types.ts +103 -0
- package/src/config/index.ts +71 -0
- package/src/config/types.ts +478 -0
- package/src/context/index.ts +814 -0
- package/src/context/nested-operations.ts +412 -0
- package/src/fields/index.ts +438 -0
- package/src/hooks/index.ts +132 -0
- package/src/index.ts +62 -0
- package/src/lib/case-utils.test.ts +127 -0
- package/src/lib/case-utils.ts +74 -0
- package/src/utils/password.ts +147 -0
- package/src/validation/schema.test.ts +171 -0
- package/src/validation/schema.ts +59 -0
- package/tests/access-relationships.test.ts +613 -0
- package/tests/access.test.ts +499 -0
- package/tests/config.test.ts +195 -0
- package/tests/context.test.ts +248 -0
- package/tests/hooks.test.ts +417 -0
- package/tests/password-type-distribution.test.ts +155 -0
- package/tests/password-types.test.ts +147 -0
- package/tests/password.test.ts +249 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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
|
+
}
|