@opensaas/stack-core 0.12.1 → 0.14.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 +291 -0
- package/README.md +6 -3
- 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 +341 -82
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +330 -60
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +38 -25
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/hooks/index.d.ts +45 -7
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +10 -4
- package/dist/hooks/index.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 +402 -91
- package/src/context/index.ts +421 -82
- package/src/context/nested-operations.ts +40 -25
- package/src/hooks/index.ts +66 -14
- package/src/index.ts +11 -0
- package/tests/access.test.ts +28 -28
- package/tests/config.test.ts +20 -3
- package/tests/nested-access-and-hooks.test.ts +8 -3
- package/tests/singleton.test.ts +329 -0
- package/tests/sudo.test.ts +2 -13
- package/tsconfig.tsbuildinfo +1 -1
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
|
|
@@ -43,39 +195,47 @@ export type FieldHooks<
|
|
|
43
195
|
*
|
|
44
196
|
* @example
|
|
45
197
|
* ```typescript
|
|
46
|
-
* resolveInput: async ({
|
|
198
|
+
* resolveInput: async ({ listKey, fieldKey, operation, inputData, item, resolvedData, context }) => {
|
|
47
199
|
* // For create operations, item is undefined
|
|
48
200
|
* // For update operations, item is the existing record
|
|
49
|
-
*
|
|
50
|
-
*
|
|
201
|
+
* const fieldValue = resolvedData[fieldKey]
|
|
202
|
+
* if (typeof fieldValue === 'string' && !isHashedPassword(fieldValue)) {
|
|
203
|
+
* return await hashPassword(fieldValue)
|
|
51
204
|
* }
|
|
52
|
-
* return
|
|
205
|
+
* return fieldValue
|
|
53
206
|
* }
|
|
54
207
|
* ```
|
|
55
208
|
*/
|
|
56
209
|
resolveInput?: (
|
|
57
|
-
args:
|
|
58
|
-
| {
|
|
59
|
-
operation: 'create'
|
|
60
|
-
inputValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
61
|
-
item: undefined
|
|
62
|
-
listKey: string
|
|
63
|
-
fieldName: TFieldKey
|
|
64
|
-
context: import('../access/types.js').AccessContext
|
|
65
|
-
}
|
|
66
|
-
| {
|
|
67
|
-
operation: 'update'
|
|
68
|
-
inputValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
69
|
-
item: TTypeInfo['item']
|
|
70
|
-
listKey: string
|
|
71
|
-
fieldName: TFieldKey
|
|
72
|
-
context: import('../access/types.js').AccessContext
|
|
73
|
-
},
|
|
210
|
+
args: FieldResolveInputHookArgs<TTypeInfo, TFieldKey>,
|
|
74
211
|
) =>
|
|
75
212
|
| Promise<GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined>
|
|
76
213
|
| GetFieldValueType<TTypeInfo['fields'], TFieldKey>
|
|
77
214
|
| undefined
|
|
78
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Validate field value after resolveInput
|
|
218
|
+
* Called during create/update operations after resolveInput hooks but before database write
|
|
219
|
+
* Use addValidationError to report validation failures
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* validate: async ({ listKey, fieldKey, operation, inputData, item, resolvedData, context, addValidationError }) => {
|
|
224
|
+
* if (operation === 'delete') return
|
|
225
|
+
* const fieldValue = resolvedData[fieldKey]
|
|
226
|
+
* if (typeof fieldValue === 'string' && fieldValue.includes('spam')) {
|
|
227
|
+
* addValidationError('Field cannot contain spam')
|
|
228
|
+
* }
|
|
229
|
+
* }
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
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
|
|
238
|
+
|
|
79
239
|
/**
|
|
80
240
|
* Perform side effects before database write
|
|
81
241
|
* Called during create/update/delete operations after validation and access control
|
|
@@ -83,75 +243,40 @@ export type FieldHooks<
|
|
|
83
243
|
*
|
|
84
244
|
* @example
|
|
85
245
|
* ```typescript
|
|
86
|
-
* beforeOperation: async ({
|
|
246
|
+
* beforeOperation: async ({ listKey, fieldKey, operation, inputData, item, resolvedData, context }) => {
|
|
87
247
|
* // For create operations, item is undefined
|
|
88
248
|
* // For update/delete operations, item is the existing record
|
|
249
|
+
* const fieldValue = resolvedData?.[fieldKey]
|
|
89
250
|
* if (operation === 'update' && item) {
|
|
90
|
-
* console.log(`Updating field from ${item
|
|
251
|
+
* console.log(`Updating field from ${item[fieldKey]} to ${fieldValue}`)
|
|
91
252
|
* }
|
|
92
253
|
* await sendAuditLog({ operation, item })
|
|
93
254
|
* }
|
|
94
255
|
* ```
|
|
95
256
|
*/
|
|
96
257
|
beforeOperation?: (
|
|
97
|
-
args:
|
|
98
|
-
| {
|
|
99
|
-
operation: 'create'
|
|
100
|
-
resolvedValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
101
|
-
item: undefined
|
|
102
|
-
listKey: string
|
|
103
|
-
fieldName: TFieldKey
|
|
104
|
-
context: import('../access/types.js').AccessContext
|
|
105
|
-
}
|
|
106
|
-
| {
|
|
107
|
-
operation: 'update' | 'delete'
|
|
108
|
-
resolvedValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
109
|
-
item: TTypeInfo['item']
|
|
110
|
-
listKey: string
|
|
111
|
-
fieldName: TFieldKey
|
|
112
|
-
context: import('../access/types.js').AccessContext
|
|
113
|
-
},
|
|
258
|
+
args: FieldBeforeOperationHookArgs<TTypeInfo, TFieldKey>,
|
|
114
259
|
) => Promise<void> | void
|
|
115
260
|
|
|
116
261
|
/**
|
|
117
262
|
* Perform side effects after database operation
|
|
118
|
-
* Called after any database operation (create/update/delete
|
|
263
|
+
* Called after any database operation (create/update/delete)
|
|
119
264
|
* This should ONLY contain side effects (logging, cache invalidation, etc.), not data transformation
|
|
120
265
|
*
|
|
121
266
|
* @example
|
|
122
267
|
* ```typescript
|
|
123
|
-
* afterOperation: async ({ operation,
|
|
124
|
-
* // For
|
|
268
|
+
* afterOperation: async ({ listKey, fieldKey, operation, inputData, item, originalItem, resolvedData, context }) => {
|
|
269
|
+
* // For create operations, originalItem is undefined
|
|
125
270
|
* // For update/delete operations, originalItem is the item before the operation
|
|
126
271
|
* if (operation === 'update' && originalItem) {
|
|
127
|
-
* console.log('Changed from:', originalItem[
|
|
272
|
+
* console.log('Changed from:', originalItem[fieldKey], 'to:', item[fieldKey])
|
|
128
273
|
* }
|
|
129
274
|
* await invalidateCache({ listKey, itemId: item.id })
|
|
130
275
|
* await sendWebhook({ operation, item })
|
|
131
276
|
* }
|
|
132
277
|
* ```
|
|
133
278
|
*/
|
|
134
|
-
afterOperation?: (
|
|
135
|
-
args:
|
|
136
|
-
| {
|
|
137
|
-
operation: 'query' | 'create'
|
|
138
|
-
value: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
139
|
-
item: TTypeInfo['item']
|
|
140
|
-
originalItem: undefined
|
|
141
|
-
listKey: string
|
|
142
|
-
fieldName: TFieldKey
|
|
143
|
-
context: import('../access/types.js').AccessContext
|
|
144
|
-
}
|
|
145
|
-
| {
|
|
146
|
-
operation: 'update' | 'delete'
|
|
147
|
-
value: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
148
|
-
item: TTypeInfo['item']
|
|
149
|
-
originalItem: TTypeInfo['item']
|
|
150
|
-
listKey: string
|
|
151
|
-
fieldName: TFieldKey
|
|
152
|
-
context: import('../access/types.js').AccessContext
|
|
153
|
-
},
|
|
154
|
-
) => Promise<void> | void
|
|
279
|
+
afterOperation?: (args: FieldAfterOperationHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void
|
|
155
280
|
|
|
156
281
|
/**
|
|
157
282
|
* Transform field value after database read
|
|
@@ -168,14 +293,9 @@ export type FieldHooks<
|
|
|
168
293
|
* }
|
|
169
294
|
* ```
|
|
170
295
|
*/
|
|
171
|
-
resolveOutput?: (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
item: TTypeInfo['item']
|
|
175
|
-
listKey: string
|
|
176
|
-
fieldName: TFieldKey
|
|
177
|
-
context: import('../access/types.js').AccessContext
|
|
178
|
-
}) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
296
|
+
resolveOutput?: (
|
|
297
|
+
args: FieldResolveOutputHookArgs<TTypeInfo, TFieldKey>,
|
|
298
|
+
) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined
|
|
179
299
|
}
|
|
180
300
|
|
|
181
301
|
/**
|
|
@@ -206,7 +326,11 @@ export type ResultExtensionConfig = {
|
|
|
206
326
|
|
|
207
327
|
export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
208
328
|
type: string
|
|
209
|
-
access?: FieldAccess
|
|
329
|
+
access?: FieldAccess<
|
|
330
|
+
TTypeInfo['item'],
|
|
331
|
+
TTypeInfo['inputs']['create'],
|
|
332
|
+
TTypeInfo['inputs']['update']
|
|
333
|
+
>
|
|
210
334
|
defaultValue?: unknown
|
|
211
335
|
hooks?: FieldHooks<TTypeInfo>
|
|
212
336
|
/**
|
|
@@ -471,6 +595,37 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
471
595
|
* ```
|
|
472
596
|
*/
|
|
473
597
|
foreignKey?: boolean | { map?: string }
|
|
598
|
+
/**
|
|
599
|
+
* Extend or modify the generated Prisma schema lines for this relationship field
|
|
600
|
+
* Receives the generated FK line (if applicable) and relation line
|
|
601
|
+
* Returns the modified lines
|
|
602
|
+
*
|
|
603
|
+
* @example Add onDelete cascade for self-referential relationship
|
|
604
|
+
* ```typescript
|
|
605
|
+
* parent: relationship({
|
|
606
|
+
* ref: 'Category.children',
|
|
607
|
+
* db: {
|
|
608
|
+
* foreignKey: true,
|
|
609
|
+
* extendPrismaSchema: ({ fkLine, relationLine }) => ({
|
|
610
|
+
* fkLine,
|
|
611
|
+
* relationLine: relationLine.replace(
|
|
612
|
+
* '@relation(',
|
|
613
|
+
* '@relation(onDelete: SetNull, '
|
|
614
|
+
* )
|
|
615
|
+
* })
|
|
616
|
+
* }
|
|
617
|
+
* })
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
620
|
+
extendPrismaSchema?: (lines: {
|
|
621
|
+
/** The foreign key field line (e.g., "parentId String?"), only present for single relationships that own the FK */
|
|
622
|
+
fkLine?: string
|
|
623
|
+
/** The relation field line (e.g., "parent Category? @relation(...)") */
|
|
624
|
+
relationLine: string
|
|
625
|
+
}) => {
|
|
626
|
+
fkLine?: string
|
|
627
|
+
relationLine: string
|
|
628
|
+
}
|
|
474
629
|
}
|
|
475
630
|
ui?: {
|
|
476
631
|
displayMode?: 'select' | 'cards'
|
|
@@ -684,6 +839,48 @@ export type OperationAccess<T = any> = {
|
|
|
684
839
|
delete?: AccessControl<T>
|
|
685
840
|
}
|
|
686
841
|
|
|
842
|
+
/**
|
|
843
|
+
* List-level access control configuration
|
|
844
|
+
* Supports two patterns:
|
|
845
|
+
*
|
|
846
|
+
* 1. Function shorthand - applies to all CRUD operations:
|
|
847
|
+
* `access: isAdmin`
|
|
848
|
+
*
|
|
849
|
+
* 2. Object form - configure operations individually:
|
|
850
|
+
* `access: { operation: { query: () => true, create: isAdmin } }`
|
|
851
|
+
*
|
|
852
|
+
* @example Function shorthand
|
|
853
|
+
* ```typescript
|
|
854
|
+
* const isAdmin = ({ session }) => session?.role === 'admin'
|
|
855
|
+
*
|
|
856
|
+
* list({
|
|
857
|
+
* access: isAdmin, // Applies to query, create, update, delete
|
|
858
|
+
* fields: { ... }
|
|
859
|
+
* })
|
|
860
|
+
* ```
|
|
861
|
+
*
|
|
862
|
+
* @example Object form
|
|
863
|
+
* ```typescript
|
|
864
|
+
* list({
|
|
865
|
+
* access: {
|
|
866
|
+
* operation: {
|
|
867
|
+
* query: () => true,
|
|
868
|
+
* create: isAdmin,
|
|
869
|
+
* update: isOwner,
|
|
870
|
+
* delete: isAdmin,
|
|
871
|
+
* }
|
|
872
|
+
* },
|
|
873
|
+
* fields: { ... }
|
|
874
|
+
* })
|
|
875
|
+
* ```
|
|
876
|
+
*/
|
|
877
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
878
|
+
export type ListAccessControl<T = any> =
|
|
879
|
+
| AccessControl<T>
|
|
880
|
+
| {
|
|
881
|
+
operation?: OperationAccess<T>
|
|
882
|
+
}
|
|
883
|
+
|
|
687
884
|
/**
|
|
688
885
|
* Hook arguments for resolveInput hook
|
|
689
886
|
* Uses discriminated union to provide proper types based on operation
|
|
@@ -696,58 +893,90 @@ export type ResolveInputHookArgs<
|
|
|
696
893
|
TUpdateInput = Record<string, unknown>,
|
|
697
894
|
> =
|
|
698
895
|
| {
|
|
896
|
+
listKey: string
|
|
699
897
|
operation: 'create'
|
|
898
|
+
inputData: TCreateInput
|
|
700
899
|
resolvedData: TCreateInput
|
|
701
900
|
item: undefined
|
|
702
901
|
context: import('../access/types.js').AccessContext
|
|
703
902
|
}
|
|
704
903
|
| {
|
|
904
|
+
listKey: string
|
|
705
905
|
operation: 'update'
|
|
906
|
+
inputData: TUpdateInput
|
|
706
907
|
resolvedData: TUpdateInput
|
|
707
908
|
item: TOutput
|
|
708
909
|
context: import('../access/types.js').AccessContext
|
|
709
910
|
}
|
|
710
911
|
|
|
711
912
|
/**
|
|
712
|
-
* Hook arguments for validateInput
|
|
913
|
+
* Hook arguments for validate hook (renamed from validateInput for Keystone compatibility)
|
|
713
914
|
* Uses discriminated union to provide proper types based on operation
|
|
714
915
|
* - create: resolvedData is CreateInput, item is undefined
|
|
715
916
|
* - update: resolvedData is UpdateInput, item is the existing record
|
|
917
|
+
* - delete: item is the item being deleted
|
|
716
918
|
*/
|
|
717
|
-
export type
|
|
919
|
+
export type ValidateHookArgs<
|
|
718
920
|
TOutput = Record<string, unknown>,
|
|
719
921
|
TCreateInput = Record<string, unknown>,
|
|
720
922
|
TUpdateInput = Record<string, unknown>,
|
|
721
923
|
> =
|
|
722
924
|
| {
|
|
925
|
+
listKey: string
|
|
723
926
|
operation: 'create'
|
|
927
|
+
inputData: TCreateInput
|
|
724
928
|
resolvedData: TCreateInput
|
|
725
929
|
item: undefined
|
|
726
930
|
context: import('../access/types.js').AccessContext
|
|
727
931
|
addValidationError: (msg: string) => void
|
|
728
932
|
}
|
|
729
933
|
| {
|
|
934
|
+
listKey: string
|
|
730
935
|
operation: 'update'
|
|
936
|
+
inputData: TUpdateInput
|
|
731
937
|
resolvedData: TUpdateInput
|
|
732
938
|
item: TOutput
|
|
733
939
|
context: import('../access/types.js').AccessContext
|
|
734
940
|
addValidationError: (msg: string) => void
|
|
735
941
|
}
|
|
942
|
+
| {
|
|
943
|
+
listKey: string
|
|
944
|
+
operation: 'delete'
|
|
945
|
+
item: TOutput
|
|
946
|
+
context: import('../access/types.js').AccessContext
|
|
947
|
+
addValidationError: (msg: string) => void
|
|
948
|
+
}
|
|
736
949
|
|
|
737
950
|
/**
|
|
738
951
|
* Hook arguments for beforeOperation hook
|
|
739
952
|
* Uses discriminated union to provide proper types based on operation
|
|
740
|
-
* - create:
|
|
741
|
-
* - update:
|
|
742
|
-
* - delete:
|
|
953
|
+
* - create: has inputData and resolvedData, no item
|
|
954
|
+
* - update: has inputData, resolvedData, and item
|
|
955
|
+
* - delete: has item only
|
|
743
956
|
*/
|
|
744
|
-
export type BeforeOperationHookArgs<
|
|
957
|
+
export type BeforeOperationHookArgs<
|
|
958
|
+
TOutput = Record<string, unknown>,
|
|
959
|
+
TCreateInput = Record<string, unknown>,
|
|
960
|
+
TUpdateInput = Record<string, unknown>,
|
|
961
|
+
> =
|
|
745
962
|
| {
|
|
963
|
+
listKey: string
|
|
746
964
|
operation: 'create'
|
|
965
|
+
inputData: TCreateInput
|
|
966
|
+
resolvedData: TCreateInput
|
|
967
|
+
context: import('../access/types.js').AccessContext
|
|
968
|
+
}
|
|
969
|
+
| {
|
|
970
|
+
listKey: string
|
|
971
|
+
operation: 'update'
|
|
972
|
+
inputData: TUpdateInput
|
|
973
|
+
item: TOutput
|
|
974
|
+
resolvedData: TUpdateInput
|
|
747
975
|
context: import('../access/types.js').AccessContext
|
|
748
976
|
}
|
|
749
977
|
| {
|
|
750
|
-
|
|
978
|
+
listKey: string
|
|
979
|
+
operation: 'delete'
|
|
751
980
|
item: TOutput
|
|
752
981
|
context: import('../access/types.js').AccessContext
|
|
753
982
|
}
|
|
@@ -755,20 +984,35 @@ export type BeforeOperationHookArgs<TOutput = Record<string, unknown>> =
|
|
|
755
984
|
/**
|
|
756
985
|
* Hook arguments for afterOperation hook
|
|
757
986
|
* Uses discriminated union to provide proper types based on operation
|
|
758
|
-
* - create: has item, no originalItem
|
|
759
|
-
* - update: has item,
|
|
760
|
-
* - delete: has
|
|
987
|
+
* - create: has item, inputData, and resolvedData, no originalItem
|
|
988
|
+
* - update: has item, originalItem, inputData, and resolvedData
|
|
989
|
+
* - delete: has originalItem only
|
|
761
990
|
*/
|
|
762
|
-
export type AfterOperationHookArgs<
|
|
991
|
+
export type AfterOperationHookArgs<
|
|
992
|
+
TOutput = Record<string, unknown>,
|
|
993
|
+
TCreateInput = Record<string, unknown>,
|
|
994
|
+
TUpdateInput = Record<string, unknown>,
|
|
995
|
+
> =
|
|
763
996
|
| {
|
|
997
|
+
listKey: string
|
|
764
998
|
operation: 'create'
|
|
999
|
+
inputData: TCreateInput
|
|
765
1000
|
item: TOutput
|
|
766
|
-
|
|
1001
|
+
resolvedData: TCreateInput
|
|
767
1002
|
context: import('../access/types.js').AccessContext
|
|
768
1003
|
}
|
|
769
1004
|
| {
|
|
770
|
-
|
|
1005
|
+
listKey: string
|
|
1006
|
+
operation: 'update'
|
|
1007
|
+
inputData: TUpdateInput
|
|
1008
|
+
originalItem: TOutput
|
|
771
1009
|
item: TOutput
|
|
1010
|
+
resolvedData: TUpdateInput
|
|
1011
|
+
context: import('../access/types.js').AccessContext
|
|
1012
|
+
}
|
|
1013
|
+
| {
|
|
1014
|
+
listKey: string
|
|
1015
|
+
operation: 'delete'
|
|
772
1016
|
originalItem: TOutput
|
|
773
1017
|
context: import('../access/types.js').AccessContext
|
|
774
1018
|
}
|
|
@@ -781,19 +1025,34 @@ export type Hooks<
|
|
|
781
1025
|
resolveInput?: (
|
|
782
1026
|
args: ResolveInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
|
|
783
1027
|
) => Promise<TCreateInput | TUpdateInput>
|
|
784
|
-
|
|
785
|
-
|
|
1028
|
+
validate?: (args: ValidateHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>
|
|
1029
|
+
beforeOperation?: (
|
|
1030
|
+
args: BeforeOperationHookArgs<TOutput, TCreateInput, TUpdateInput>,
|
|
1031
|
+
) => Promise<void>
|
|
1032
|
+
afterOperation?: (
|
|
1033
|
+
args: AfterOperationHookArgs<TOutput, TCreateInput, TUpdateInput>,
|
|
786
1034
|
) => Promise<void>
|
|
787
|
-
|
|
788
|
-
|
|
1035
|
+
/**
|
|
1036
|
+
* @deprecated Use 'validate' instead. This alias is provided for backwards compatibility.
|
|
1037
|
+
*/
|
|
1038
|
+
validateInput?: (args: ValidateHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>
|
|
789
1039
|
}
|
|
790
1040
|
|
|
791
1041
|
// Generic `any` default allows ListConfig to work with any list item type
|
|
792
1042
|
// This is needed because the item type varies per list and is inferred from Prisma models
|
|
1043
|
+
/**
|
|
1044
|
+
* Internal list configuration type (after normalization by list() function)
|
|
1045
|
+
* Access control is always in object form internally.
|
|
1046
|
+
* Use list() function which accepts both function shorthand and object form.
|
|
1047
|
+
*/
|
|
793
1048
|
export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
794
1049
|
// Field configs are automatically transformed to inject the full TypeInfo
|
|
795
1050
|
// This enables proper typing in field hooks where item, create input, and update input are all typed
|
|
796
1051
|
fields: FieldsWithTypeInfo<TTypeInfo>
|
|
1052
|
+
/**
|
|
1053
|
+
* Access control configuration for this list (normalized object form).
|
|
1054
|
+
* The list() function normalizes function shorthand to this object form.
|
|
1055
|
+
*/
|
|
797
1056
|
access?: {
|
|
798
1057
|
operation?: OperationAccess<TTypeInfo['item']>
|
|
799
1058
|
}
|
|
@@ -802,6 +1061,58 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
802
1061
|
* MCP server configuration for this list
|
|
803
1062
|
*/
|
|
804
1063
|
mcp?: ListMcpConfig
|
|
1064
|
+
/**
|
|
1065
|
+
* Restricts this list to a single record (singleton pattern)
|
|
1066
|
+
* When true:
|
|
1067
|
+
* - Prevents creating multiple records
|
|
1068
|
+
* - Auto-creates the single record on first access (if autoCreate: true, which is the default)
|
|
1069
|
+
* - Provides a get() method for easy access to the singleton
|
|
1070
|
+
* - Blocks delete and findMany operations
|
|
1071
|
+
* - Changes UI to show edit form instead of list view
|
|
1072
|
+
*
|
|
1073
|
+
* @example Simple boolean (auto-create enabled)
|
|
1074
|
+
* ```typescript
|
|
1075
|
+
* isSingleton: true
|
|
1076
|
+
* ```
|
|
1077
|
+
*
|
|
1078
|
+
* @example With options
|
|
1079
|
+
* ```typescript
|
|
1080
|
+
* isSingleton: {
|
|
1081
|
+
* autoCreate: false // Don't auto-create, must be created manually
|
|
1082
|
+
* }
|
|
1083
|
+
* ```
|
|
1084
|
+
*/
|
|
1085
|
+
isSingleton?:
|
|
1086
|
+
| boolean
|
|
1087
|
+
| {
|
|
1088
|
+
/**
|
|
1089
|
+
* Auto-create the singleton record on first access using field defaults
|
|
1090
|
+
* @default true
|
|
1091
|
+
*/
|
|
1092
|
+
autoCreate?: boolean
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Input type for the list() function
|
|
1098
|
+
* Accepts both function shorthand and object form for access control.
|
|
1099
|
+
*/
|
|
1100
|
+
export type ListConfigInput<TTypeInfo extends TypeInfo> = Omit<ListConfig<TTypeInfo>, 'access'> & {
|
|
1101
|
+
/**
|
|
1102
|
+
* Access control configuration for this list.
|
|
1103
|
+
* Supports both function shorthand and object form.
|
|
1104
|
+
*
|
|
1105
|
+
* @example Function shorthand (applies to all operations)
|
|
1106
|
+
* ```typescript
|
|
1107
|
+
* access: isAdmin
|
|
1108
|
+
* ```
|
|
1109
|
+
*
|
|
1110
|
+
* @example Object form (per-operation)
|
|
1111
|
+
* ```typescript
|
|
1112
|
+
* access: { operation: { query: () => true, create: isAdmin } }
|
|
1113
|
+
* ```
|
|
1114
|
+
*/
|
|
1115
|
+
access?: ListAccessControl<TTypeInfo['item']>
|
|
805
1116
|
}
|
|
806
1117
|
|
|
807
1118
|
/**
|