@opensaas/stack-core 0.10.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 +121 -0
- package/dist/config/types.d.ts +145 -23
- 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 +202 -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 +186 -45
- package/src/context/index.ts +13 -0
- package/src/context/nested-operations.ts +2 -0
- package/src/fields/index.ts +238 -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
|
}
|
|
@@ -309,18 +389,32 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
309
389
|
many?: boolean
|
|
310
390
|
db?: {
|
|
311
391
|
/**
|
|
312
|
-
* Controls foreign key placement for bidirectional relationships
|
|
392
|
+
* Controls foreign key placement and column name for bidirectional relationships
|
|
393
|
+
* Can be a boolean or an object with a map property
|
|
313
394
|
* Only valid on single (non-many) relationships
|
|
314
395
|
* Cannot be true on both sides of a one-to-one relationship
|
|
315
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
|
+
*
|
|
316
400
|
* @example
|
|
317
401
|
* ```typescript
|
|
318
|
-
* // One-to-one: User has one Account
|
|
402
|
+
* // One-to-one: User has one Account (default foreign key name)
|
|
319
403
|
* User: list({
|
|
320
404
|
* fields: {
|
|
321
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")
|
|
322
415
|
* }
|
|
323
416
|
* })
|
|
417
|
+
*
|
|
324
418
|
* Account: list({
|
|
325
419
|
* fields: {
|
|
326
420
|
* user: relationship({ ref: 'User.account' }) // No foreign key on this side
|
|
@@ -328,7 +422,7 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
328
422
|
* })
|
|
329
423
|
* ```
|
|
330
424
|
*/
|
|
331
|
-
foreignKey?: boolean
|
|
425
|
+
foreignKey?: boolean | { map?: string }
|
|
332
426
|
}
|
|
333
427
|
ui?: {
|
|
334
428
|
displayMode?: 'select' | 'cards'
|
|
@@ -567,19 +661,69 @@ export type ResolveInputHookArgs<
|
|
|
567
661
|
}
|
|
568
662
|
|
|
569
663
|
/**
|
|
570
|
-
* Hook arguments for
|
|
571
|
-
*
|
|
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
|
|
572
668
|
*/
|
|
573
|
-
export type
|
|
669
|
+
export type ValidateInputHookArgs<
|
|
574
670
|
TOutput = Record<string, unknown>,
|
|
575
671
|
TCreateInput = Record<string, unknown>,
|
|
576
672
|
TUpdateInput = Record<string, unknown>,
|
|
577
|
-
> =
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
}
|
|
583
727
|
|
|
584
728
|
export type Hooks<
|
|
585
729
|
TOutput = Record<string, unknown>,
|
|
@@ -590,13 +734,10 @@ export type Hooks<
|
|
|
590
734
|
args: ResolveInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
|
|
591
735
|
) => Promise<TCreateInput | TUpdateInput>
|
|
592
736
|
validateInput?: (
|
|
593
|
-
args:
|
|
594
|
-
operation: 'create' | 'update'
|
|
595
|
-
addValidationError: (msg: string) => void
|
|
596
|
-
},
|
|
737
|
+
args: ValidateInputHookArgs<TOutput, TCreateInput, TUpdateInput>,
|
|
597
738
|
) => Promise<void>
|
|
598
|
-
beforeOperation?: (args:
|
|
599
|
-
afterOperation?: (args:
|
|
739
|
+
beforeOperation?: (args: BeforeOperationHookArgs<TOutput>) => Promise<void>
|
|
740
|
+
afterOperation?: (args: AfterOperationHookArgs<TOutput>) => Promise<void>
|
|
600
741
|
}
|
|
601
742
|
|
|
602
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
|
|