@opensaas/stack-core 0.12.1 → 0.13.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.
@@ -9,7 +9,7 @@ import {
9
9
  } from '../access/index.js'
10
10
  import {
11
11
  executeResolveInput,
12
- executeValidateInput,
12
+ executeValidate,
13
13
  executeBeforeOperation,
14
14
  executeAfterOperation,
15
15
  validateFieldRules,
@@ -27,7 +27,9 @@ import type { FieldConfig } from '../config/types.js'
27
27
  */
28
28
  async function executeFieldResolveInputHooks(
29
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- data: Record<string, any>,
30
+ inputData: Record<string, any>,
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ resolvedData: Record<string, any>,
31
33
  fields: Record<string, FieldConfig>,
32
34
  operation: 'create' | 'update',
33
35
  context: AccessContext,
@@ -35,11 +37,11 @@ async function executeFieldResolveInputHooks(
35
37
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
38
  item?: any,
37
39
  ): Promise<Record<string, unknown>> {
38
- const result = { ...data }
40
+ let result = { ...resolvedData }
39
41
 
40
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
42
+ for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
41
43
  // Skip if field not in data
42
- if (!(fieldName in result)) continue
44
+ if (!(fieldKey in result)) continue
43
45
 
44
46
  // Skip if no hooks defined
45
47
  if (!fieldConfig.hooks?.resolveInput) continue
@@ -49,27 +51,101 @@ async function executeFieldResolveInputHooks(
49
51
  // and we're working with runtime values that match those types
50
52
 
51
53
  const transformedValue = await fieldConfig.hooks.resolveInput({
52
- inputValue: result[fieldName],
53
- operation,
54
- fieldName,
55
54
  listKey,
55
+ fieldKey,
56
+ operation,
57
+ inputData,
56
58
  item,
59
+ resolvedData: { ...result }, // Pass a copy to avoid mutation affecting recorded args
57
60
  context,
58
- })
61
+ } as Parameters<typeof fieldConfig.hooks.resolveInput>[0])
59
62
 
60
- result[fieldName] = transformedValue
63
+ // Create new object with updated field to avoid mutating the passed reference
64
+ result = { ...result, [fieldKey]: transformedValue }
61
65
  }
62
66
 
63
67
  return result
64
68
  }
65
69
 
70
+ /**
71
+ * Execute field-level validate hooks
72
+ * Allows fields to perform custom validation after resolveInput but before database write
73
+ */
74
+ async function executeFieldValidateHooks(
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ inputData: Record<string, any> | undefined,
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ resolvedData: Record<string, any> | undefined,
79
+ fields: Record<string, FieldConfig>,
80
+ operation: 'create' | 'update' | 'delete',
81
+ context: AccessContext,
82
+ listKey: string,
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ item?: any,
85
+ ): Promise<void> {
86
+ const errors: string[] = []
87
+ const fieldErrors: Record<string, string> = {}
88
+
89
+ const addValidationError = (fieldKey: string) => (msg: string) => {
90
+ errors.push(msg)
91
+ fieldErrors[fieldKey] = msg
92
+ }
93
+
94
+ for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
95
+ // Skip if no hooks defined
96
+ if (!fieldConfig.hooks?.validate) continue
97
+
98
+ // Execute field hook
99
+ // Type assertion is safe here because hooks are typed correctly in field definitions
100
+ if (operation === 'delete') {
101
+ await fieldConfig.hooks.validate({
102
+ listKey,
103
+ fieldKey,
104
+ operation: 'delete',
105
+ item,
106
+ context,
107
+ addValidationError: addValidationError(fieldKey),
108
+ } as Parameters<typeof fieldConfig.hooks.validate>[0])
109
+ } else if (operation === 'create') {
110
+ await fieldConfig.hooks.validate({
111
+ listKey,
112
+ fieldKey,
113
+ operation: 'create',
114
+ inputData,
115
+ item: undefined,
116
+ resolvedData,
117
+ context,
118
+ addValidationError: addValidationError(fieldKey),
119
+ } as Parameters<typeof fieldConfig.hooks.validate>[0])
120
+ } else {
121
+ // operation === 'update'
122
+ await fieldConfig.hooks.validate({
123
+ listKey,
124
+ fieldKey,
125
+ operation: 'update',
126
+ inputData,
127
+ item,
128
+ resolvedData,
129
+ context,
130
+ addValidationError: addValidationError(fieldKey),
131
+ } as Parameters<typeof fieldConfig.hooks.validate>[0])
132
+ }
133
+ }
134
+
135
+ if (errors.length > 0) {
136
+ throw new ValidationError(errors, fieldErrors)
137
+ }
138
+ }
139
+
66
140
  /**
67
141
  * Execute field-level beforeOperation hooks (side effects only)
68
142
  * Allows fields to perform side effects before database write
69
143
  */
