@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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,MAAM,CAAC,UAA0B;IAC/C,wEAAwE;IACxE,IAAI,CAAC,UAAU,CAAC,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,OAAO,UAAU,CAAA;IACnB,CAAC;IAED,qCAAqC;IACrC,OAAO,cAAc,CAAC,UAAU,CAAC,CAAA;AACnC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,UAAU,IAAI,CAAkD,MAOrE;IACC,0CAA0C;IAC1C,8DAA8D;IAC9D,OAAO,MAA+B,CAAA;AACxC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGnD;;;GAGG;AACH,SAAS,mBAAmB,CAC1B,MAAwC;IAExC,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAE7B,wEAAwE;IACxE,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,MAA0B,CAAA;QACrC,OAAO;YACL,SAAS,EAAE;gBACT,KAAK,EAAE,EAAE;gBACT,MAAM,EAAE,EAAE;gBACV,MAAM,EAAE,EAAE;gBACV,MAAM,EAAE,EAAE;aACX;SACF,CAAA;IACH,CAAC;IAED,yBAAyB;IACzB,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,MAAM,CAAC,UAA0B;IAC/C,wEAAwE;IACxE,IAAI,CAAC,UAAU,CAAC,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,OAAO,UAAU,CAAA;IACnB,CAAC;IAED,qCAAqC;IACrC,OAAO,cAAc,CAAC,UAAU,CAAC,CAAA;AACnC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,UAAU,IAAI,CAClB,MAAkC;IAElC,oDAAoD;IACpD,MAAM,gBAAgB,GAAG;QACvB,GAAG,MAAM;QACT,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC;KAC3C,CAAA;IAED,0CAA0C;IAC1C,8DAA8D;IAC9D,OAAO,gBAAyC,CAAA;AAClD,CAAC"}
@@ -4,6 +4,126 @@ import type { z } from 'zod';
4
4
  * Field configuration types
5
5
  */
6
6
  export type FieldType = 'text' | 'integer' | 'checkbox' | 'timestamp' | 'password' | 'select' | 'relationship' | string;
