@opensaas/stack-core 0.9.0 → 0.11.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 +174 -0
- package/dist/config/types.d.ts +165 -20
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +14 -6
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +2 -0
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/fields/index.d.ts +45 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +215 -12
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/index.d.ts +24 -12
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config/types.ts +207 -43
- package/src/context/index.ts +13 -0
- package/src/context/nested-operations.ts +2 -0
- package/src/fields/index.ts +257 -12
- package/src/hooks/index.ts +43 -35
- package/tests/hooks.test.ts +64 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/hooks/index.d.ts
CHANGED
|
@@ -25,12 +25,12 @@ export declare class DatabaseError extends Error {
|
|
|
25
25
|
export declare function executeResolveInput<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>>(hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined, args: {
|
|
26
26
|
operation: 'create';
|
|
27
27
|
resolvedData: TCreateInput;
|
|
28
|
-
item
|
|
28
|
+
item: undefined;
|
|
29
29
|
context: AccessContext;
|
|
30
30
|
} | {
|
|
31
31
|
operation: 'update';
|
|
32
32
|
resolvedData: TUpdateInput;
|
|
33
|
-
item
|
|
33
|
+
item: TOutput;
|
|
34
34
|
context: AccessContext;
|
|
35
35
|
}): Promise<TCreateInput | TUpdateInput>;
|
|
36
36
|
/**
|
|
@@ -38,29 +38,41 @@ export declare function executeResolveInput<TOutput = Record<string, unknown>, T
|
|
|
38
38
|
* Allows custom validation logic
|
|
39
39
|
*/
|
|
40
40
|
export declare function executeValidateInput<TOutput = Record<string, unknown>, TCreateInput = Record<string, unknown>, TUpdateInput = Record<string, unknown>>(hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined, args: {
|
|
41
|
-
operation: 'create'
|
|
42
|
-
resolvedData: TCreateInput
|
|
43
|
-
item
|
|
41
|
+
operation: 'create';
|
|
42
|
+
resolvedData: TCreateInput;
|
|
43
|
+
item: undefined;
|
|
44
|
+
context: AccessContext;
|
|
45
|
+
} | {
|
|
46
|
+
operation: 'update';
|
|
47
|
+
resolvedData: TUpdateInput;
|
|
48
|
+
item: TOutput;
|
|
44
49
|
context: AccessContext;
|
|
45
50
|
}): Promise<void>;
|
|
46
51
|
/**
|
|
47
52
|
* Execute beforeOperation hook
|
|
48
53
|
* Runs before database operation (cannot modify data)
|
|
49
54
|
*/
|
|
50
|
-
export declare function executeBeforeOperation<TOutput = Record<string, unknown
|
|
51
|
-
operation: 'create'
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
export declare function executeBeforeOperation<TOutput = Record<string, unknown>>(hooks: Hooks<TOutput> | undefined, args: {
|
|
56
|
+
operation: 'create';
|
|
57
|
+
context: AccessContext;
|
|
58
|
+
} | {
|
|
59
|
+
operation: 'update' | 'delete';
|
|
60
|
+
item: TOutput;
|
|
54
61
|
context: AccessContext;
|
|
55
62
|
}): Promise<void>;
|
|
56
63
|
/**
|
|
57
64
|
* Execute afterOperation hook
|
|
58
65
|
* Runs after database operation
|
|
59
66
|
*/
|
|
60
|
-
export declare function executeAfterOperation<TOutput = Record<string, unknown
|
|
61
|
-
operation: 'create'
|
|
62
|
-
|
|
67
|
+
export declare function executeAfterOperation<TOutput = Record<string, unknown>>(hooks: Hooks<TOutput> | undefined, args: {
|
|
68
|
+
operation: 'create';
|
|
69
|
+
item: TOutput;
|
|
70
|
+
originalItem: undefined;
|
|
71
|
+
context: AccessContext;
|
|
72
|
+
} | {
|
|
73
|
+
operation: 'update' | 'delete';
|
|
63
74
|
item: TOutput;
|
|
75
|
+
originalItem: TOutput;
|
|
64
76
|
context: AccessContext;
|
|
65
77
|
}): Promise<void>;
|
|
66
78
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGrD;;GAEG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACjC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;gBAE9B,MAAM,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM;CAMvE;AAED;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;gBAER,OAAO,EAAE,MAAM,EAAE,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAMrF;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEtC,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,YAAY,CAAC,GAAG,SAAS,EAC7D,IAAI,EACA;IACE,SAAS,EAAE,QAAQ,CAAA;IACnB,YAAY,EAAE,YAAY,CAAA;IAC1B,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -34,8 +34,6 @@ export async function executeResolveInput(hooks, args) {
|
|
|
34
34
|
if (!hooks?.resolveInput) {
|
|
35
35
|
return args.resolvedData;
|
|
36
36
|
}
|
|
37
|
-
// Type assertion is safe because we've constrained the args type
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
37
|
const result = await hooks.resolveInput(args);
|
|
40
38
|
return result;
|
|
41
39
|
}
|
package/dist/hooks/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAEzD;;GAEG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACjC,MAAM,CAAU;IAChB,WAAW,CAAwB;IAE1C,YAAY,MAAgB,EAAE,cAAsC,EAAE;QACpE,KAAK,CAAC,sBAAsB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAChD,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;QAC7B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;IAChC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC/B,WAAW,CAAwB;IACnC,IAAI,CAAS;IAEpB,YAAY,OAAe,EAAE,cAAsC,EAAE,EAAE,IAAa;QAClF,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,eAAe,CAAA;QAC3B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAKvC,KAA6D,EAC7D,IAYK;IAEL,IAAI,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,YAAY,CAAA;IAC1B,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -43,7 +43,9 @@ export type FieldHooks<
|
|
|
43
43
|
*
|
|
44
44
|
* @example
|
|
45
45
|
* ```typescript
|
|
46
|
-
* resolveInput: async ({ inputValue, operation }) => {
|
|
46
|
+
* resolveInput: async ({ inputValue, operation, item }) => {
|
|
47
|
+
* // For create operations, item is undefined
|
|
48
|
+
* // For update operations, item is the existing record
|
|
47
49
|
* if (typeof inputValue === 'string' && !isHashedPassword(inputValue)) {
|
|
48
50
|
* return await hashPassword(inputValue)
|
|
49
51
|
* }
|
|
@@ -51,14 +53,25 @@ export type FieldHooks<
|
|
|
51
53
|
* }
|
|
52
54
|
* ```
|
|
53
55
|
*/
|
|
54
|
-
resolveInput?: (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
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
|
+
},
|
|
74
|
+
) =>
|
|
62
75
|
| Promise<GetFieldValueType<TTypeInfo['fields'], TFieldKey> | undefined>
|
|
63
76
|
| GetFieldValueType<TTypeInfo['fields'], TFieldKey>
|
|
64
77
|
| undefined
|
|
@@ -71,19 +84,34 @@ export type FieldHooks<
|
|
|
71
84
|
* @example
|
|
72
85
|
* ```typescript
|
|
73
86
|
* beforeOperation: async ({ resolvedValue, operation, item }) => {
|
|
74
|
-
*
|
|
87
|
+
* // For create operations, item is undefined
|
|
88
|
+
* // For update/delete operations, item is the existing record
|
|
89
|
+
* if (operation === 'update' && item) {
|
|
90
|
+
* console.log(`Updating field from ${item.fieldName} to ${resolvedValue}`)
|
|
91
|
+
* }
|
|
75
92
|
* await sendAuditLog({ operation, item })
|
|
76
93
|
* }
|
|
77
94
|
* ```
|
|
78
95
|
*/
|
|
79
|
-
beforeOperation?: (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
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
|
+
},
|
|
114
|
+
) => Promise<void> | void
|
|
87
115
|
|
|
88
116
|
/**
|
|
89
117
|
* Perform side effects after database operation
|
|
@@ -92,20 +120,38 @@ export type FieldHooks<
|
|
|
92
120
|
*
|
|
93
121
|
* @example
|
|
94
122
|
* ```typescript
|
|
95
|
-
* afterOperation: async ({ operation, value, item }) => {
|
|
123
|
+
* afterOperation: async ({ operation, value, item, originalItem }) => {
|
|
124
|
+
* // For query/create operations, originalItem is undefined
|
|
125
|
+
* // For update/delete operations, originalItem is the item before the operation
|
|
126
|
+
* if (operation === 'update' && originalItem) {
|
|
127
|
+
* console.log('Changed from:', originalItem[fieldName], 'to:', value)
|
|
128
|
+
* }
|
|
96
129
|
* await invalidateCache({ listKey, itemId: item.id })
|
|
97
130
|
* await sendWebhook({ operation, item })
|
|
98
131
|
* }
|
|
99
132
|
* ```
|
|
100
133
|
*/
|
|
101
|
-
afterOperation?: (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
109
155
|
|
|
110
156
|
/**
|
|
111
157
|
* Transform field value after database read
|
|
@@ -175,6 +221,23 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
|
175
221
|
* Transforms field values and types in query results using Prisma's native extension system
|
|
176
222
|
*/
|
|
177
223
|
resultExtension?: ResultExtensionConfig
|
|
224
|
+
/**
|
|
225
|
+
* Database configuration
|
|
226
|
+
*/
|
|
227
|
+
db?: {
|
|
228
|
+
/**
|
|
229
|
+
* Custom database column name
|
|
230
|
+
* Adds a @map attribute in Prisma schema
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* fields: {
|
|
234
|
+
* firstName: text({ db: { map: 'first_name' } })
|
|
235
|
+
* }
|
|
236
|
+
* // Generates: firstName String @map("first_name")
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
map?: string
|
|
240
|
+
}
|
|
178
241
|
ui?: {
|
|
179
242
|
/**
|
|
180
243
|
* Custom React component to render this field
|
|
@@ -275,6 +338,23 @@ export type IntegerField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfi
|
|
|
275
338
|
}
|
|
276
339
|
}
|
|
277
340
|
|
|
341
|
+
export type DecimalField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
342
|
+
type: 'decimal'
|
|
343
|
+
defaultValue?: string
|
|
344
|
+
precision?: number
|
|
345
|
+
scale?: number
|
|
346
|
+
db?: {
|
|
347
|
+
map?: string
|
|
348
|
+
isNullable?: boolean
|
|
349
|
+
}
|
|
350
|
+
validation?: {
|
|
351
|
+
isRequired?: boolean
|
|
352
|
+
min?: string
|
|
353
|
+
max?: string
|
|
354
|
+
}
|
|
355
|
+
isIndexed?: boolean | 'unique'
|
|
356
|
+
}
|
|
357
|
+
|
|
278
358
|
export type CheckboxField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
279
359
|
type: 'checkbox'
|
|
280
360
|
}
|
|
@@ -305,8 +385,45 @@ export type SelectField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig
|
|
|
305
385
|
export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
306
386
|
BaseFieldConfig<TTypeInfo> & {
|
|
307
387
|
type: 'relationship'
|
|
308
|
-
ref: string // Format: 'ListName.fieldName'
|
|
388
|
+
ref: string // Format: 'ListName.fieldName' or 'ListName'
|
|
309
389
|
many?: boolean
|
|
390
|
+
db?: {
|
|
391
|
+
/**
|
|
392
|
+
* Controls foreign key placement and column name for bidirectional relationships
|
|
393
|
+
* Can be a boolean or an object with a map property
|
|
394
|
+
* Only valid on single (non-many) relationships
|
|
395
|
+
* Cannot be true on both sides of a one-to-one relationship
|
|
396
|
+
*
|
|
397
|
+
* When a boolean, defaults the foreign key column name to the field name
|
|
398
|
+
* When an object with map, uses the provided column name
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```typescript
|
|
402
|
+
* // One-to-one: User has one Account (default foreign key name)
|
|
403
|
+
* User: list({
|
|
404
|
+
* fields: {
|
|
405
|
+
* account: relationship({ ref: 'Account.user', db: { foreignKey: true } })
|
|
406
|
+
* // Generates: accountId String? @unique
|
|
407
|
+
* }
|
|
408
|
+
* })
|
|
409
|
+
*
|
|
410
|
+
* // One-to-one: User has one Account (custom foreign key name)
|
|
411
|
+
* User: list({
|
|
412
|
+
* fields: {
|
|
413
|
+
* account: relationship({ ref: 'Account.user', db: { foreignKey: { map: 'account_id' } } })
|
|
414
|
+
* // Generates: accountId String? @unique @map("account_id")
|
|
415
|
+
* }
|
|
416
|
+
* })
|
|
417
|
+
*
|
|
418
|
+
* Account: list({
|
|
419
|
+
* fields: {
|
|
420
|
+
* user: relationship({ ref: 'User.account' }) // No foreign key on this side
|
|
421
|
+
* }
|
|
422
|
+
* })
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
foreignKey?: boolean | { map?: string }
|
|
426
|
+
}
|
|
310
427
|
ui?: {
|
|
311
428
|
displayMode?: 'select' | 'cards'
|
|
312
429
|
}
|
|
@@ -544,19 +661,69 @@ export type ResolveInputHookArgs<
|
|
|
544
661
|
}
|
|
545
662
|
|
|
546
663
|
/**
|
|
547
|
-
* Hook arguments for
|
|
548
|
-
*
|
|
664
|
+
* Hook arguments for validateInput hook
|
|
665
|
+
* Uses discriminated union to provide proper types based on operation
|
|
666
|
+
* - create: resolvedData is CreateInput, item is undefined
|
|
667
|
+
* - update: resolvedData is UpdateInput, item is the existing record
|
|
549
668
|
*/
|
|
550
|
-
export type
|
|
669
|
+
export type ValidateInputHookArgs<
|
|
551
670
|
TOutput = Record<string, unknown>,
|
|
552
671
|
TCreateInput = Record<string, unknown>,
|
|
553
672
|
TUpdateInput = Record<string, unknown>,
|
|
554
|
-
> =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
673
|
+
> =
|
|
674
|
+
| {
|
|
675
|
+
operation: 'create'
|
|
676
|
+
resolvedData: TCreateInput
|
|
677
|
+
item: undefined
|
|
678
|
+
context: import('../access/types.js').AccessContext
|
|
679
|
+
addValidationError: (msg: string) => void
|
|
680
|
+
}
|
|
681
|
+
| {
|
|
682
|
+
operation: 'update'
|
|
683
|
+
resolvedData: TUpdateInput
|
|
684
|
+
item: TOutput
|
|
685
|
+
context: import('../access/types.js').AccessContext
|
|
686
|
+
addValidationError: (msg: string) => void
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Hook arguments for beforeOperation hook
|
|
691
|
+
* Uses discriminated union to provide proper types based on operation
|
|
692
|
+
* - create: no resolvedData, no item
|
|
693
|
+
* - update: no resolvedData, has item
|
|
694
|
+
* - delete: no resolvedData, has item
|
|
695
|
+
*/
|
|
696
|
+
export type BeforeOperationHookArgs<TOutput = Record<string, unknown>> =
|
|
697
|
+
| {
|
|
698
|
+
operation: 'create'
|
|
699
|
+
context: import('../access/types.js').AccessContext
|
|
700
|
+
}
|
|
701
|
+
| {
|
|
702
|
+
operation: 'update' | 'delete'
|
|
703
|
+
item: TOutput
|
|
704
|
+
context: import('../access/types.js').AccessContext
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Hook arguments for afterOperation hook
|
|
709
|
+
* Uses discriminated union to provide proper types based on operation
|
|
710
|
+
* - create: has item, no originalItem
|
|
711
|
+
* - update: has item, has originalItem
|
|
712
|
+
* - delete: has item, has originalItem
|
|
713
|
+
*/
|
|
714
|
+
export type AfterOperationHookArgs<TOutput = Record<string, unknown>> =
|
|
715
|
+
| {
|
|
716
|
+
operation: 'create'
|
|
717
|
+
item: TOutput
|
|
718
|
+
originalItem: undefined
|
|
719
|
+
context: import('../access/types.js').AccessContext
|
|
720
|
+
}
|
|
721
|
+
| {
|
|
722
|
+
operation: 'update' | 'delete'
|
|
723
|
+
item: TOutput
|
|
724
|
+
originalItem: TOutput
|
|
725
|
+
context: import('../access/types.js').AccessContext
|
|
726
|
+
}
|
|
560
727
|
|
|
561
728
|
export type Hooks<
|
|
562
729
|
TOutput = Record<string, unknown>,
|
|
@@ -567,13 +734,10 @@ export type Hooks<
|
|
|
567
734
|
args: ResolveInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
|
|
568
735
|
) => Promise<TCreateInput | TUpdateInput>
|
|
569
736
|
validateInput?: (
|
|
570
|
-
args:
|
|
571
|
-
operation: 'create' | 'update'
|
|
572
|
-
addValidationError: (msg: string) => void
|
|
573
|
-
},
|
|
737
|
+
args: ValidateInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
|
|
574
738
|
) => Promise<void>
|
|
575
|
-
beforeOperation?: (args:
|
|
576
|
-
afterOperation?: (args:
|
|
739
|
+
beforeOperation?: (args: BeforeOperationHookArgs<TOutput>) => Promise<void>
|
|
740
|
+
afterOperation?: (args: AfterOperationHookArgs<TOutput>) => Promise<void>
|
|
577
741
|
}
|
|
578
742
|
|
|
579
743
|
// Generic `any` default allows ListConfig to work with any list item type
|
package/src/context/index.ts
CHANGED
|
@@ -107,6 +107,8 @@ async function executeFieldAfterOperationHooks(
|
|
|
107
107
|
operation: 'create' | 'update' | 'delete' | 'query',
|
|
108
108
|
context: AccessContext,
|
|
109
109
|
listKey: string,
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
originalItem?: any,
|
|
110
112
|
): Promise<void> {
|
|
111
113
|
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
112
114
|
// Skip if no hooks defined
|
|
@@ -122,6 +124,7 @@ async function executeFieldAfterOperationHooks(
|
|
|
122
124
|
fieldName,
|
|
123
125
|
listKey,
|
|
124
126
|
item,
|
|
127
|
+
originalItem,
|
|
125
128
|
context,
|
|
126
129
|
})
|
|
127
130
|
}
|
|
@@ -483,6 +486,7 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
|
483
486
|
'query',
|
|
484
487
|
context,
|
|
485
488
|
listName,
|
|
489
|
+
undefined, // originalItem is undefined for query operations
|
|
486
490
|
)
|
|
487
491
|
|
|
488
492
|
return filtered
|
|
@@ -579,6 +583,7 @@ function createFindMany<TPrisma extends PrismaClientLike>(
|
|
|
579
583
|
'query',
|
|
580
584
|
context,
|
|
581
585
|
listName,
|
|
586
|
+
undefined, // originalItem is undefined for query operations
|
|
582
587
|
),
|
|
583
588
|
),
|
|
584
589
|
)
|
|
@@ -616,6 +621,7 @@ function createCreate<TPrisma extends PrismaClientLike>(
|
|
|
616
621
|
let resolvedData = await executeResolveInput(listConfig.hooks, {
|
|
617
622
|
operation: 'create',
|
|
618
623
|
resolvedData: args.data,
|
|
624
|
+
item: undefined,
|
|
619
625
|
context,
|
|
620
626
|
})
|
|
621
627
|
|
|
@@ -632,6 +638,7 @@ function createCreate<TPrisma extends PrismaClientLike>(
|
|
|
632
638
|
await executeValidateInput(listConfig.hooks, {
|
|
633
639
|
operation: 'create',
|
|
634
640
|
resolvedData,
|
|
641
|
+
item: undefined,
|
|
635
642
|
context,
|
|
636
643
|
})
|
|
637
644
|
|
|
@@ -677,6 +684,7 @@ function createCreate<TPrisma extends PrismaClientLike>(
|
|
|
677
684
|
await executeAfterOperation(listConfig.hooks, {
|
|
678
685
|
operation: 'create',
|
|
679
686
|
item,
|
|
687
|
+
originalItem: undefined,
|
|
680
688
|
context,
|
|
681
689
|
})
|
|
682
690
|
|
|
@@ -688,6 +696,7 @@ function createCreate<TPrisma extends PrismaClientLike>(
|
|
|
688
696
|
'create',
|
|
689
697
|
context,
|
|
690
698
|
listName,
|
|
699
|
+
undefined, // originalItem is undefined for create operations
|
|
691
700
|
)
|
|
692
701
|
|
|
693
702
|
// 11. Filter readable fields and apply resolveOutput hooks (including nested relationships)
|
|
@@ -832,6 +841,7 @@ function createUpdate<TPrisma extends PrismaClientLike>(
|
|
|
832
841
|
await executeAfterOperation(listConfig.hooks, {
|
|
833
842
|
operation: 'update',
|
|
834
843
|
item: updated,
|
|
844
|
+
originalItem: item, // item is the original item before the update
|
|
835
845
|
context,
|
|
836
846
|
})
|
|
837
847
|
|
|
@@ -843,6 +853,7 @@ function createUpdate<TPrisma extends PrismaClientLike>(
|
|
|
843
853
|
'update',
|
|
844
854
|
context,
|
|
845
855
|
listName,
|
|
856
|
+
item, // item is the original item before the update
|
|
846
857
|
)
|
|
847
858
|
|
|
848
859
|
// 12. Filter readable fields and apply resolveOutput hooks (including nested relationships)
|
|
@@ -930,6 +941,7 @@ function createDelete<TPrisma extends PrismaClientLike>(
|
|
|
930
941
|
await executeAfterOperation(listConfig.hooks, {
|
|
931
942
|
operation: 'delete',
|
|
932
943
|
item: deleted,
|
|
944
|
+
originalItem: item, // item is the original item before deletion
|
|
933
945
|
context,
|
|
934
946
|
})
|
|
935
947
|
|
|
@@ -941,6 +953,7 @@ function createDelete<TPrisma extends PrismaClientLike>(
|
|
|
941
953
|
'delete',
|
|
942
954
|
context,
|
|
943
955
|
listName,
|
|
956
|
+
item, // item is the original item before deletion
|
|
944
957
|
)
|
|
945
958
|
|
|
946
959
|
return deleted
|
|
@@ -87,6 +87,7 @@ async function processNestedCreate(
|
|
|
87
87
|
let resolvedData = await executeResolveInput(relatedListConfig.hooks, {
|
|
88
88
|
operation: 'create',
|
|
89
89
|
resolvedData: item,
|
|
90
|
+
item: undefined,
|
|
90
91
|
context,
|
|
91
92
|
})
|
|
92
93
|
|
|
@@ -113,6 +114,7 @@ async function processNestedCreate(
|
|
|
113
114
|
await executeValidateInput(relatedListConfig.hooks, {
|
|
114
115
|
operation: 'create',
|
|
115
116
|
resolvedData,
|
|
117
|
+
item: undefined,
|
|
116
118
|
context,
|
|
117
119
|
})
|
|
118
120
|
|