@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +86 -0
- package/README.md +6 -3
- package/dist/config/types.d.ts +147 -47
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +202 -58
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +36 -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/package.json +1 -1
- package/src/config/types.ts +170 -49
- package/src/context/index.ts +253 -80
- package/src/context/nested-operations.ts +38 -25
- package/src/hooks/index.ts +66 -14
- package/tests/nested-access-and-hooks.test.ts +8 -3
- package/tests/sudo.test.ts +2 -13
- package/tsconfig.tsbuildinfo +1 -1
package/src/context/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '../access/index.js'
|
|
10
10
|
import {
|
|
11
11
|
executeResolveInput,
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
let result = { ...resolvedData }
|
|
39
41
|
|
|
40
|
-
for (const [
|
|
42
|
+
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
41
43
|
// Skip if field not in data
|
|
42
|
-
if (!(
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
81
|
-
// Skip if
|
|
156
|
+
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
157
|
+
// Skip if no hooks defined
|
|
82
158
|
if (!fieldConfig.hooks?.beforeOperation) continue
|
|
83
|
-
if
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
203
|
+
inputData: Record<string, unknown> | undefined,
|
|
204
|
+
resolvedData: Record<string, unknown> | undefined,
|
|
106
205
|
fields: Record<string, FieldConfig>,
|
|
107
|
-
operation: 'create' | 'update' | 'delete'
|
|
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 [
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
638
|
-
await
|
|
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(
|
|
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
|
-
|
|
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
|
|
788
|
-
await
|
|
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
|
-
|
|
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
|
|
926
|
-
await
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1107
|
+
// 6. Execute database delete
|
|
936
1108
|
const deleted = await model.delete({
|
|
937
1109
|
where: args.where,
|
|
938
1110
|
})
|
|
939
1111
|
|
|
940
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
let result = { ...resolvedData }
|
|
27
29
|
|
|
28
|
-
for (const [
|
|
30
|
+
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
29
31
|
// Skip if field not in data
|
|
30
|
-
if (!(
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
114
|
-
await
|
|
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
|
|
277
|
-
await
|
|
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,
|