7
+ /**
8
+ * Field-level hook argument types (exported for user annotations)
9
+ */
10
+ /**
11
+ * Arguments for field-level resolveInput hook
12
+ * Used to transform field values before database write
13
+ */
14
+ export type FieldResolveInputHookArgs<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>> = {
15
+ listKey: string;
16
+ fieldKey: TFieldKey;
17
+ operation: 'create';
18
+ inputData: TTypeInfo['inputs']['create'];
19
+ item: undefined;
20
+ resolvedData: TTypeInfo['inputs']['create'];
21
+ context: import('../access/types.js').AccessContext;
22
+ } | {
23
+ listKey: string;
24
+ fieldKey: TFieldKey;
25
+ operation: 'update';
26
+ inputData: TTypeInfo['inputs']['update'];
27
+ item: TTypeInfo['item'];
28
+ resolvedData: TTypeInfo['inputs']['update'];
29
+ context: import('../access/types.js').AccessContext;
30
+ };
31
+ /**
32
+ * Arguments for field-level validate hook
33
+ * Used for custom validation logic
34
+ */
35
+ export type FieldValidateHookArgs<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>> = {
36
+ listKey: string;
37
+ fieldKey: TFieldKey;
38
+ operation: 'create';
39
+ inputData: TTypeInfo['inputs']['create'];
40
+ item: undefined;
41
+ resolvedData: TTypeInfo['inputs']['create'];
42
+ context: import('../access/types.js').AccessContext;
43
+ addValidationError: (msg: string) => void;
44
+ } | {
45
+ listKey: string;
46
+ fieldKey: TFieldKey;
47
+ operation: 'update';
48
+ inputData: TTypeInfo['inputs']['update'];
49
+ item: TTypeInfo['item'];
50
+ resolvedData: TTypeInfo['inputs']['update'];
51
+ context: import('../access/types.js').AccessContext;
52
+ addValidationError: (msg: string) => void;
53
+ } | {
54
+ listKey: string;
55
+ fieldKey: TFieldKey;
56
+ operation: 'delete';
57
+ item: TTypeInfo['item'];
58
+ context: import('../access/types.js').AccessContext;
59
+ addValidationError: (msg: string) => void;
60
+ };
61
+ /**
62
+ * Arguments for field-level beforeOperation hook
63
+ * Used for side effects before database write
64
+ */
65
+ export type FieldBeforeOperationHookArgs<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>> = {
66
+ listKey: string;
67
+ fieldKey: TFieldKey;
68
+ operation: 'create';
69
+ inputData: TTypeInfo['inputs']['create'];
70
+ resolvedData: TTypeInfo['inputs']['create'];
71
+ context: import('../access/types.js').AccessContext;
72
+ } | {
73
+ listKey: string;
74
+ fieldKey: TFieldKey;
75
+ operation: 'update';
76
+ inputData: TTypeInfo['inputs']['update'];
77
+ item: TTypeInfo['item'];
78
+ resolvedData: TTypeInfo['inputs']['update'];
79
+ context: import('../access/types.js').AccessContext;
80
+ } | {
81
+ listKey: string;
82
+ fieldKey: TFieldKey;
83
+ operation: 'delete';
84
+ item: TTypeInfo['item'];
85
+ context: import('../access/types.js').AccessContext;
86
+ };
87
+ /**
88
+ * Arguments for field-level afterOperation hook
89
+ * Used for side effects after database operation
90
+ */
91
+ export type FieldAfterOperationHookArgs<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>> = {
92
+ listKey: string;
93
+ fieldKey: TFieldKey;
94
+ operation: 'create';
95
+ inputData: TTypeInfo['inputs']['create'];
96
+ item: TTypeInfo['item'];
97
+ resolvedData: TTypeInfo['inputs']['create'];
98
+ context: import('../access/types.js').AccessContext;
99
+ } | {
100
+ listKey: string;
101
+ fieldKey: TFieldKey;
102
+ operation: 'update';
103
+ inputData: TTypeInfo['inputs']['update'];
104
+ originalItem: TTypeInfo['item'];
105
+ item: TTypeInfo['item'];
106
+ resolvedData: TTypeInfo['inputs']['update'];
107
+ context: import('../access/types.js').AccessContext;
108
+ } | {
109
+ listKey: string;
110
+ fieldKey: TFieldKey;
111
+ operation: 'delete';
112
+ originalItem: TTypeInfo['item'];
113
+ context: import('../access/types.js').AccessContext;
114
+ };
115
+ /**
116
+ * Arguments for field-level resolveOutput hook
117
+ * Used to transform field values after database read
118
+ */
119
+ export type FieldResolveOutputHookArgs<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>> = {
120
+ operation: 'query';
121
+ value: GetFieldValueType<TTypeInfo['fields'], TFieldKey>;
122
+ item: TTypeInfo['item'];
123
+ listKey: string;
124
+ fieldName: TFieldKey;
125
+ context: import('../access/types.js').AccessContext;
126
+ };
7
127
  /**
8
128
  * Field-level hooks for data transformation and side effects
9
129
  * Allows field types to define custom behavior during operations
@@ -30,31 +150,39 @@ export type FieldHooks<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<T
30
150
  *
31
151
  * @example
32
152
  * ```typescript
33
- * resolveInput: async ({ inputValue, operation, item }) => {
153
+ * resolveInput: async ({ listKey, fieldKey, operation, inputData, item, resolvedData, context }) => {
34
154
  * // For create operations, item is undefined
35
155
  * // For update operations, item is the existing record
36
- * if (typeof inputValue === 'string' && !isHashedPassword(inputValue)) {
37
- * return await hashPassword(inputValue)
156
+ * const fieldValue = resolvedData[fieldKey]
157
+ * if (typeof fieldValue === 'string' && !isHashedPassword(fieldValue)) {
158
+ * return await hashPassword(fieldValue)
38
159
  * }
39
- * return inputValue
160
+ * return fieldValue
40
161
  * }
41
162
  * ```
42
163
  */