70
144
  async function executeFieldBeforeOperationHooks(
71
145
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
- data: Record<string, any>,
146
+ inputData: Record<string, any>,
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ resolvedData: Record<string, any>,
73
149
  fields: Record<string, FieldConfig>,
74
150
  operation: 'create' | 'update' | 'delete',
75
151
  context: AccessContext,
@@ -77,21 +153,43 @@ async function executeFieldBeforeOperationHooks(
77
153
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
154
  item?: any,
79
155
  ): Promise<void> {
80
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
81
- // Skip if field not in data (for create/update) or if no hooks defined
156
+ for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
157
+ // Skip if no hooks defined
82
158
  if (!fieldConfig.hooks?.beforeOperation) continue
83
- if (operation !== 'delete' && !(fieldName in data)) continue
159
+ // Skip if field not in data (for create/update)
160
+ if (operation !== 'delete' && !(fieldKey in resolvedData)) continue
84
161
 
85
162
  // Execute field hook (side effects only, no return value used)
86
163
  // Type assertion is safe here because hooks are typed correctly in field definitions
87
- await fieldConfig.hooks.beforeOperation({
88
- resolvedValue: data[fieldName],
89
- operation,
90
- fieldName,
91
- listKey,
92
- item,
93
- context,
94
- })
164
+ if (operation === 'delete') {
165
+ await fieldConfig.hooks.beforeOperation({
166
+ listKey,
167
+ fieldKey,
168
+ operation: 'delete',
169
+ item,
170
+ context,
171
+ } as Parameters<typeof fieldConfig.hooks.beforeOperation>[0])
172
+ } else if (operation === 'create') {
173
+ await fieldConfig.hooks.beforeOperation({
174
+ listKey,
175
+ fieldKey,
176
+ operation: 'create',
177
+ inputData,
178
+ resolvedData,
179
+ context,
180
+ } as Parameters<typeof fieldConfig.hooks.beforeOperation>[0])
181
+ } else {
182
+ // operation === 'update'
183
+ await fieldConfig.hooks.beforeOperation({
184
+ listKey,
185
+ fieldKey,
186
+ operation: 'update',
187
+ inputData,
188
+ item,
189
+ resolvedData,
190
+ context,
191
+ } as Parameters<typeof fieldConfig.hooks.beforeOperation>[0])
192
+ }
95
193
  }
96
194
  }
97
195
 
@@ -102,31 +200,51 @@ async function executeFieldBeforeOperationHooks(
102
200
  async function executeFieldAfterOperationHooks(
103
201
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
202
  item: any,
105
- data: Record<string, unknown> | undefined,
203
+ inputData: Record<string, unknown> | undefined,
204
+ resolvedData: Record<string, unknown> | undefined,
106
205
  fields: Record<string, FieldConfig>,
107
- operation: 'create' | 'update' | 'delete' | 'query',
206
+ operation: 'create' | 'update' | 'delete',
108
207
  context: AccessContext,
109
208
  listKey: string,
110
209
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
210
  originalItem?: any,
112
211
  ): Promise<void> {
113
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
212
+ for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
114
213
  // Skip if no hooks defined
115
214
  if (!fieldConfig.hooks?.afterOperation) continue
116
215
 
117
- // Get the value from item (for all operations)
118
- const value = item?.[fieldName]
119
-
120
216
  // Execute field hook (side effects only, no return value used)
121
- await fieldConfig.hooks.afterOperation({
122
- value,
123
- operation,
124
- fieldName,
125
- listKey,
126
- item,
127
- originalItem,
128
- context,
129
- })
217
+ if (operation === 'delete') {
218
+ await fieldConfig.hooks.afterOperation({
219
+ listKey,
220
+ fieldKey,
221
+ operation: 'delete',
222
+ originalItem,
223
+ context,
224
+ } as Parameters<typeof fieldConfig.hooks.afterOperation>[0])
225
+ } else if (operation === 'create') {
226
+ await fieldConfig.hooks.afterOperation({
227
+ listKey,
228
+ fieldKey,
229
+ operation: 'create',
230
+ inputData,
231
+ item,
232
+ resolvedData,
233
+ context,
234
+ } as Parameters<typeof fieldConfig.hooks.afterOperation>[0])
235
+ } else {
236
+ // operation === 'update'
237
+ await fieldConfig.hooks.afterOperation({
238
+ listKey,
239
+ fieldKey,
240
+ operation: 'update',
241
+ inputData,
242
+ originalItem,
243
+ item,
244
+ resolvedData,
245
+ context,
246
+ } as Parameters<typeof fieldConfig.hooks.afterOperation>[0])
247
+ }
130
248
  }
131
249
  }
