@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/src/fields/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod'
|
|
|
2
2
|
import type {
|
|
3
3
|
TextField,
|
|
4
4
|
IntegerField,
|
|
5
|
+
DecimalField,
|
|
5
6
|
CheckboxField,
|
|
6
7
|
TimestampField,
|
|
7
8
|
PasswordField,
|
|
@@ -64,7 +65,7 @@ export function text<
|
|
|
64
65
|
|
|
65
66
|
return !isRequired ? withMax.optional().nullable() : withMax
|
|
66
67
|
},
|
|
67
|
-
getPrismaType: () => {
|
|
68
|
+
getPrismaType: (_fieldName: string) => {
|
|
68
69
|
const validation = options?.validation
|
|
69
70
|
const isRequired = validation?.isRequired
|
|
70
71
|
let modifiers = ''
|
|
@@ -81,6 +82,11 @@ export function text<
|
|
|
81
82
|
modifiers += ' @index'
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// Map modifier
|
|
86
|
+
if (options?.db?.map) {
|
|
87
|
+
modifiers += ` @map("${options.db.map}")`
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
return {
|
|
85
91
|
type: 'String',
|
|
86
92
|
modifiers: modifiers || undefined,
|
|
@@ -130,12 +136,23 @@ export function integer<
|
|
|
130
136
|
? withMax.optional().nullable()
|
|
131
137
|
: withMax
|
|
132
138
|
},
|
|
133
|
-
getPrismaType: () => {
|
|
139
|
+
getPrismaType: (_fieldName: string) => {
|
|
134
140
|
const isRequired = options?.validation?.isRequired
|
|
141
|
+
let modifiers = ''
|
|
142
|
+
|
|
143
|
+
// Optional modifier
|
|
144
|
+
if (!isRequired) {
|
|
145
|
+
modifiers += '?'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Map modifier
|
|
149
|
+
if (options?.db?.map) {
|
|
150
|
+
modifiers += ` @map("${options.db.map}")`
|
|
151
|
+
}
|
|
135
152
|
|
|
136
153
|
return {
|
|
137
154
|
type: 'Int',
|
|
138
|
-
modifiers:
|
|
155
|
+
modifiers: modifiers || undefined,
|
|
139
156
|
}
|
|
140
157
|
},
|
|
141
158
|
getTypeScriptType: () => {
|
|
@@ -149,6 +166,172 @@ export function integer<
|
|
|
149
166
|
}
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Decimal field for precise numeric values (e.g., currency, measurements)
|
|
171
|
+
*
|
|
172
|
+
* **Features:**
|
|
173
|
+
* - Stores decimal numbers with configurable precision and scale
|
|
174
|
+
* - Uses Prisma's Decimal type (backed by decimal.js for precision)
|
|
175
|
+
* - Default precision: 18 digits, scale: 4 decimal places
|
|
176
|
+
* - Validation for min/max values
|
|
177
|
+
* - Optional database column mapping and nullability control
|
|
178
|
+
* - Index support (boolean or 'unique')
|
|
179
|
+
*
|
|
180
|
+
* **Usage Example:**
|
|
181
|
+
* ```typescript
|
|
182
|
+
* // In opensaas.config.ts
|
|
183
|
+
* fields: {
|
|
184
|
+
* price: decimal({
|
|
185
|
+
* precision: 10,
|
|
186
|
+
* scale: 2,
|
|
187
|
+
* validation: {
|
|
188
|
+
* isRequired: true,
|
|
189
|
+
* min: '0',
|
|
190
|
+
* max: '999999.99'
|
|
191
|
+
* }
|
|
192
|
+
* }),
|
|
193
|
+
* coordinates: decimal({
|
|
194
|
+
* precision: 18,
|
|
195
|
+
* scale: 8,
|
|
196
|
+
* db: { map: 'coord_value' }
|
|
197
|
+
* })
|
|
198
|
+
* }
|
|
199
|
+
*
|
|
200
|
+
* // Creating with decimal values
|
|
201
|
+
* const product = await context.db.product.create({
|
|
202
|
+
* data: {
|
|
203
|
+
* price: '19.99', // Can use string
|
|
204
|
+
* // price: 19.99, // or number (converted to Decimal)
|
|
205
|
+
* }
|
|
206
|
+
* })
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @param options - Field configuration options
|
|
210
|
+
* @returns Decimal field configuration
|
|
211
|
+
*/
|
|
212
|
+
export function decimal<
|
|
213
|
+
TTypeInfo extends import('../config/types.js').TypeInfo = import('../config/types.js').TypeInfo,
|
|
214
|
+
>(options?: Omit<DecimalField<TTypeInfo>, 'type'>): DecimalField<TTypeInfo> {
|
|
215
|
+
const precision = options?.precision ?? 18
|
|
216
|
+
const scale = options?.scale ?? 4
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
type: 'decimal',
|
|
220
|
+
precision,
|
|
221
|
+
scale,
|
|
222
|
+
...options,
|
|
223
|
+
getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
|
|
224
|
+
// Decimal values can be provided as strings or numbers
|
|
225
|
+
// Prisma will convert them to Decimal instances
|
|
226
|
+
const baseSchema = z.union(
|
|
227
|
+
[
|
|
228
|
+
z.string({
|
|
229
|
+
message: `${formatFieldName(fieldName)} must be a decimal value (string or number)`,
|
|
230
|
+
}),
|
|
231
|
+
z.number({
|
|
232
|
+
message: `${formatFieldName(fieldName)} must be a decimal value (string or number)`,
|
|
233
|
+
}),
|
|
234
|
+
],
|
|
235
|
+
{
|
|
236
|
+
message: `${formatFieldName(fieldName)} must be a decimal value`,
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
let schema = baseSchema
|
|
241
|
+
|
|
242
|
+
// Add min validation if specified
|
|
243
|
+
if (options?.validation?.min !== undefined) {
|
|
244
|
+
const minValue = parseFloat(options.validation.min)
|
|
245
|
+
schema = schema.refine(
|
|
246
|
+
(val) => {
|
|
247
|
+
const numVal = typeof val === 'string' ? parseFloat(val) : val
|
|
248
|
+
return !isNaN(numVal) && numVal >= minValue
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
message: `${formatFieldName(fieldName)} must be at least ${options.validation.min}`,
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Add max validation if specified
|
|
257
|
+
if (options?.validation?.max !== undefined) {
|
|
258
|
+
const maxValue = parseFloat(options.validation.max)
|
|
259
|
+
schema = schema.refine(
|
|
260
|
+
(val) => {
|
|
261
|
+
const numVal = typeof val === 'string' ? parseFloat(val) : val
|
|
262
|
+
return !isNaN(numVal) && numVal <= maxValue
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
message: `${formatFieldName(fieldName)} must be at most ${options.validation.max}`,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return !options?.validation?.isRequired || operation === 'update'
|
|
271
|
+
? schema.optional().nullable()
|
|
272
|
+
: schema
|
|
273
|
+
},
|
|
274
|
+
getPrismaType: (_fieldName: string) => {
|
|
275
|
+
const validation = options?.validation
|
|
276
|
+
const db = options?.db
|
|
277
|
+
const isRequired = validation?.isRequired
|
|
278
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
279
|
+
|
|
280
|
+
let modifiers = ''
|
|
281
|
+
|
|
282
|
+
// Optional modifier
|
|
283
|
+
if (isNullable) {
|
|
284
|
+
modifiers += '?'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Precision and scale
|
|
288
|
+
modifiers += ` @db.Decimal(${precision}, ${scale})`
|
|
289
|
+
|
|
290
|
+
// Default value if provided
|
|
291
|
+
if (options?.defaultValue !== undefined) {
|
|
292
|
+
modifiers += ` @default(${options.defaultValue})`
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Database mapping
|
|
296
|
+
if (db?.map) {
|
|
297
|
+
modifiers += ` @map("${db.map}")`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Unique/index modifiers
|
|
301
|
+
if (options?.isIndexed === 'unique') {
|
|
302
|
+
modifiers += ' @unique'
|
|
303
|
+
} else if (options?.isIndexed === true) {
|
|
304
|
+
modifiers += ' @index'
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
type: 'Decimal',
|
|
309
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
getTypeScriptType: () => {
|
|
313
|
+
const validation = options?.validation
|
|
314
|
+
const db = options?.db
|
|
315
|
+
const isRequired = validation?.isRequired
|
|
316
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
type: "import('decimal.js').Decimal",
|
|
320
|
+
optional: isNullable,
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
getTypeScriptImports: () => {
|
|
324
|
+
return [
|
|
325
|
+
{
|
|
326
|
+
names: ['Decimal'],
|
|
327
|
+
from: 'decimal.js',
|
|
328
|
+
typeOnly: true,
|
|
329
|
+
},
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
152
335
|
/**
|
|
153
336
|
* Checkbox (boolean) field
|
|
154
337
|
*/
|
|
@@ -161,7 +344,7 @@ export function checkbox<
|
|
|
161
344
|
getZodSchema: () => {
|
|
162
345
|
return z.boolean().optional().nullable()
|
|
163
346
|
},
|
|
164
|
-
getPrismaType: () => {
|
|
347
|
+
getPrismaType: (_fieldName: string) => {
|
|
165
348
|
const hasDefault = options?.defaultValue !== undefined
|
|
166
349
|
let modifiers = ''
|
|
167
350
|
|
|
@@ -169,6 +352,11 @@ export function checkbox<
|
|
|
169
352
|
modifiers = ` @default(${options.defaultValue})`
|
|
170
353
|
}
|
|
171
354
|
|
|
355
|
+
// Map modifier
|
|
356
|
+
if (options?.db?.map) {
|
|
357
|
+
modifiers += ` @map("${options.db.map}")`
|
|
358
|
+
}
|
|
359
|
+
|
|
172
360
|
return {
|
|
173
361
|
type: 'Boolean',
|
|
174
362
|
modifiers: modifiers || undefined,
|
|
@@ -195,7 +383,7 @@ export function timestamp<
|
|
|
195
383
|
getZodSchema: () => {
|
|
196
384
|
return z.union([z.date(), z.iso.datetime()]).optional().nullable()
|
|
197
385
|
},
|
|
198
|
-
getPrismaType: () => {
|
|
386
|
+
getPrismaType: (_fieldName: string) => {
|
|
199
387
|
let modifiers = '?'
|
|
200
388
|
|
|
201
389
|
// Check for default value
|
|
@@ -208,6 +396,11 @@ export function timestamp<
|
|
|
208
396
|
modifiers = ' @default(now())'
|
|
209
397
|
}
|
|
210
398
|
|
|
399
|
+
// Map modifier
|
|
400
|
+
if (options?.db?.map) {
|
|
401
|
+
modifiers += ` @map("${options.db.map}")`
|
|
402
|
+
}
|
|
403
|
+
|
|
211
404
|
return {
|
|
212
405
|
type: 'DateTime',
|
|
213
406
|
modifiers,
|
|
@@ -360,12 +553,23 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
360
553
|
.nullable()
|
|
361
554
|
}
|
|
362
555
|
},
|
|
363
|
-
getPrismaType: () => {
|
|
556
|
+
getPrismaType: (_fieldName: string) => {
|
|
364
557
|
const isRequired = options?.validation?.isRequired
|
|
558
|
+
let modifiers = ''
|
|
559
|
+
|
|
560
|
+
// Optional modifier
|
|
561
|
+
if (!isRequired) {
|
|
562
|
+
modifiers += '?'
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Map modifier
|
|
566
|
+
if (options?.db?.map) {
|
|
567
|
+
modifiers += ` @map("${options.db.map}")`
|
|
568
|
+
}
|
|
365
569
|
|
|
366
570
|
return {
|
|
367
571
|
type: 'String',
|
|
368
|
-
modifiers:
|
|
572
|
+
modifiers: modifiers || undefined,
|
|
369
573
|
}
|
|
370
574
|
},
|
|
371
575
|
getTypeScriptType: () => {
|
|
@@ -404,17 +608,28 @@ export function select<
|
|
|
404
608
|
|
|
405
609
|
return schema
|
|
406
610
|
},
|
|
407
|
-
getPrismaType: () => {
|
|
408
|
-
|
|
611
|
+
getPrismaType: (_fieldName: string) => {
|
|
612
|
+
const isRequired = options.validation?.isRequired
|
|
613
|
+
let modifiers = ''
|
|
614
|
+
|
|
615
|
+
// Required fields don't get the ? modifier
|
|
616
|
+
if (!isRequired) {
|
|
617
|
+
modifiers = '?'
|
|
618
|
+
}
|
|
409
619
|
|
|
410
620
|
// Add default value if provided
|
|
411
621
|
if (options.defaultValue !== undefined) {
|
|
412
622
|
modifiers = ` @default("${options.defaultValue}")`
|
|
413
623
|
}
|
|
414
624
|
|
|
625
|
+
// Map modifier
|
|
626
|
+
if (options.db?.map) {
|
|
627
|
+
modifiers += ` @map("${options.db.map}")`
|
|
628
|
+
}
|
|
629
|
+
|
|
415
630
|
return {
|
|
416
631
|
type: 'String',
|
|
417
|
-
modifiers,
|
|
632
|
+
modifiers: modifiers || undefined,
|
|
418
633
|
}
|
|
419
634
|
},
|
|
420
635
|
getTypeScriptType: () => {
|
|
@@ -447,6 +662,25 @@ export function relationship<
|
|
|
447
662
|
)
|
|
448
663
|
}
|
|
449
664
|
|
|
665
|
+
// Validate db.foreignKey usage
|
|
666
|
+
if (options.db?.foreignKey !== undefined) {
|
|
667
|
+
// Can only be used on single relationships (not many)
|
|
668
|
+
if (options.many) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
'db.foreignKey can only be used on single relationships (many: false or undefined). ' +
|
|
671
|
+
'Many-side of a relationship never stores the foreign key.',
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Can only be used on bidirectional relationships (with target field)
|
|
676
|
+
if (refParts.length === 1) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
'db.foreignKey can only be used on bidirectional relationships (ref: "ListName.fieldName"). ' +
|
|
679
|
+
'List-only refs (ref: "ListName") always create foreign keys automatically.',
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
450
684
|
return {
|
|
451
685
|
type: 'relationship',
|
|
452
686
|
...options,
|
|
@@ -520,12 +754,23 @@ export function json<
|
|
|
520
754
|
return baseSchema.optional().nullable()
|
|
521
755
|
}
|
|
522
756
|
},
|
|
523
|
-
getPrismaType: () => {
|
|
757
|
+
getPrismaType: (_fieldName: string) => {
|
|
524
758
|
const isRequired = options?.validation?.isRequired
|
|
759
|
+
let modifiers = ''
|
|
760
|
+
|
|
761
|
+
// Optional modifier
|
|
762
|
+
if (!isRequired) {
|
|
763
|
+
modifiers += '?'
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Map modifier
|
|
767
|
+
if (options?.db?.map) {
|
|
768
|
+
modifiers += ` @map("${options.db.map}")`
|
|
769
|
+
}
|
|
525
770
|
|
|
526
771
|
return {
|
|
527
772
|
type: 'Json',
|
|
528
|
-
modifiers:
|
|
773
|
+
modifiers: modifiers || undefined,
|
|
529
774
|
}
|
|
530
775
|
},
|
|
531
776
|
getTypeScriptType: () => {
|
package/src/hooks/index.ts
CHANGED
|
@@ -48,13 +48,13 @@ export async function executeResolveInput<
|
|
|
48
48
|
| {
|
|
49
49
|
operation: 'create'
|
|
50
50
|
resolvedData: TCreateInput
|
|
51
|
-
item
|
|
51
|
+
item: undefined
|
|
52
52
|
context: AccessContext
|
|
53
53
|
}
|
|
54
54
|
| {
|
|
55
55
|
operation: 'update'
|
|
56
56
|
resolvedData: TUpdateInput
|
|
57
|
-
item
|
|
57
|
+
item: TOutput
|
|
58
58
|
context: AccessContext
|
|
59
59
|
},
|
|
60
60
|
): Promise<TCreateInput | TUpdateInput> {
|
|
@@ -62,9 +62,7 @@ export async function executeResolveInput<
|
|
|
62
62
|
return args.resolvedData
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
-
const result = await hooks.resolveInput(args as any)
|
|
65
|
+
const result = await hooks.resolveInput(args)
|
|
68
66
|
return result
|
|
69
67
|
}
|
|
70
68
|
|
|
@@ -78,12 +76,19 @@ export async function executeValidateInput<
|
|
|
78
76
|
TUpdateInput = Record<string, unknown>,
|
|
79
77
|
>(
|
|
80
78
|
hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
|
|
81
|
-
args:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
args:
|
|
80
|
+
| {
|
|
81
|
+
operation: 'create'
|
|
82
|
+
resolvedData: TCreateInput
|
|
83
|
+
item: undefined
|
|
84
|
+
context: AccessContext
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
operation: 'update'
|
|
88
|
+
resolvedData: TUpdateInput
|
|
89
|
+
item: TOutput
|
|
90
|
+
context: AccessContext
|
|
91
|
+
},
|
|
87
92
|
): Promise<void> {
|
|
88
93
|
if (!hooks?.validateInput) {
|
|
89
94
|
return
|
|
@@ -109,18 +114,18 @@ export async function executeValidateInput<
|
|
|
109
114
|
* Execute beforeOperation hook
|
|
110
115
|
* Runs before database operation (cannot modify data)
|
|
111
116
|
*/
|
|
112
|
-
export async function executeBeforeOperation<
|
|
113
|
-
TOutput
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
export async function executeBeforeOperation<TOutput = Record<string, unknown>>(
|
|
118
|
+
hooks: Hooks<TOutput> | undefined,
|
|
119
|
+
args:
|
|
120
|
+
| {
|
|
121
|
+
operation: 'create'
|
|
122
|
+
context: AccessContext
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
operation: 'update' | 'delete'
|
|
126
|
+
item: TOutput
|
|
127
|
+
context: AccessContext
|
|
128
|
+
},
|
|
124
129
|
): Promise<void> {
|
|
125
130
|
if (!hooks?.beforeOperation) {
|
|
126
131
|
return
|
|
@@ -133,18 +138,21 @@ export async function executeBeforeOperation<
|
|
|
133
138
|
* Execute afterOperation hook
|
|
134
139
|
* Runs after database operation
|
|
135
140
|
*/
|
|
136
|
-
export async function executeAfterOperation<
|
|
137
|
-
TOutput
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
export async function executeAfterOperation<TOutput = Record<string, unknown>>(
|
|
142
|
+
hooks: Hooks<TOutput> | undefined,
|
|
143
|
+
args:
|
|
144
|
+
| {
|
|
145
|
+
operation: 'create'
|
|
146
|
+
item: TOutput
|
|
147
|
+
originalItem: undefined
|
|
148
|
+
context: AccessContext
|
|
149
|
+
}
|
|
150
|
+
| {
|
|
151
|
+
operation: 'update' | 'delete'
|
|
152
|
+
item: TOutput
|
|
153
|
+
originalItem: TOutput
|
|
154
|
+
context: AccessContext
|
|
155
|
+
},
|
|
148
156
|
): Promise<void> {
|
|
149
157
|
if (!hooks?.afterOperation) {
|
|
150
158
|
return
|
package/tests/hooks.test.ts
CHANGED
|
@@ -252,6 +252,70 @@ describe('Hooks', () => {
|
|
|
252
252
|
context: mockContext,
|
|
253
253
|
})
|
|
254
254
|
})
|
|
255
|
+
|
|
256
|
+
it('should pass originalItem to afterOperation hook for update operation', async () => {
|
|
257
|
+
const originalItem = { id: '1', name: 'John' }
|
|
258
|
+
const updatedItem = { id: '1', name: 'Jane' }
|
|
259
|
+
const hooks: Hooks = {
|
|
260
|
+
afterOperation: vi.fn(async () => {}),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await executeAfterOperation(hooks, {
|
|
264
|
+
operation: 'update',
|
|
265
|
+
item: updatedItem,
|
|
266
|
+
originalItem,
|
|
267
|
+
context: mockContext,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(hooks.afterOperation).toHaveBeenCalledWith({
|
|
271
|
+
operation: 'update',
|
|
272
|
+
item: updatedItem,
|
|
273
|
+
originalItem,
|
|
274
|
+
context: mockContext,
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should pass originalItem to afterOperation hook for delete operation', async () => {
|
|
279
|
+
const item = { id: '1', name: 'John' }
|
|
280
|
+
const hooks: Hooks = {
|
|
281
|
+
afterOperation: vi.fn(async () => {}),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await executeAfterOperation(hooks, {
|
|
285
|
+
operation: 'delete',
|
|
286
|
+
item,
|
|
287
|
+
originalItem: item,
|
|
288
|
+
context: mockContext,
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
expect(hooks.afterOperation).toHaveBeenCalledWith({
|
|
292
|
+
operation: 'delete',
|
|
293
|
+
item,
|
|
294
|
+
originalItem: item,
|
|
295
|
+
context: mockContext,
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should pass undefined originalItem for create operation', async () => {
|
|
300
|
+
const item = { id: '1', name: 'John' }
|
|
301
|
+
const hooks: Hooks = {
|
|
302
|
+
afterOperation: vi.fn(async () => {}),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await executeAfterOperation(hooks, {
|
|
306
|
+
operation: 'create',
|
|
307
|
+
item,
|
|
308
|
+
originalItem: undefined,
|
|
309
|
+
context: mockContext,
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
expect(hooks.afterOperation).toHaveBeenCalledWith({
|
|
313
|
+
operation: 'create',
|
|
314
|
+
item,
|
|
315
|
+
originalItem: undefined,
|
|
316
|
+
context: mockContext,
|
|
317
|
+
})
|
|
318
|
+
})
|
|
255
319
|
})
|
|
256
320
|
|
|
257
321
|
describe('validateFieldRules', () => {
|