43
- resolveInput?: (args: {
44
- operation: 'create';
45
- inputValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
46
- item: undefined;
47
- listKey: string;
48
- fieldName: TFieldKey;
49
- context: import('../access/types.js').AccessContext;
50
- } | {
51
- operation: 'update';
52
- inputValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
53
- item: TTypeInfo['item'];
54
- listKey: string;
55
- fieldName: TFieldKey;
56
- context: import('../access/types.js').AccessContext;
57
- }) => Promise<GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined> | GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
164
+ resolveInput?: (args: FieldResolveInputHookArgs<TTypeInfo, TFieldKey>) => Promise<GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined> | GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
165
+ /**
166
+ * Validate field value after resolveInput
167
+ * Called during create/update operations after resolveInput hooks but before database write
168
+ * Use addValidationError to report validation failures
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * validate: async ({ listKey, fieldKey, operation, inputData, item, resolvedData, context, addValidationError }) => {
173
+ * if (operation === 'delete') return
174
+ * const fieldValue = resolvedData[fieldKey]
175
+ * if (typeof fieldValue === 'string' && fieldValue.includes('spam')) {
176
+ * addValidationError('Field cannot contain spam')
177
+ * }
178
+ * }
179
+ * ```
180
+ */
181
+ validate?: (args: FieldValidateHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void;
182
+ /**
183
+ * @deprecated Use 'validate' instead. This alias is provided for backwards compatibility.
184
+ */
185
+ validateInput?: (args: FieldValidateHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void;
58
186
  /**
59
187
  * Perform side effects before database write
60
188
  * Called during create/update/delete operations after validation and access control
@@ -62,66 +190,37 @@ export type FieldHooks<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<T
62
190
  *
63
191
  * @example
64
192
  * ```typescript
65
- * beforeOperation: async ({ resolvedValue, operation, item }) => {
193
+ * beforeOperation: async ({ listKey, fieldKey, operation, inputData, item, resolvedData, context }) => {
66
194
  * // For create operations, item is undefined
67
195
  * // For update/delete operations, item is the existing record
196
+ * const fieldValue = resolvedData?.[fieldKey]
68
197
  * if (operation === 'update' && item) {
69
- * console.log(`Updating field from ${item.fieldName} to ${resolvedValue}`)
198
+ * console.log(`Updating field from ${item[fieldKey]} to ${fieldValue}`)
70
199
  * }
71
200
  * await sendAuditLog({ operation, item })
72
201
  * }
73
202
  * ```
74
203
  */
75
- beforeOperation?: (args: {
76
- operation: 'create';
77
- resolvedValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
78
- item: undefined;
79
- listKey: string;
80
- fieldName: TFieldKey;
81
- context: import('../access/types.js').AccessContext;
82
- } | {
83
- operation: 'update' | 'delete';
84
- resolvedValue: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
85
- item: TTypeInfo['item'];
86
- listKey: string;
87
- fieldName: TFieldKey;
88
- context: import('../access/types.js').AccessContext;
89
- }) => Promise<void> | void;
204
+ beforeOperation?: (args: FieldBeforeOperationHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void;
90
205
  /**
91
206
  * Perform side effects after database operation
92
- * Called after any database operation (create/update/delete/query)
207
+ * Called after any database operation (create/update/delete)
93
208
  * This should ONLY contain side effects (logging, cache invalidation, etc.), not data transformation
94
209
  *
95
210
  * @example
96
211
  * ```typescript
97
- * afterOperation: async ({ operation, value, item, originalItem }) => {
98
- * // For query/create operations, originalItem is undefined
212
+ * afterOperation: async ({ listKey, fieldKey, operation, inputData, item, originalItem, resolvedData, context }) => {
213
+ * // For create operations, originalItem is undefined
99
214
  * // For update/delete operations, originalItem is the item before the operation
100
215
  * if (operation === 'update' && originalItem) {
101
- * console.log('Changed from:', originalItem[fieldName], 'to:', value)
216
+ * console.log('Changed from:', originalItem[fieldKey], 'to:', item[fieldKey])
102
217
  * }
103
218
  * await invalidateCache({ listKey, itemId: item.id })
104
219
  * await sendWebhook({ operation, item })
105
220
  * }
106
221
  * ```
107
222
  */
108
- afterOperation?: (args: {
109
- operation: 'query' | 'create';
110
- value: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
111
- item: TTypeInfo['item'];
112
- originalItem: undefined;
113
- listKey: string;
114
- fieldName: TFieldKey;
115
- context: import('../access/types.js').AccessContext;
116
- } | {
117
- operation: 'update' | 'delete';
118
- value: GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
119
- item: TTypeInfo['item'];
120
- originalItem: TTypeInfo['item'];
121
- listKey: string;
122
- fieldName: TFieldKey;
123
- context: import('../access/types.js').AccessContext;
124
- }) => Promise<void> | void;
223
+ afterOperation?: (args: FieldAfterOperationHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void;
125
224
  /**
126
225
  * Transform field value after database read
127
226
  * Called when returning results from query operations
@@ -137,14 +236,7 @@ export type FieldHooks<TTypeInfo extends TypeInfo, TFieldKey extends FieldKeys<T
137
236
  * }
138
237
  * ```
139
238
  */
140
- resolveOutput?: (args: {
141
- operation: 'query';
142
- value: GetFieldValueType<TTypeInfo['fields'], TFieldKey>;
143
- item: TTypeInfo['item'];
144
- listKey: string;
145
- fieldName: TFieldKey;
146
- context: import('../access/types.js').AccessContext;
147
- }) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
239
+ resolveOutput?: (args: FieldResolveOutputHookArgs<TTypeInfo, TFieldKey>) => GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined;
148
240
  };
149
241
  /**
150
242
  * Configuration for Prisma result extensions
@@ -173,7 +265,7 @@ export type ResultExtensionConfig = {
173
265
  };
174
266
  export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
175
267
  type: string;
176
- access?: FieldAccess;
268
+ access?: FieldAccess<TTypeInfo['item'], TTypeInfo['inputs']['create'], TTypeInfo['inputs']['update']>;
177
269
  defaultValue?: unknown;
178
270
  hooks?: FieldHooks<TTypeInfo>;
179
271
  /**
@@ -433,6 +525,37 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> = BaseField
433
525
  foreignKey?: boolean | {
434
526
  map?: string;
435
527
  };
528
+ /**
529
+ * Extend or modify the generated Prisma schema lines for this relationship field
530
+ * Receives the generated FK line (if applicable) and relation line
531
+ * Returns the modified lines
532
+ *
533
+ * @example Add onDelete cascade for self-referential relationship
534
+ * ```typescript
535
+ * parent: relationship({
536
+ * ref: 'Category.children',
537
+ * db: {
538
+ * foreignKey: true,
539
+ * extendPrismaSchema: ({ fkLine, relationLine }) => ({
540
+ * fkLine,
541
+ * relationLine: relationLine.replace(
542
+ * '@relation(',
543
+ * '@relation(onDelete: SetNull, '
544
+ * )
545
+ * })
546
+ * }
547
+ * })
548
+ * ```
549
+ */
550
+ extendPrismaSchema?: (lines: {
551
+ /** The foreign key field line (e.g., "parentId String?"), only present for single relationships that own the FK */
552
+ fkLine?: string;
553
+ /** The relation field line (e.g., "parent Category? @relation(...)") */
554
+ relationLine: string;
555
+ }) => {
556
+ fkLine?: string;
557
+ relationLine: string;
558
+ };
436
559
  };
437
560
  ui?: {
438
561
  displayMode?: 'select' | 'cards';
@@ -601,6 +724,44 @@ export type OperationAccess<T = any> = {
601
724
  update?: AccessControl<T>;
602
725
  delete?: AccessControl<T>;
603
726
  };
727
+ /**
728
+ * List-level access control configuration
729
+ * Supports two patterns:
730
+ *
731
+ * 1. Function shorthand - applies to all CRUD operations:
732
+ * `access: isAdmin`
733
+ *
734
+ * 2. Object form - configure operations individually:
735
+ * `access: { operation: { query: () => true, create: isAdmin } }`
736
+ *
737
+ * @example Function shorthand
738
+ * ```typescript
739
+ * const isAdmin = ({ session }) => session?.role === 'admin'
740
+ *
741
+ * list({
742
+ * access: isAdmin, // Applies to query, create, update, delete
743
+ * fields: { ... }
744
+ * })
745
+ * ```
746
+ *
747
+ * @example Object form
748
+ * ```typescript
749
+ * list({
750
+ * access: {
751
+ * operation: {
752
+ * query: () => true,
753
+ * create: isAdmin,
754
+ * update: isOwner,
755
+ * delete: isAdmin,
756
+ * }
757
+ * },
758
+ * fields: { ... }
759
+ * })
760
+ * ```
761
+ */
762
+ export type ListAccessControl<T = any> = AccessControl<T> | {
763
+ operation?: OperationAccess<T>;
764
+ };
604
765
  /**
605
766
  * Hook arguments for resolveInput hook
606
767
  * Uses discriminated union to provide proper types based on operation
@@ -608,76 +769,125 @@ export type OperationAccess<T = any> = {
608
769
  * - update: resolvedData is UpdateInput, item is the existing record
609
770
  */
610
771
  export type ResolveInputHookArgs<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>> = {
772
+ listKey: string;
611
773
  operation: 'create';
774
+ inputData: TCreateInput;
612
775
  resolvedData: TCreateInput;
613
776
  item: undefined;
614
777
  context: import('../access/types.js').AccessContext;
615
778
  } | {
779
+ listKey: string;
616
780
  operation: 'update';
781
+ inputData: TUpdateInput;
617
782
  resolvedData: TUpdateInput;
618
783
  item: TOutput;
619
784
  context: import('../access/types.js').AccessContext;
620
785
  };
621
786
  /**
622
- * Hook arguments for validateInput hook
787
+ * Hook arguments for validate hook (renamed from validateInput for Keystone compatibility)
623
788
  * Uses discriminated union to provide proper types based on operation
624
789
  * - create: resolvedData is CreateInput, item is undefined
625
790
  * - update: resolvedData is UpdateInput, item is the existing record
791
+ * - delete: item is the item being deleted
626
792
  */
627
- export type ValidateInputHookArgs<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>> = {
793
+ export type ValidateHookArgs<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>> = {
794
+ listKey: string;
628
795
  operation: 'create';
796
+ inputData: TCreateInput;
629
797
  resolvedData: TCreateInput;
630
798
  item: undefined;
631
799
  context: import('../access/types.js').AccessContext;
632
800
  addValidationError: (msg: string) => void;
633
801
  } | {
802
+ listKey: string;
634
803
  operation: 'update';
804
+ inputData: TUpdateInput;
635
805
  resolvedData: TUpdateInput;
636
806
  item: TOutput;
637
807
  context: import('../access/types.js').AccessContext;
638
808
  addValidationError: (msg: string) => void;
809
+ } | {
810
+ listKey: string;
811
+ operation: 'delete';
812
+ item: TOutput;
813
+ context: import('../access/types.js').AccessContext;
814
+ addValidationError: (msg: string) => void;
639
815
  };
640
816
  /**
641
817
  * Hook arguments for beforeOperation hook
642
818
  * Uses discriminated union to provide proper types based on operation
643
- * - create: no resolvedData, no item
644
- * - update: no resolvedData, has item
645
- * - delete: no resolvedData, has item
819
+ * - create: has inputData and resolvedData, no item
820
+ * - update: has inputData, resolvedData, and item
821
+ * - delete: has item only
646
822
  */
647
- export type BeforeOperationHookArgs<TOutput = Record<string, unknown>> = {
823
+ export type BeforeOperationHookArgs<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>> = {
824
+ listKey: string;
648
825
  operation: 'create';
826
+ inputData: TCreateInput;
827
+ resolvedData: TCreateInput;
649
828
  context: import('../access/types.js').AccessContext;
650
829
  } | {
651
- operation: 'update' | 'delete';
830
+ listKey: string;
831
+ operation: 'update';
832
+ inputData: TUpdateInput;
833
+ item: TOutput;
834
+ resolvedData: TUpdateInput;
835
+ context: import('../access/types.js').AccessContext;
836
+ } | {
837
+ listKey: string;
838
+ operation: 'delete';
652
839
  item: TOutput;
653
840
  context: import('../access/types.js').AccessContext;
654
841
  };
655
842
  /**
656
843
  * Hook arguments for afterOperation hook
657
844
  * Uses discriminated union to provide proper types based on operation
658
- * - create: has item, no originalItem
659
- * - update: has item, has originalItem
660
- * - delete: has item, has originalItem
845
+ * - create: has item, inputData, and resolvedData, no originalItem
846
+ * - update: has item, originalItem, inputData, and resolvedData
847
+ * - delete: has originalItem only
661
848
  */
662
- export type AfterOperationHookArgs<TOutput = Record<string, unknown>> = {
849
+ export type AfterOperationHookArgs<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>> = {
850
+ listKey: string;
663
851
  operation: 'create';
852
+ inputData: TCreateInput;
664
853
  item: TOutput;
665
- originalItem: undefined;
854
+ resolvedData: TCreateInput;
666
855
  context: import('../access/types.js').AccessContext;
667
856
  } | {
668
- operation: 'update' | 'delete';
857
+ listKey: string;
858
+ operation: 'update';
859
+ inputData: TUpdateInput;
860
+ originalItem: TOutput;
669
861
  item: TOutput;
862
+ resolvedData: TUpdateInput;
863
+ context: import('../access/types.js').AccessContext;
864
+ } | {
865
+ listKey: string;
866
+ operation: 'delete';
670
867
  originalItem: TOutput;
671
868
  context: import('../access/types.js').AccessContext;
672
869
  };
673
870
  export type Hooks<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>> = {
674
871
  resolveInput?: (args: ResolveInputHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<TCreateInput | TUpdateInput>;
675
- validateInput?: (args: ValidateInputHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>;
676
- beforeOperation?: (args: BeforeOperationHookArgs<TOutput>) => Promise<void>;
677
- afterOperation?: (args: AfterOperationHookArgs<TOutput>) => Promise<void>;
872
+ validate?: (args: ValidateHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>;
873
+ beforeOperation?: (args: BeforeOperationHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>;
874
+ afterOperation?: (args: AfterOperationHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>;
875
+ /**
876
+ * @deprecated Use 'validate' instead. This alias is provided for backwards compatibility.
877
+ */
878
+ validateInput?: (args: ValidateHookArgs<TOutput, TCreateInput, TUpdateInput>) => Promise<void>;
678
879
  };
880
+ /**
881
+ * Internal list configuration type (after normalization by list() function)
882
+ * Access control is always in object form internally.
883
+ * Use list() function which accepts both function shorthand and object form.
884
+ */
679
885
  export type ListConfig<TTypeInfo extends TypeInfo> = {
680
886
  fields: FieldsWithTypeInfo<TTypeInfo>;
887
+ /**
888
+ * Access control configuration for this list (normalized object form).
889
+ * The list() function normalizes function shorthand to this object form.
890
+ */
681
891
  access?: {
682
892
  operation?: OperationAccess<TTypeInfo['item']>;
683
893
  };
@@ -686,6 +896,55 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
686
896
  * MCP server configuration for this list
687
897
  */
688
898
  mcp?: ListMcpConfig;
899
+ /**
900
+ * Restricts this list to a single record (singleton pattern)
901
+ * When true:
902
+ * - Prevents creating multiple records
903
+ * - Auto-creates the single record on first access (if autoCreate: true, which is the default)
904
+ * - Provides a get() method for easy access to the singleton
905
+ * - Blocks delete and findMany operations
906
+ * - Changes UI to show edit form instead of list view
907
+ *
908
+ * @example Simple boolean (auto-create enabled)
909
+ * ```typescript
910
+ * isSingleton: true
911
+ * ```
912
+ *
913
+ * @example With options
914
+ * ```typescript
915
+ * isSingleton: {
916
+ * autoCreate: false // Don't auto-create, must be created manually
917
+ * }
918
+ * ```
919
+ */
920
+ isSingleton?: boolean | {
921
+ /**
922
+ * Auto-create the singleton record on first access using field defaults
923
+ * @default true
924
+ */
925
+ autoCreate?: boolean;
926
+ };
927
+ };
928
+ /**
929
+ * Input type for the list() function
930
+ * Accepts both function shorthand and object form for access control.
931
+ */
932
+ export type ListConfigInput<TTypeInfo extends TypeInfo> = Omit<ListConfig<TTypeInfo>, 'access'> & {
933
+ /**
934
+ * Access control configuration for this list.
935
+ * Supports both function shorthand and object form.
936
+ *
937
+ * @example Function shorthand (applies to all operations)
938
+ * ```typescript
939
+ * access: isAdmin
940
+ * ```
941
+ *
942
+ * @example Object form (per-operation)
943
+ * ```typescript
944
+ * access: { operation: { query: () => true, create: isAdmin } }
945
+ * ```
946
+ */
947
+ access?: ListAccessControl<TTypeInfo['item']>;
689
948
  };
690
949
  /**
691
950
  * Database configuration