132
250
 
@@ -478,17 +596,6 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
478
596
  listName,
479
597
  )
480
598
 
481
- // Execute field afterOperation hooks (side effects only)
482
- await executeFieldAfterOperationHooks(
483
- filtered,
484
- undefined,
485
- listConfig.fields,
486
- 'query',
487
- context,
488
- listName,
489
- undefined, // originalItem is undefined for query operations
490
- )
491
-
492
599
  return filtered
493
600
  }
494
601
  }
@@ -573,21 +680,6 @@ function createFindMany<TPrisma extends PrismaClientLike>(
573
680
  ),
574
681
  )
575
682
 
576
- // Execute field afterOperation hooks for each item (side effects only)
577
- await Promise.all(
578
- filtered.map((item) =>
579
- executeFieldAfterOperationHooks(
580
- item,
581
- undefined,
582
- listConfig.fields,
583
- 'query',
584
- context,
585
- listName,
586
- undefined, // originalItem is undefined for query operations
587
- ),
588
- ),
589
- )
590
-
591
683
  return filtered
592
684
  }
593
685
  }
@@ -619,7 +711,9 @@ function createCreate<TPrisma extends PrismaClientLike>(
619
711
 
620
712
  // 2. Execute list-level resolveInput hook
621
713
  let resolvedData = await executeResolveInput(listConfig.hooks, {
714
+ listKey: listName,
622
715
  operation: 'create',
716
+ inputData: args.data,
623
717
  resolvedData: args.data,
624
718
  item: undefined,
625
719
  context,
@@ -627,6 +721,7 @@ function createCreate<TPrisma extends PrismaClientLike>(
627
721
 
628
722
  // 2.5. Execute field-level resolveInput hooks (e.g., hash passwords)
629
723
  resolvedData = await executeFieldResolveInputHooks(
724
+ args.data,
630
725
  resolvedData,
631
726
  listConfig.fields,
632
727
  'create',
@@ -634,14 +729,26 @@ function createCreate<TPrisma extends PrismaClientLike>(
634
729
  listName,
635
730
  )
636
731
 
637
- // 3. Execute validateInput hook
638
- await executeValidateInput(listConfig.hooks, {
732
+ // 3. Execute list-level validate hook
733
+ await executeValidate(listConfig.hooks, {
734
+ listKey: listName,
639
735
  operation: 'create',
736
+ inputData: args.data,
640
737
  resolvedData,
641
738
  item: undefined,
642
739
  context,
643
740
  })
644
741
 
742
+ // 3.5. Execute field-level validate hooks
743
+ await executeFieldValidateHooks(
744
+ args.data,
745
+ resolvedData,
746
+ listConfig.fields,
747
+ 'create',
748
+ context,
749
+ listName,
750
+ )
751
+
645
752
  // 4. Field validation (isRequired, length, etc.)
646
753
  const validation = validateFieldRules(resolvedData, listConfig.fields, 'create')
647
754
  if (validation.errors.length > 0) {
@@ -664,11 +771,21 @@ function createCreate<TPrisma extends PrismaClientLike>(
664
771
  )
665
772
 
666
773
  // 6. Execute field-level beforeOperation hooks (side effects only)
667
- await executeFieldBeforeOperationHooks(data, listConfig.fields, 'create', context, listName)
774
+ await executeFieldBeforeOperationHooks(
775
+ args.data,
776
+ resolvedData,
777
+ listConfig.fields,
778
+ 'create',
779
+ context,
780
+ listName,
781
+ )
668
782
 
669
783
  // 7. Execute list-level beforeOperation hook
670
784
  await executeBeforeOperation(listConfig.hooks, {
785
+ listKey: listName,
671
786
  operation: 'create',
787
+ inputData: args.data,
788
+ resolvedData,
672
789
  context,
673
790
  })
674
791
 
@@ -682,16 +799,19 @@ function createCreate<TPrisma extends PrismaClientLike>(
682
799
 
683
800
  // 9. Execute list-level afterOperation hook
684
801
  await executeAfterOperation(listConfig.hooks, {
802
+ listKey: listName,
685
803
  operation: 'create',
804
+ inputData: args.data,
686
805
  item,
687
- originalItem: undefined,
806
+ resolvedData,
688
807
  context,
689
808
  })
690
809
 
691
810
  // 10. Execute field-level afterOperation hooks (side effects only)
692
811
  await executeFieldAfterOperationHooks(
693
812
  item,
694
- data,
813
+ args.data,
814
+ resolvedData,
695
815
  listConfig.fields,
696
816
  'create',
697
817
  context,
@@ -768,7 +888,9 @@ function createUpdate<TPrisma extends PrismaClientLike>(
768
888
 
769
889
  // 3. Execute list-level resolveInput hook
770
890
  let resolvedData = await executeResolveInput(listConfig.hooks, {
891
+ listKey: listName,
771
892
  operation: 'update',
893
+ inputData: args.data,
772
894
  resolvedData: args.data,
773
895
  item,
774
896
  context,
@@ -776,6 +898,7 @@ function createUpdate<TPrisma extends PrismaClientLike>(
776
898
 
777
899
  // 3.5. Execute field-level resolveInput hooks (e.g., hash passwords)
778
900
  resolvedData = await executeFieldResolveInputHooks(
901
+ args.data,
779
902
  resolvedData,
780
903
  listConfig.fields,
781
904
  'update',
@@ -784,14 +907,27 @@ function createUpdate<TPrisma extends PrismaClientLike>(
784
907
  item,
785
908
  )
786
909
 
787
- // 4. Execute validateInput hook
788
- await executeValidateInput(listConfig.hooks, {
910
+ // 4. Execute list-level validate hook
911
+ await executeValidate(listConfig.hooks, {
912
+ listKey: listName,
789
913
  operation: 'update',
914
+ inputData: args.data,
790
915
  resolvedData,
791
916
  item,
792
917
  context,
793
918
  })
794
919
 
920
+ // 4.5. Execute field-level validate hooks
921
+ await executeFieldValidateHooks(
922
+ args.data,
923
+ resolvedData,
924
+ listConfig.fields,
925
+ 'update',
926
+ context,
927
+ listName,
928
+ item,
929
+ )
930
+
795
931
  // 5. Field validation (isRequired, length, etc.)
796
932
  const validation = validateFieldRules(resolvedData, listConfig.fields, 'update')
797
933
  if (validation.errors.length > 0) {
@@ -816,7 +952,8 @@ function createUpdate<TPrisma extends PrismaClientLike>(
816
952
 
817
953
  // 7. Execute field-level beforeOperation hooks (side effects only)
818
954
  await executeFieldBeforeOperationHooks(
819
- data,
955
+ args.data,
956
+ resolvedData,
820
957
  listConfig.fields,
821
958
  'update',
822
959
  context,
@@ -826,8 +963,11 @@ function createUpdate<TPrisma extends PrismaClientLike>(
826
963
 
827
964
  // 8. Execute list-level beforeOperation hook
828
965
  await executeBeforeOperation(listConfig.hooks, {
966
+ listKey: listName,
829
967
  operation: 'update',
968
+ inputData: args.data,
830
969
  item,
970
+ resolvedData,
831
971
  context,
832
972
  })
833
973
 
@@ -839,16 +979,20 @@ function createUpdate<TPrisma extends PrismaClientLike>(
839
979
 
840
980
  // 10. Execute list-level afterOperation hook
841
981
  await executeAfterOperation(listConfig.hooks, {
982
+ listKey: listName,
842
983
  operation: 'update',
843
- item: updated,
984
+ inputData: args.data,
844
985
  originalItem: item, // item is the original item before the update
986
+ item: updated,
987
+ resolvedData,
845
988
  context,
846
989
  })
847
990
 
848
991
  // 11. Execute field-level afterOperation hooks (side effects only)
849
992
  await executeFieldAfterOperationHooks(
850
993
  updated,
851
- data,
994
+ args.data,
995
+ resolvedData,
852
996
  listConfig.fields,
853
997
  'update',
854
998
  context,
@@ -922,33 +1066,62 @@ function createDelete<TPrisma extends PrismaClientLike>(
922
1066
  }
923
1067
  }
924
1068
 
925
- // 3. Execute field-level beforeOperation hooks (side effects only)
926
- await executeFieldBeforeOperationHooks({}, listConfig.fields, 'delete', context, listName, item)
1069
+ // 3. Execute list-level validate hook
1070
+ await executeValidate(listConfig.hooks, {
1071
+ listKey: listName,
1072
+ operation: 'delete',
1073
+ item,
1074
+ context,
1075
+ })
1076
+
1077
+ // 3.5. Execute field-level validate hooks
1078
+ await executeFieldValidateHooks(
1079
+ undefined,
1080
+ undefined,
1081
+ listConfig.fields,
1082
+ 'delete',
1083
+ context,
1084
+ listName,
1085
+ item,
1086
+ )
1087
+
1088
+ // 4. Execute field-level beforeOperation hooks (side effects only)
1089
+ await executeFieldBeforeOperationHooks(
1090
+ {},
1091
+ {},
1092
+ listConfig.fields,
1093
+ 'delete',
1094
+ context,
1095
+ listName,
1096
+ item,
1097
+ )
927
1098
 
928
- // 4. Execute list-level beforeOperation hook
1099
+ // 5. Execute list-level beforeOperation hook
929
1100
  await executeBeforeOperation(listConfig.hooks, {
1101
+ listKey: listName,
930
1102
  operation: 'delete',
931
1103
  item,
932
1104
  context,
933
1105
  })
934
1106
 
935
- // 5. Execute database delete
1107
+ // 6. Execute database delete
936
1108
  const deleted = await model.delete({
937
1109
  where: args.where,
938
1110
  })
939
1111
 
940
- // 6. Execute list-level afterOperation hook
1112
+ // 7. Execute list-level afterOperation hook
941
1113
  await executeAfterOperation(listConfig.hooks, {
1114
+ listKey: listName,
942
1115
  operation: 'delete',
943
- item: deleted,
944
1116
  originalItem: item, // item is the original item before deletion
945
1117
  context,
946
1118
  })
947
1119
 
948
- // 7. Execute field-level afterOperation hooks (side effects only)
1120
+ // 8. Execute field-level afterOperation hooks (side effects only)
949
1121
  await executeFieldAfterOperationHooks(
950
1122
  deleted,
951
1123
  undefined,
1124
+ undefined,
952
1125
  listConfig.fields,
953
1126
  'delete',
954
1127
  context,
@@ -3,7 +3,7 @@ import type { AccessContext } from '../access/types.js'
3
3
  import { checkAccess, filterWritableFields, getRelatedListConfig } from '../access/index.js'
4
4
  import {
5
5
  executeResolveInput,
6
- executeValidateInput,
6
+ executeValidate,
7
7
  validateFieldRules,
8
8
  ValidationError,
9
9
  } from '../hooks/index.js'
@@ -15,7 +15,9 @@ import { getDbKey } from '../lib/case-utils.js'
15
15
  */
16
16
  async function executeFieldResolveInputHooks(
17
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- data: Record<string, any>,
18
+ inputData: Record<string, any>,
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ resolvedData: Record<string, any>,
19
21
  fields: Record<string, FieldConfig>,
20
22
  operation: 'create' | 'update',
21
23
  context: AccessContext,
@@ -23,26 +25,28 @@ async function executeFieldResolveInputHooks(
23
25
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
26
  item?: any,
25
27
  ): Promise<Record<string, unknown>> {
26
- const result = { ...data }
28
+ let result = { ...resolvedData }
27
29
 
28
- for (const [fieldName, fieldConfig] of Object.entries(fields)) {
30
+ for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
29
31
  // Skip if field not in data
30
- if (!(fieldName in result)) continue
32
+ if (!(fieldKey in result)) continue
31
33
 
32
34
  // Skip if no hooks defined
33
35
  if (!fieldConfig.hooks?.resolveInput) continue
34
36
 
35
37
  // Execute field hook
36
38
  const transformedValue = await fieldConfig.hooks.resolveInput({
37
- inputValue: result[fieldName],
38
- operation,
39
- fieldName,
40
39
  listKey,
40
+ fieldKey,
41
+ operation,
42
+ inputData,
41
43
  item,
44
+ resolvedData: { ...result }, // Pass a copy to avoid mutation affecting recorded args
42
45
  context,
43
- })
46
+ } as Parameters<typeof fieldConfig.hooks.resolveInput>[0])
44
47
 
45
- result[fieldName] = transformedValue
48
+ // Create new object with updated field to avoid mutating the passed reference
49
+ result = { ...result, [fieldKey]: transformedValue }
46
50
  }
47
51
 
48
52
  return result
@@ -83,17 +87,7 @@ async function processNestedCreate(
83
87
  }
84
88
  }
85
89
 
86
- // 2. Execute list-level resolveInput hook
87
- let resolvedData = await executeResolveInput(relatedListConfig.hooks, {
88
- operation: 'create',
89
- resolvedData: item,
90
- item: undefined,
91
- context,
92
- })
93
-
94
- // 2.5. Execute field-level resolveInput hooks
95
- // We need to get the list name for this related config
96
- // Since we don't have it directly, we'll need to find it from the config
90
+ // 2. Get the list name for this related config
97
91
  let relatedListName = ''
98
92
  for (const [listKey, listCfg] of Object.entries(config.lists)) {
99
93
  if (listCfg === relatedListConfig) {
@@ -102,7 +96,19 @@ async function processNestedCreate(
102
96
  }
103
97
  }
104
98
 
99
+ // 3. Execute list-level resolveInput hook
100
+ let resolvedData = await executeResolveInput(relatedListConfig.hooks, {
101
+ listKey: relatedListName,
102
+ operation: 'create',
103
+ inputData: item,
104
+ resolvedData: item,
105
+ item: undefined,
106
+ context,
107
+ })
108
+
109
+ // 4. Execute field-level resolveInput hooks
105
110
  resolvedData = await executeFieldResolveInputHooks(
111
+ item,
106
112
  resolvedData,
107
113
  relatedListConfig.fields,
108
114
  'create',
@@ -110,9 +116,11 @@ async function processNestedCreate(
110
116
  relatedListName,
111
117
  )
112
118
 
113
- // 3. Execute validateInput hook
114
- await executeValidateInput(relatedListConfig.hooks, {
119
+ // 5. Execute validate hook
120
+ await executeValidate(relatedListConfig.hooks, {
121
+ listKey: relatedListName,
115
122
  operation: 'create',
123
+ inputData: item,
116
124
  resolvedData,
117
125
  item: undefined,
118
126
  context,
@@ -257,7 +265,9 @@ async function processNestedUpdate(
257
265
  // Execute list-level resolveInput hook
258
266
  const updateData = (update as Record<string, unknown>).data as Record<string, unknown>
259
267
  let resolvedData = await executeResolveInput(relatedListConfig.hooks, {
268
+ listKey: relatedListName,
260
269
  operation: 'update',
270
+ inputData: updateData,
261
271
  resolvedData: updateData,
262
272
  item,
263
273
  context,
@@ -265,6 +275,7 @@ async function processNestedUpdate(
265
275
 
266
276
  // Execute field-level resolveInput hooks
267
277
  resolvedData = await executeFieldResolveInputHooks(
278
+ updateData,
268
279
  resolvedData,
269
280
  relatedListConfig.fields,
270
281
  'update',
@@ -273,9 +284,11 @@ async function processNestedUpdate(
273
284
  item,
274
285
  )
275
286
 
276
- // Execute validateInput hook
277
- await executeValidateInput(relatedListConfig.hooks, {
287
+ // Execute validate hook
288
+ await executeValidate(relatedListConfig.hooks, {
289
+ listKey: relatedListName,
278
290
  operation: 'update',
291
+ inputData: updateData,
279
292
  resolvedData,
280
293
  item,
281
294
  context,