@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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +291 -0
  3. package/README.md +6 -3
  4. package/dist/access/engine.d.ts +2 -0
  5. package/dist/access/engine.d.ts.map +1 -1
  6. package/dist/access/engine.js +8 -6
  7. package/dist/access/engine.js.map +1 -1
  8. package/dist/access/engine.test.js +4 -0
  9. package/dist/access/engine.test.js.map +1 -1
  10. package/dist/access/types.d.ts +31 -4
  11. package/dist/access/types.d.ts.map +1 -1
  12. package/dist/config/index.d.ts +12 -10
  13. package/dist/config/index.d.ts.map +1 -1
  14. package/dist/config/index.js +37 -1
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/types.d.ts +341 -82
  17. package/dist/config/types.d.ts.map +1 -1
  18. package/dist/context/index.d.ts.map +1 -1
  19. package/dist/context/index.js +330 -60
  20. package/dist/context/index.js.map +1 -1
  21. package/dist/context/nested-operations.d.ts.map +1 -1
  22. package/dist/context/nested-operations.js +38 -25
  23. package/dist/context/nested-operations.js.map +1 -1
  24. package/dist/hooks/index.d.ts +45 -7
  25. package/dist/hooks/index.d.ts.map +1 -1
  26. package/dist/hooks/index.js +10 -4
  27. package/dist/hooks/index.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/access/engine.test.ts +4 -0
  33. package/src/access/engine.ts +10 -7
  34. package/src/access/types.ts +45 -4
  35. package/src/config/index.ts +65 -9
  36. package/src/config/types.ts +402 -91
  37. package/src/context/index.ts +421 -82
  38. package/src/context/nested-operations.ts +40 -25
  39. package/src/hooks/index.ts +66 -14
  40. package/src/index.ts +11 -0
  41. package/tests/access.test.ts +28 -28
  42. package/tests/config.test.ts +20 -3
  43. package/tests/nested-access-and-hooks.test.ts +8 -3
  44. package/tests/singleton.test.ts +329 -0
  45. package/tests/sudo.test.ts +2 -13
  46. package/tsconfig.tsbuildinfo +1 -1
@@ -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 ({ inputValue, operation, item }) => {
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
- * if (typeof inputValue === 'string' && !isHashedPassword(inputValue)) {
50
- * return await hashPassword(inputValue)
201
+ * const fieldValue = resolvedData[fieldKey]
202
+ * if (typeof fieldValue === 'string' && !isHashedPassword(fieldValue)) {
203
+ * return await hashPassword(fieldValue)
51
204
  * }
52
- * return inputValue
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 ({ resolvedValue, operation, item }) => {
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.fieldName} to ${resolvedValue}`)
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/query)
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, value, item, originalItem }) => {
124
- * // For query/create operations, originalItem is undefined
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[fieldName], 'to:', value)
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?: (args: {
172
- operation: 'query'
173
- value: GetFieldValueType<TTypeInfo['fields'], TFieldKey>
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 hook
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 ValidateInputHookArgs<
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: no resolvedData, no item
741
- * - update: no resolvedData, has item
742
- * - delete: no resolvedData, has item
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<TOutput = Record<string, unknown>> =
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
- operation: 'update' | 'delete'
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, has originalItem
760
- * - delete: has item, has originalItem
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<TOutput = Record<string, unknown>> =
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
- originalItem: undefined
1001
+ resolvedData: TCreateInput
767
1002
  context: import('../access/types.js').AccessContext
768
1003
  }
769
1004
  | {
770
- operation: 'update' | 'delete'
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
- validateInput?: (
785
- args: ValidateInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
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
- beforeOperation?: (args: BeforeOperationHookArgs<TOutput>) => Promise<void>
788
- afterOperation?: (args: AfterOperationHookArgs<TOutput>) => Promise<void>
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
  /**