@opensaas/stack-core 0.13.0 → 0.15.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 +235 -0
- package/dist/access/engine.d.ts +2 -0
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +8 -6
- package/dist/access/engine.js.map +1 -1
- package/dist/access/engine.test.js +4 -0
- package/dist/access/engine.test.js.map +1 -1
- package/dist/access/types.d.ts +31 -4
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +12 -10
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +37 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +280 -98
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +133 -7
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +2 -0
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/engine.test.ts +4 -0
- package/src/access/engine.ts +10 -7
- package/src/access/types.ts +45 -4
- package/src/config/index.ts +65 -9
- package/src/config/types.ts +326 -113
- package/src/context/index.ts +176 -10
- package/src/context/nested-operations.ts +2 -0
- package/src/index.ts +11 -0
- package/tests/access.test.ts +28 -28
- package/tests/config.test.ts +20 -3
- package/tests/singleton.test.ts +329 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config/index.ts
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
OpenSaasConfig,
|
|
3
|
+
ListConfig,
|
|
4
|
+
ListConfigInput,
|
|
5
|
+
OperationAccess,
|
|
6
|
+
ListAccessControl,
|
|
7
|
+
} from './types.js'
|
|
2
8
|
import { executePlugins } from './plugin-engine.js'
|
|
9
|
+
import type { AccessControl } from '../access/types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize access control shorthand to object form
|
|
13
|
+
* Converts function shorthand to { operation: { query, create, update, delete } } form
|
|
14
|
+
*/
|
|
15
|
+
function normalizeListAccess<T>(
|
|
16
|
+
access: ListAccessControl<T> | undefined,
|
|
17
|
+
): { operation?: OperationAccess<T> } | undefined {
|
|
18
|
+
if (!access) return undefined
|
|
19
|
+
|
|
20
|
+
// If it's a function, convert to object form applying to all operations
|
|
21
|
+
if (typeof access === 'function') {
|
|
22
|
+
const fn = access as AccessControl<T>
|
|
23
|
+
return {
|
|
24
|
+
operation: {
|
|
25
|
+
query: fn,
|
|
26
|
+
create: fn,
|
|
27
|
+
update: fn,
|
|
28
|
+
delete: fn,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Already in object form
|
|
34
|
+
return access
|
|
35
|
+
}
|
|
3
36
|
|
|
4
37
|
/**
|
|
5
38
|
* Helper function to define configuration with type safety
|
|
@@ -61,25 +94,37 @@ export function config(userConfig: OpenSaasConfig): OpenSaasConfig | Promise<Ope
|
|
|
61
94
|
* fields: { title: text() },
|
|
62
95
|
* hooks: { ... }
|
|
63
96
|
* })
|
|
97
|
+
*
|
|
98
|
+
* // Access control shorthand
|
|
99
|
+
* const isAdmin = ({ session }) => session?.role === 'admin'
|
|
100
|
+
*
|
|
101
|
+
* Settings: list({
|
|
102
|
+
* access: isAdmin, // Applies to all operations
|
|
103
|
+
* isSingleton: true,
|
|
104
|
+
* fields: { ... }
|
|
105
|
+
* })
|
|
64
106
|
* ```
|
|
65
107
|
*/
|
|
66
|
-
export function list<TTypeInfo extends import('./types.js').TypeInfo>(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
108
|
+
export function list<TTypeInfo extends import('./types.js').TypeInfo>(
|
|
109
|
+
config: ListConfigInput<TTypeInfo>,
|
|
110
|
+
): ListConfig<TTypeInfo> {
|
|
111
|
+
// Normalize access control shorthand to object form
|
|
112
|
+
const normalizedConfig = {
|
|
113
|
+
...config,
|
|
114
|
+
access: normalizeListAccess(config.access),
|
|
70
115
|
}
|
|
71
|
-
|
|
72
|
-
mcp?: import('./types.js').ListMcpConfig
|
|
73
|
-
}): ListConfig<TTypeInfo> {
|
|
116
|
+
|
|
74
117
|
// At runtime, field configs are unchanged
|
|
75
118
|
// At type level, they're transformed to inject TypeInfo types
|
|
76
|
-
return
|
|
119
|
+
return normalizedConfig as ListConfig<TTypeInfo>
|
|
77
120
|
}
|
|
78
121
|
|
|
79
122
|
// Re-export all types
|
|
80
123
|
export type {
|
|
81
124
|
OpenSaasConfig,
|
|
82
125
|
ListConfig,
|
|
126
|
+
ListConfigInput,
|
|
127
|
+
ListAccessControl,
|
|
83
128
|
FieldConfig,
|
|
84
129
|
BaseFieldConfig,
|
|
85
130
|
TextField,
|
|
@@ -115,4 +160,15 @@ export type {
|
|
|
115
160
|
Plugin,
|
|
116
161
|
PluginContext,
|
|
117
162
|
GeneratedFiles,
|
|
163
|
+
// List-level hook argument types
|
|
164
|
+
ResolveInputHookArgs,
|
|
165
|
+
ValidateHookArgs,
|
|
166
|
+
BeforeOperationHookArgs,
|
|
167
|
+
AfterOperationHookArgs,
|
|
168
|
+
// Field-level hook argument types
|
|
169
|
+
FieldResolveInputHookArgs,
|
|
170
|
+
FieldValidateHookArgs,
|
|
171
|
+
FieldBeforeOperationHookArgs,
|
|
172
|
+
FieldAfterOperationHookArgs,
|
|
173
|
+
FieldResolveOutputHookArgs,
|
|
118
174
|
} from './types.js'
|
package/src/config/types.ts
CHANGED
|
@@ -14,6 +14,158 @@ export type FieldType =
|
|
|
14
14
|
| 'relationship'
|
|
15
15
|
| string // Allow custom field types from third-party packages
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Field-level hook argument types (exported for user annotations)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Arguments for field-level resolveInput hook
|
|
23
|
+
* Used to transform field values before database write
|
|
24
|
+
*/
|
|
25
|
+
export type FieldResolveInputHookArgs<
|
|
26
|
+
TTypeInfo extends TypeInfo,
|
|
27
|
+
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
28
|
+
> =
|
|
29
|
+
| {
|
|
30
|
+
listKey: string
|
|
31
|
+
fieldKey: TFieldKey
|
|
32
|
+
operation: 'create'
|
|
33
|
+
inputData: TTypeInfo['inputs']['create']
|
|
34
|
+
item: undefined
|
|
35
|
+
resolvedData: TTypeInfo['inputs']['create']
|
|
36
|
+
context: import('../access/types.js').AccessContext
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
listKey: string
|
|
40
|
+
fieldKey: TFieldKey
|
|
41
|
+
operation: 'update'
|
|
42
|
+
inputData: TTypeInfo['inputs']['update']
|
|
43
|
+
item: TTypeInfo['item']
|
|
44
|
+
resolvedData: TTypeInfo['inputs']['update']
|
|
45
|
+
context: import('../access/types.js').AccessContext
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Arguments for field-level validate hook
|
|
50
|
+
* Used for custom validation logic
|
|
51
|
+
*/
|
|
52
|
+
export type FieldValidateHookArgs<
|
|
53
|
+
TTypeInfo extends TypeInfo,
|
|
54
|
+
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
55
|
+
> =
|
|
56
|
+
| {
|
|
57
|
+
listKey: string
|
|
58
|
+
fieldKey: TFieldKey
|
|
59
|
+
operation: 'create'
|
|
60
|
+
inputData: TTypeInfo['inputs']['create']
|
|
61
|
+
item: undefined
|
|
62
|
+
resolvedData: TTypeInfo['inputs']['create']
|
|
63
|
+
context: import('../access/types.js').AccessContext
|
|
64
|
+
addValidationError: (msg: string) => void
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
listKey: string
|
|
68
|
+
fieldKey: TFieldKey
|
|
69
|
+
operation: 'update'
|
|
70
|
+
inputData: TTypeInfo['inputs']['update']
|
|
71
|
+
item: TTypeInfo['item']
|
|
72
|
+
resolvedData: TTypeInfo['inputs']['update']
|
|
73
|
+
context: import('../access/types.js').AccessContext
|
|
74
|
+
addValidationError: (msg: string) => void
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
listKey: string
|
|
78
|
+
fieldKey: TFieldKey
|
|
79
|
+
operation: 'delete'
|
|
80
|
+
item: TTypeInfo['item']
|
|
81
|
+
context: import('../access/types.js').AccessContext
|
|
82
|
+
addValidationError: (msg: string) => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Arguments for field-level beforeOperation hook
|
|
87
|
+
* Used for side effects before database write
|
|
88
|
+
*/
|
|
89
|
+
export type FieldBeforeOperationHookArgs<
|
|
90
|
+
TTypeInfo extends TypeInfo,
|
|
91
|
+
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
92
|
+
> =
|
|
93
|
+
| {
|
|
94
|
+
listKey: string
|
|
95
|
+
fieldKey: TFieldKey
|
|
96
|
+
operation: 'create'
|
|
97
|
+
inputData: TTypeInfo['inputs']['create']
|
|
98
|
+
resolvedData: TTypeInfo['inputs']['create']
|
|
99
|
+
context: import('../access/types.js').AccessContext
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
listKey: string
|
|
103
|
+
fieldKey: TFieldKey
|
|
104
|
+
operation: 'update'
|
|
105
|
+
inputData: TTypeInfo['inputs']['update']
|
|
106
|
+
item: TTypeInfo['item']
|
|
107
|
+
resolvedData: TTypeInfo['inputs']['update']
|
|
108
|
+
context: import('../access/types.js').AccessContext
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
listKey: string
|
|
112
|
+
fieldKey: TFieldKey
|
|
113
|
+
operation: 'delete'
|
|
114
|
+
item: TTypeInfo['item']
|
|
115
|
+
context: import('../access/types.js').AccessContext
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Arguments for field-level afterOperation hook
|
|
120
|
+
* Used for side effects after database operation
|
|
121
|
+
*/
|
|
122
|
+
export type FieldAfterOperationHookArgs<
|
|
123
|
+
TTypeInfo extends TypeInfo,
|
|
124
|
+
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
125
|
+
> =
|
|
126
|
+
| {
|
|
127
|
+
listKey: string
|
|
128
|
+
fieldKey: TFieldKey
|
|
129
|
+
operation: 'create'
|
|
130
|
+
inputData: TTypeInfo['inputs']['create']
|
|
131
|
+
item: TTypeInfo['item']
|
|
132
|
+
resolvedData: TTypeInfo['inputs']['create']
|
|
133
|
+
context: import('../access/types.js').AccessContext
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
listKey: string
|
|
137
|
+
fieldKey: TFieldKey
|
|
138
|
+
operation: 'update'
|
|
139
|
+
inputData: TTypeInfo['inputs']['update']
|
|
140
|
+
originalItem: TTypeInfo['item']
|
|
141
|
+
item: TTypeInfo['item']
|
|
142
|
+
resolvedData: TTypeInfo['inputs']['update']
|
|
143
|
+
context: import('../access/types.js').AccessContext
|
|
144
|
+
}
|
|
145
|
+
| {
|
|
146
|
+
listKey: string
|
|
147
|
+
fieldKey: TFieldKey
|
|
148
|
+
operation: 'delete'
|
|
149
|
+
originalItem: TTypeInfo['item']
|
|
150
|
+
context: import('../access/types.js').AccessContext
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Arguments for field-level resolveOutput hook
|
|
155
|
+
* Used to transform field values after database read
|
|
156
|
+
*/
|
|
157
|
+
export type FieldResolveOutputHookArgs<
|
|
158
|
+
TTypeInfo extends TypeInfo,
|
|
159
|
+
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
160
|
+
> = {
|
|
161
|
+
operation: 'query'
|
|
162
|
+
value: GetFieldValueType<TTypeInfo['fields'], TFieldKey>
|
|
163
|
+
item: TTypeInfo['item']
|
|
164
|
+
listKey: string
|
|
165
|
+
fieldName: TFieldKey
|
|
166
|
+
context: import('../access/types.js').AccessContext
|
|
167
|
+
}
|
|
168
|
+
|
|
17
169
|
/**
|
|
18
170
|
* Field-level hooks for data transformation and side effects
|
|
19
171
|
* Allows field types to define custom behavior during operations
|
|
@@ -55,25 +207,7 @@ export type FieldHooks<
|
|
|
55
207
|
* ```
|
|
56
208
|
*/
|
|
57
209
|
resolveInput?: (
|
|
58
|
-
args:
|
|
59
|
-
| {
|
|
60
|
-
listKey: string
|
|
61
|
-
fieldKey: TFieldKey
|
|
62
|
-
operation: 'create'
|
|
63
|
-
inputData: TTypeInfo['inputs']['create']
|
|
64
|
-
item: undefined
|
|
65
|
-
resolvedData: TTypeInfo['inputs']['create']
|
|
66
|
-
context: import('../access/types.js').AccessContext
|
|
67
|
-
}
|
|
68
|
-
| {
|
|
69
|
-
listKey: string
|
|
70
|
-
fieldKey: TFieldKey
|
|
71
|
-
operation: 'update'
|
|
72
|
-
inputData: TTypeInfo['inputs']['update']
|
|
73
|
-
item: TTypeInfo['item']
|
|
74
|
-
resolvedData: TTypeInfo['inputs']['update']
|
|
75
|
-
context: import('../access/types.js').AccessContext
|
|
76
|
-
},
|
|
210
|
+
args: FieldResolveInputHookArgs<TTypeInfo, TFieldKey>,
|
|
77
211
|
) =>
|
|
78
212
|
| Promise<GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined>
|
|
79
213
|
| GetFieldValueType<TTypeInfo['fields'], TFieldKey>
|
|
@@ -95,37 +229,12 @@ export type FieldHooks<
|
|
|
95
229
|
* }
|
|
96
230
|
* ```
|
|
97
231
|
*/
|
|
98
|
-
validate?: (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
inputData: TTypeInfo['inputs']['create']
|
|
105
|
-
item: undefined
|
|
106
|
-
resolvedData: TTypeInfo['inputs']['create']
|
|
107
|
-
context: import('../access/types.js').AccessContext
|
|
108
|
-
addValidationError: (msg: string) => void
|
|
109
|
-
}
|
|
110
|
-
| {
|
|
111
|
-
listKey: string
|
|
112
|
-
fieldKey: TFieldKey
|
|
113
|
-
operation: 'update'
|
|
114
|
-
inputData: TTypeInfo['inputs']['update']
|
|
115
|
-
item: TTypeInfo['item']
|
|
116
|
-
resolvedData: TTypeInfo['inputs']['update']
|
|
117
|
-
context: import('../access/types.js').AccessContext
|
|
118
|
-
addValidationError: (msg: string) => void
|
|
119
|
-
}
|
|
120
|
-
| {
|
|
121
|
-
listKey: string
|
|
122
|
-
fieldKey: TFieldKey
|
|
123
|
-
operation: 'delete'
|
|
124
|
-
item: TTypeInfo['item']
|
|
125
|
-
context: import('../access/types.js').AccessContext
|
|
126
|
-
addValidationError: (msg: string) => void
|
|
127
|
-
},
|
|
128
|
-
) => Promise<void> | void
|
|
232
|
+
validate?: (args: FieldValidateHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @deprecated Use 'validate' instead. This alias is provided for backwards compatibility.
|
|
236
|
+
*/
|
|
237
|
+
validateInput?: (args: FieldValidateHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void
|
|
129
238
|
|
|
130
239
|
/**
|
|
131
240
|
* Perform side effects before database write
|
|
@@ -146,31 +255,7 @@ export type FieldHooks<
|
|
|
146
255
|
* ```
|
|
147
256
|
*/
|
|
148
257
|
beforeOperation?: (
|
|
149
|
-
args:
|
|
150
|
-
| {
|
|
151
|
-
listKey: string
|
|
152
|
-
fieldKey: TFieldKey
|
|
153
|
-
operation: 'create'
|
|
154
|
-
inputData: TTypeInfo['inputs']['create']
|
|
155
|
-
resolvedData: TTypeInfo['inputs']['create']
|
|
156
|
-
context: import('../access/types.js').AccessContext
|
|
157
|
-
}
|
|
158
|
-
| {
|
|
159
|
-
listKey: string
|
|
160
|
-
fieldKey: TFieldKey
|
|
161
|
-
operation: 'update'
|
|
162
|
-
inputData: TTypeInfo['inputs']['update']
|
|
163
|
-
item: TTypeInfo['item']
|
|
164
|
-
resolvedData: TTypeInfo['inputs']['update']
|
|
165
|
-
context: import('../access/types.js').AccessContext
|
|
166
|
-
}
|
|
167
|
-
| {
|
|
168
|
-
listKey: string
|
|
169
|
-
fieldKey: TFieldKey
|
|
170
|
-
operation: 'delete'
|
|
171
|
-
item: TTypeInfo['item']
|
|
172
|
-
context: import('../access/types.js').AccessContext
|
|
173
|
-
},
|
|
258
|
+
args: FieldBeforeOperationHookArgs<TTypeInfo, TFieldKey>,
|
|
174
259
|
) => Promise<void> | void
|
|
175
260
|
|
|
176
261
|
/**
|
|
@@ -191,35 +276,7 @@ export type FieldHooks<
|
|
|
191
276
|
* }
|
|
192
277
|
* ```
|
|
193
278
|
*/
|
|
194
|
-
afterOperation?: (
|
|
195
|
-
args:
|
|
196
|
-
| {
|
|
197
|
-
listKey: string
|
|
198
|
-
fieldKey: TFieldKey
|
|
199
|
-
operation: 'create'
|
|
200
|
-
inputData: TTypeInfo['inputs']['create']
|
|
201
|
-
item: TTypeInfo['item']
|
|
202
|
-
resolvedData: TTypeInfo['inputs']['create']
|
|
203
|
-
context: import('../access/types.js').AccessContext
|
|
204
|
-
}
|
|
205
|
-
| {
|
|
206
|
-
listKey: string
|
|
207
|
-
fieldKey: TFieldKey
|
|
208
|
-
operation: 'update'
|
|
209
|
-
inputData: TTypeInfo['inputs']['update']
|
|
210
|
-
originalItem: TTypeInfo['item']
|
|
211
|
-
item: TTypeInfo['item']
|
|
212
|
-
resolvedData: TTypeInfo['inputs']['update']
|
|
213
|
-
context: import('../access/types.js').AccessContext
|
|
214
|
-
}
|
|
215
|
-
| {
|
|
216
|
-
listKey: string
|
|
217
|
-
fieldKey: TFieldKey
|
|
218
|
-
operation: 'delete'
|
|
219
|
-
originalItem: TTypeInfo['item']
|
|
220
|
-
context: import('../access/types.js').AccessContext
|
|
221
|
-
},
|
|
222
|
-
) => Promise<void> | void
|
|
279
|
+
afterOperation?: (args: FieldAfterOperationHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void
|
|
223
280
|
|
|
224
281
|
/**
|
|
225
282
|
* Transform field value after database read
|
|
@@ -236,14 +293,9 @@ export type FieldHooks<
|
|
|
236
293
|
* }
|
|
237
294
|
* ```
|
|
238
295
|
*/
|
|
239
|
-
resolveOutput?: (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
item: TTypeInfo['item']
|
|
243
|
-
listKey: string
|
|
244
|
-
fieldName: TFieldKey
|
|
245
|
-
context: import('../access/types.js').AccessContext
|
|
246
|
-
}) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
296
|
+
resolveOutput?: (
|
|
297
|
+
args: FieldResolveOutputHookArgs<TTypeInfo, TFieldKey>,
|
|
298
|
+
) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
247
299
|
}
|
|
248
300
|
|
|
249
301
|
/**
|
|
@@ -274,7 +326,11 @@ export type ResultExtensionConfig = {
|
|
|
274
326
|
|
|
275
327
|
export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
276
328
|
type: string
|
|
277
|
-
access?: FieldAccess
|
|
329
|
+
access?: FieldAccess<
|
|
330
|
+
TTypeInfo['item'],
|
|
331
|
+
TTypeInfo['inputs']['create'],
|
|
332
|
+
TTypeInfo['inputs']['update']
|
|
333
|
+
>
|
|
278
334
|
defaultValue?: unknown
|
|
279
335
|
hooks?: FieldHooks<TTypeInfo>
|
|
280
336
|
/**
|
|
@@ -503,6 +559,29 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
503
559
|
type: 'relationship'
|
|
504
560
|
ref: string // Format: 'ListName.fieldName' or 'ListName'
|
|
505
561
|
many?: boolean
|
|
562
|
+
/**
|
|
563
|
+
* Controls whether to create an index on the foreign key field
|
|
564
|
+
* Defaults to true for all foreign key fields (matching Keystone behavior)
|
|
565
|
+
* Can be set to 'unique' for unique constraints or false to disable indexing
|
|
566
|
+
*
|
|
567
|
+
* @default true (for foreign key fields)
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* ```typescript
|
|
571
|
+
* // Standard indexed foreign key (default)
|
|
572
|
+
* author: relationship({ ref: 'User.posts' })
|
|
573
|
+
* // Generates: @@index([authorId])
|
|
574
|
+
*
|
|
575
|
+
* // Unique foreign key (one-to-one)
|
|
576
|
+
* author: relationship({ ref: 'User.posts', isIndexed: 'unique' })
|
|
577
|
+
* // Generates: @@unique([authorId])
|
|
578
|
+
*
|
|
579
|
+
* // Disable indexing (not recommended, may cause performance issues)
|
|
580
|
+
* author: relationship({ ref: 'User.posts', isIndexed: false })
|
|
581
|
+
* // No index generated
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
isIndexed?: boolean | 'unique'
|
|
506
585
|
db?: {
|
|
507
586
|
/**
|
|
508
587
|
* Controls foreign key placement and column name for bidirectional relationships
|
|
@@ -539,6 +618,37 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
539
618
|
* ```
|
|
540
619
|
*/
|
|
541
620
|
foreignKey?: boolean | { map?: string }
|
|
621
|
+
/**
|
|
622
|
+
* Extend or modify the generated Prisma schema lines for this relationship field
|
|
623
|
+
* Receives the generated FK line (if applicable) and relation line
|
|
624
|
+
* Returns the modified lines
|
|
625
|
+
*
|
|
626
|
+
* @example Add onDelete cascade for self-referential relationship
|
|
627
|
+
* ```typescript
|
|
628
|
+
* parent: relationship({
|
|
629
|
+
* ref: 'Category.children',
|
|
630
|
+
* db: {
|
|
631
|
+
* foreignKey: true,
|
|
632
|
+
* extendPrismaSchema: ({ fkLine, relationLine }) => ({
|
|
633
|
+
* fkLine,
|
|
634
|
+
* relationLine: relationLine.replace(
|
|
635
|
+
* '@relation(',
|
|
636
|
+
* '@relation(onDelete: SetNull, '
|
|
637
|
+
* )
|
|
638
|
+
* })
|
|
639
|
+
* }
|
|
640
|
+
* })
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
extendPrismaSchema?: (lines: {
|
|
644
|
+
/** The foreign key field line (e.g., "parentId String?"), only present for single relationships that own the FK */
|
|
645
|
+
fkLine?: string
|
|
646
|
+
/** The relation field line (e.g., "parent Category? @relation(...)") */
|
|
647
|
+
relationLine: string
|
|
648
|
+
}) => {
|
|
649
|
+
fkLine?: string
|
|
650
|
+
relationLine: string
|
|
651
|
+
}
|
|
542
652
|
}
|
|
543
653
|
ui?: {
|
|
544
654
|
displayMode?: 'select' | 'cards'
|
|
@@ -752,6 +862,48 @@ export type OperationAccess<T = any> = {
|
|
|
752
862
|
delete?: AccessControl<T>
|
|
753
863
|
}
|
|
754
864
|
|
|
865
|
+
/**
|
|
866
|
+
* List-level access control configuration
|
|
867
|
+
* Supports two patterns:
|
|
868
|
+
*
|
|
869
|
+
* 1. Function shorthand - applies to all CRUD operations:
|
|
870
|
+
* `access: isAdmin`
|
|
871
|
+
*
|
|
872
|
+
* 2. Object form - configure operations individually:
|
|
873
|
+
* `access: { operation: { query: () => true, create: isAdmin } }`
|
|
874
|
+
*
|
|
875
|
+
* @example Function shorthand
|
|
876
|
+
* ```typescript
|
|
877
|
+
* const isAdmin = ({ session }) => session?.role === 'admin'
|
|
878
|
+
*
|
|
879
|
+
* list({
|
|
880
|
+
* access: isAdmin, // Applies to query, create, update, delete
|
|
881
|
+
* fields: { ... }
|
|
882
|
+
* })
|
|
883
|
+
* ```
|
|
884
|
+
*
|
|
885
|
+
* @example Object form
|
|
886
|
+
* ```typescript
|
|
887
|
+
* list({
|
|
888
|
+
* access: {
|
|
889
|
+
* operation: {
|
|
890
|
+
* query: () => true,
|
|
891
|
+
* create: isAdmin,
|
|
892
|
+
* update: isOwner,
|
|
893
|
+
* delete: isAdmin,
|
|
894
|
+
* }
|
|
895
|
+
* },
|
|
896
|
+
* fields: { ... }
|
|
897
|
+
* })
|
|
898
|
+
* ```
|
|
899
|
+
*/
|
|
900
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
901
|
+
export type ListAccessControl<T = any> =
|
|
902
|
+
| AccessControl<T>
|
|
903
|
+
| {
|
|
904
|
+
operation?: OperationAccess<T>
|
|
905
|
+
}
|
|
906
|
+
|
|
755
907
|
/**
|
|
756
908
|
* Hook arguments for resolveInput hook
|
|
757
909
|
* Uses discriminated union to provide proper types based on operation
|
|
@@ -911,10 +1063,19 @@ export type Hooks<
|
|
|
911
1063
|
|
|
912
1064
|
// Generic `any` default allows ListConfig to work with any list item type
|
|
913
1065
|
// This is needed because the item type varies per list and is inferred from Prisma models
|
|
1066
|
+
/**
|
|
1067
|
+
* Internal list configuration type (after normalization by list() function)
|
|
1068
|
+
* Access control is always in object form internally.
|
|
1069
|
+
* Use list() function which accepts both function shorthand and object form.
|
|
1070
|
+
*/
|
|
914
1071
|
export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
915
1072
|
// Field configs are automatically transformed to inject the full TypeInfo
|
|
916
1073
|
// This enables proper typing in field hooks where item, create input, and update input are all typed
|
|
917
1074
|
fields: FieldsWithTypeInfo<TTypeInfo>
|
|
1075
|
+
/**
|
|
1076
|
+
* Access control configuration for this list (normalized object form).
|
|
1077
|
+
* The list() function normalizes function shorthand to this object form.
|
|
1078
|
+
*/
|
|
918
1079
|
access?: {
|
|
919
1080
|
operation?: OperationAccess<TTypeInfo['item']>
|
|
920
1081
|
}
|
|
@@ -923,6 +1084,58 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
923
1084
|
* MCP server configuration for this list
|
|
924
1085
|
*/
|
|
925
1086
|
mcp?: ListMcpConfig
|
|
1087
|
+
/**
|
|
1088
|
+
* Restricts this list to a single record (singleton pattern)
|
|
1089
|
+
* When true:
|
|
1090
|
+
* - Prevents creating multiple records
|
|
1091
|
+
* - Auto-creates the single record on first access (if autoCreate: true, which is the default)
|
|
1092
|
+
* - Provides a get() method for easy access to the singleton
|
|
1093
|
+
* - Blocks delete and findMany operations
|
|
1094
|
+
* - Changes UI to show edit form instead of list view
|
|
1095
|
+
*
|
|
1096
|
+
* @example Simple boolean (auto-create enabled)
|
|
1097
|
+
* ```typescript
|
|
1098
|
+
* isSingleton: true
|
|
1099
|
+
* ```
|
|
1100
|
+
*
|
|
1101
|
+
* @example With options
|
|
1102
|
+
* ```typescript
|
|
1103
|
+
* isSingleton: {
|
|
1104
|
+
* autoCreate: false // Don't auto-create, must be created manually
|
|
1105
|
+
* }
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
isSingleton?:
|
|
1109
|
+
| boolean
|
|
1110
|
+
| {
|
|
1111
|
+
/**
|
|
1112
|
+
* Auto-create the singleton record on first access using field defaults
|
|
1113
|
+
* @default true
|
|
1114
|
+
*/
|
|
1115
|
+
autoCreate?: boolean
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Input type for the list() function
|
|
1121
|
+
* Accepts both function shorthand and object form for access control.
|
|
1122
|
+
*/
|
|
1123
|
+
export type ListConfigInput<TTypeInfo extends TypeInfo> = Omit<ListConfig<TTypeInfo>, 'access'> & {
|
|
1124
|
+
/**
|
|
1125
|
+
* Access control configuration for this list.
|
|
1126
|
+
* Supports both function shorthand and object form.
|
|
1127
|
+
*
|
|
1128
|
+
* @example Function shorthand (applies to all operations)
|
|
1129
|
+
* ```typescript
|
|
1130
|
+
* access: isAdmin
|
|
1131
|
+
* ```
|
|
1132
|
+
*
|
|
1133
|
+
* @example Object form (per-operation)
|
|
1134
|
+
* ```typescript
|
|
1135
|
+
* access: { operation: { query: () => true, create: isAdmin } }
|
|
1136
|
+
* ```
|
|
1137
|
+
*/
|
|
1138
|
+
access?: ListAccessControl<TTypeInfo['item']>
|
|
926
1139
|
}
|
|
927
1140
|
|
|
928
1141
|
/**
|