@opensaas/stack-core 0.10.0 → 0.12.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 +177 -0
- package/dist/config/types.d.ts +190 -24
- 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 +86 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +323 -13
- 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 +235 -46
- package/src/context/index.ts +13 -0
- package/src/context/nested-operations.ts +2 -0
- package/src/fields/index.ts +372 -13
- 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,8 +2,10 @@ import { z } from 'zod'
|
|
|
2
2
|
import type {
|
|
3
3
|
TextField,
|
|
4
4
|
IntegerField,
|
|
5
|
+
DecimalField,
|
|
5
6
|
CheckboxField,
|
|
6
7
|
TimestampField,
|
|
8
|
+
CalendarDayField,
|
|
7
9
|
PasswordField,
|
|
8
10
|
SelectField,
|
|
9
11
|
RelationshipField,
|
|
@@ -64,16 +66,23 @@ export function text<
|
|
|
64
66
|
|
|
65
67
|
return !isRequired ? withMax.optional().nullable() : withMax
|
|
66
68
|
},
|
|
67
|
-
getPrismaType: () => {
|
|
69
|
+
getPrismaType: (_fieldName: string) => {
|
|
68
70
|
const validation = options?.validation
|
|
71
|
+
const db = options?.db
|
|
69
72
|
const isRequired = validation?.isRequired
|
|
73
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
70
74
|
let modifiers = ''
|
|
71
75
|
|
|
72
76
|
// Optional modifier
|
|
73
|
-
if (
|
|
77
|
+
if (isNullable) {
|
|
74
78
|
modifiers += '?'
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
// Native type modifier (e.g., @db.Text)
|
|
82
|
+
if (db?.nativeType) {
|
|
83
|
+
modifiers += ` @db.${db.nativeType}`
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
// Unique/index modifiers
|
|
78
87
|
if (options?.isIndexed === 'unique') {
|
|
79
88
|
modifiers += ' @unique'
|
|
@@ -81,6 +90,11 @@ export function text<
|
|
|
81
90
|
modifiers += ' @index'
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
// Map modifier
|
|
94
|
+
if (db?.map) {
|
|
95
|
+
modifiers += ` @map("${db.map}")`
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
return {
|
|
85
99
|
type: 'String',
|
|
86
100
|
modifiers: modifiers || undefined,
|
|
@@ -130,12 +144,23 @@ export function integer<
|
|
|
130
144
|
? withMax.optional().nullable()
|
|
131
145
|
: withMax
|
|
132
146
|
},
|
|
133
|
-
getPrismaType: () => {
|
|
147
|
+
getPrismaType: (_fieldName: string) => {
|
|
134
148
|
const isRequired = options?.validation?.isRequired
|
|
149
|
+
let modifiers = ''
|
|
150
|
+
|
|
151
|
+
// Optional modifier
|
|
152
|
+
if (!isRequired) {
|
|
153
|
+
modifiers += '?'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Map modifier
|
|
157
|
+
if (options?.db?.map) {
|
|
158
|
+
modifiers += ` @map("${options.db.map}")`
|
|
159
|
+
}
|
|
135
160
|
|
|
136
161
|
return {
|
|
137
162
|
type: 'Int',
|
|
138
|
-
modifiers:
|
|
163
|
+
modifiers: modifiers || undefined,
|
|
139
164
|
}
|
|
140
165
|
},
|
|
141
166
|
getTypeScriptType: () => {
|
|
@@ -149,6 +174,172 @@ export function integer<
|
|
|
149
174
|
}
|
|
150
175
|
}
|
|
151
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Decimal field for precise numeric values (e.g., currency, measurements)
|
|
179
|
+
*
|
|
180
|
+
* **Features:**
|
|
181
|
+
* - Stores decimal numbers with configurable precision and scale
|
|
182
|
+
* - Uses Prisma's Decimal type (backed by decimal.js for precision)
|
|
183
|
+
* - Default precision: 18 digits, scale: 4 decimal places
|
|
184
|
+
* - Validation for min/max values
|
|
185
|
+
* - Optional database column mapping and nullability control
|
|
186
|
+
* - Index support (boolean or 'unique')
|
|
187
|
+
*
|
|
188
|
+
* **Usage Example:**
|
|
189
|
+
* ```typescript
|
|
190
|
+
* // In opensaas.config.ts
|
|
191
|
+
* fields: {
|
|
192
|
+
* price: decimal({
|
|
193
|
+
* precision: 10,
|
|
194
|
+
* scale: 2,
|
|
195
|
+
* validation: {
|
|
196
|
+
* isRequired: true,
|
|
197
|
+
* min: '0',
|
|
198
|
+
* max: '999999.99'
|
|
199
|
+
* }
|
|
200
|
+
* }),
|
|
201
|
+
* coordinates: decimal({
|
|
202
|
+
* precision: 18,
|
|
203
|
+
* scale: 8,
|
|
204
|
+
* db: { map: 'coord_value' }
|
|
205
|
+
* })
|
|
206
|
+
* }
|
|
207
|
+
*
|
|
208
|
+
* // Creating with decimal values
|
|
209
|
+
* const product = await context.db.product.create({
|
|
210
|
+
* data: {
|
|
211
|
+
* price: '19.99', // Can use string
|
|
212
|
+
* // price: 19.99, // or number (converted to Decimal)
|
|
213
|
+
* }
|
|
214
|
+
* })
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* @param options - Field configuration options
|
|
218
|
+
* @returns Decimal field configuration
|
|
219
|
+
*/
|
|
220
|
+
export function decimal<
|
|
221
|
+
TTypeInfo extends import('../config/types.js').TypeInfo = import('../config/types.js').TypeInfo,
|
|
222
|
+
>(options?: Omit<DecimalField<TTypeInfo>, 'type'>): DecimalField<TTypeInfo> {
|
|
223
|
+
const precision = options?.precision ?? 18
|
|
224
|
+
const scale = options?.scale ?? 4
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
type: 'decimal',
|
|
228
|
+
precision,
|
|
229
|
+
scale,
|
|
230
|
+
...options,
|
|
231
|
+
getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
|
|
232
|
+
// Decimal values can be provided as strings or numbers
|
|
233
|
+
// Prisma will convert them to Decimal instances
|
|
234
|
+
const baseSchema = z.union(
|
|
235
|
+
[
|
|
236
|
+
z.string({
|
|
237
|
+
message: `${formatFieldName(fieldName)} must be a decimal value (string or number)`,
|
|
238
|
+
}),
|
|
239
|
+
z.number({
|
|
240
|
+
message: `${formatFieldName(fieldName)} must be a decimal value (string or number)`,
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
{
|
|
244
|
+
message: `${formatFieldName(fieldName)} must be a decimal value`,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
let schema = baseSchema
|
|
249
|
+
|
|
250
|
+
// Add min validation if specified
|
|
251
|
+
if (options?.validation?.min !== undefined) {
|
|
252
|
+
const minValue = parseFloat(options.validation.min)
|
|
253
|
+
schema = schema.refine(
|
|
254
|
+
(val) => {
|
|
255
|
+
const numVal = typeof val === 'string' ? parseFloat(val) : val
|
|
256
|
+
return !isNaN(numVal) && numVal >= minValue
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
message: `${formatFieldName(fieldName)} must be at least ${options.validation.min}`,
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Add max validation if specified
|
|
265
|
+
if (options?.validation?.max !== undefined) {
|
|
266
|
+
const maxValue = parseFloat(options.validation.max)
|
|
267
|
+
schema = schema.refine(
|
|
268
|
+
(val) => {
|
|
269
|
+
const numVal = typeof val === 'string' ? parseFloat(val) : val
|
|
270
|
+
return !isNaN(numVal) && numVal <= maxValue
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
message: `${formatFieldName(fieldName)} must be at most ${options.validation.max}`,
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return !options?.validation?.isRequired || operation === 'update'
|
|
279
|
+
? schema.optional().nullable()
|
|
280
|
+
: schema
|
|
281
|
+
},
|
|
282
|
+
getPrismaType: (_fieldName: string) => {
|
|
283
|
+
const validation = options?.validation
|
|
284
|
+
const db = options?.db
|
|
285
|
+
const isRequired = validation?.isRequired
|
|
286
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
287
|
+
|
|
288
|
+
let modifiers = ''
|
|
289
|
+
|
|
290
|
+
// Optional modifier
|
|
291
|
+
if (isNullable) {
|
|
292
|
+
modifiers += '?'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Precision and scale
|
|
296
|
+
modifiers += ` @db.Decimal(${precision}, ${scale})`
|
|
297
|
+
|
|
298
|
+
// Default value if provided
|
|
299
|
+
if (options?.defaultValue !== undefined) {
|
|
300
|
+
modifiers += ` @default(${options.defaultValue})`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Database mapping
|
|
304
|
+
if (db?.map) {
|
|
305
|
+
modifiers += ` @map("${db.map}")`
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Unique/index modifiers
|
|
309
|
+
if (options?.isIndexed === 'unique') {
|
|
310
|
+
modifiers += ' @unique'
|
|
311
|
+
} else if (options?.isIndexed === true) {
|
|
312
|
+
modifiers += ' @index'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
type: 'Decimal',
|
|
317
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
getTypeScriptType: () => {
|
|
321
|
+
const validation = options?.validation
|
|
322
|
+
const db = options?.db
|
|
323
|
+
const isRequired = validation?.isRequired
|
|
324
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
type: "import('decimal.js').Decimal",
|
|
328
|
+
optional: isNullable,
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
getTypeScriptImports: () => {
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
names: ['Decimal'],
|
|
335
|
+
from: 'decimal.js',
|
|
336
|
+
typeOnly: true,
|
|
337
|
+
},
|
|
338
|
+
]
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
152
343
|
/**
|
|
153
344
|
* Checkbox (boolean) field
|
|
154
345
|
*/
|
|
@@ -161,7 +352,7 @@ export function checkbox<
|
|
|
161
352
|
getZodSchema: () => {
|
|
162
353
|
return z.boolean().optional().nullable()
|
|
163
354
|
},
|
|
164
|
-
getPrismaType: () => {
|
|
355
|
+
getPrismaType: (_fieldName: string) => {
|
|
165
356
|
const hasDefault = options?.defaultValue !== undefined
|
|
166
357
|
let modifiers = ''
|
|
167
358
|
|
|
@@ -169,6 +360,11 @@ export function checkbox<
|
|
|
169
360
|
modifiers = ` @default(${options.defaultValue})`
|
|
170
361
|
}
|
|
171
362
|
|
|
363
|
+
// Map modifier
|
|
364
|
+
if (options?.db?.map) {
|
|
365
|
+
modifiers += ` @map("${options.db.map}")`
|
|
366
|
+
}
|
|
367
|
+
|
|
172
368
|
return {
|
|
173
369
|
type: 'Boolean',
|
|
174
370
|
modifiers: modifiers || undefined,
|
|
@@ -195,7 +391,7 @@ export function timestamp<
|
|
|
195
391
|
getZodSchema: () => {
|
|
196
392
|
return z.union([z.date(), z.iso.datetime()]).optional().nullable()
|
|
197
393
|
},
|
|
198
|
-
getPrismaType: () => {
|
|
394
|
+
getPrismaType: (_fieldName: string) => {
|
|
199
395
|
let modifiers = '?'
|
|
200
396
|
|
|
201
397
|
// Check for default value
|
|
@@ -208,6 +404,11 @@ export function timestamp<
|
|
|
208
404
|
modifiers = ' @default(now())'
|
|
209
405
|
}
|
|
210
406
|
|
|
407
|
+
// Map modifier
|
|
408
|
+
if (options?.db?.map) {
|
|
409
|
+
modifiers += ` @map("${options.db.map}")`
|
|
410
|
+
}
|
|
411
|
+
|
|
211
412
|
return {
|
|
212
413
|
type: 'DateTime',
|
|
213
414
|
modifiers,
|
|
@@ -228,6 +429,131 @@ export function timestamp<
|
|
|
228
429
|
}
|
|
229
430
|
}
|
|
230
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Calendar Day field - date only (no time) in ISO8601 format
|
|
434
|
+
*
|
|
435
|
+
* **Features:**
|
|
436
|
+
* - Stores date values only (no time component)
|
|
437
|
+
* - PostgreSQL/MySQL: Uses native DATE type via @db.Date
|
|
438
|
+
* - SQLite: Uses String representation
|
|
439
|
+
* - Accepts ISO8601 date strings (YYYY-MM-DD format)
|
|
440
|
+
* - Optional validation for required fields
|
|
441
|
+
* - Database column mapping and nullability control
|
|
442
|
+
* - Index support (boolean or 'unique')
|
|
443
|
+
*
|
|
444
|
+
* **Usage Example:**
|
|
445
|
+
* ```typescript
|
|
446
|
+
* // In opensaas.config.ts
|
|
447
|
+
* fields: {
|
|
448
|
+
* birthDate: calendarDay({
|
|
449
|
+
* validation: { isRequired: true }
|
|
450
|
+
* }),
|
|
451
|
+
* startDate: calendarDay({
|
|
452
|
+
* defaultValue: '2025-01-01',
|
|
453
|
+
* db: { map: 'start_date' }
|
|
454
|
+
* }),
|
|
455
|
+
* endDate: calendarDay({
|
|
456
|
+
* isIndexed: true
|
|
457
|
+
* })
|
|
458
|
+
* }
|
|
459
|
+
*
|
|
460
|
+
* // Creating with date values
|
|
461
|
+
* const event = await context.db.event.create({
|
|
462
|
+
* data: {
|
|
463
|
+
* startDate: '2025-01-15',
|
|
464
|
+
* endDate: '2025-01-20'
|
|
465
|
+
* }
|
|
466
|
+
* })
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* @param options - Field configuration options
|
|
470
|
+
* @returns Calendar Day field configuration
|
|
471
|
+
*/
|
|
472
|
+
export function calendarDay<
|
|
473
|
+
TTypeInfo extends import('../config/types.js').TypeInfo = import('../config/types.js').TypeInfo,
|
|
474
|
+
>(options?: Omit<CalendarDayField<TTypeInfo>, 'type'>): CalendarDayField<TTypeInfo> {
|
|
475
|
+
return {
|
|
476
|
+
type: 'calendarDay',
|
|
477
|
+
...options,
|
|
478
|
+
getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
|
|
479
|
+
const validation = options?.validation
|
|
480
|
+
const isRequired = validation?.isRequired
|
|
481
|
+
|
|
482
|
+
// Accept ISO8601 date strings (YYYY-MM-DD)
|
|
483
|
+
const baseSchema = z.string({
|
|
484
|
+
message: `${formatFieldName(fieldName)} must be a valid date in ISO8601 format (YYYY-MM-DD)`,
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Validate ISO8601 date format (YYYY-MM-DD)
|
|
488
|
+
const dateSchema = baseSchema.regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
489
|
+
message: `${formatFieldName(fieldName)} must be in YYYY-MM-DD format`,
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
if (isRequired && operation === 'create') {
|
|
493
|
+
return dateSchema
|
|
494
|
+
} else if (isRequired && operation === 'update') {
|
|
495
|
+
// Required in update mode: can be undefined for partial updates
|
|
496
|
+
return z.union([dateSchema, z.undefined()])
|
|
497
|
+
} else {
|
|
498
|
+
return dateSchema.optional().nullable()
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
getPrismaType: (_fieldName: string, provider?: string) => {
|
|
502
|
+
const validation = options?.validation
|
|
503
|
+
const db = options?.db
|
|
504
|
+
const isRequired = validation?.isRequired
|
|
505
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
506
|
+
|
|
507
|
+
let modifiers = ''
|
|
508
|
+
|
|
509
|
+
// Optional modifier
|
|
510
|
+
if (isNullable) {
|
|
511
|
+
modifiers += '?'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Add @db.Date attribute for date-only storage
|
|
515
|
+
// Only for PostgreSQL/MySQL - SQLite doesn't support native DATE type
|
|
516
|
+
// SQLite will use TEXT for DateTime fields
|
|
517
|
+
if (provider && provider.toLowerCase() !== 'sqlite') {
|
|
518
|
+
modifiers += ' @db.Date'
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Default value if provided
|
|
522
|
+
if (options?.defaultValue !== undefined) {
|
|
523
|
+
modifiers += ` @default("${options.defaultValue}")`
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Database mapping
|
|
527
|
+
if (db?.map) {
|
|
528
|
+
modifiers += ` @map("${db.map}")`
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Unique/index modifiers
|
|
532
|
+
if (options?.isIndexed === 'unique') {
|
|
533
|
+
modifiers += ' @unique'
|
|
534
|
+
} else if (options?.isIndexed === true) {
|
|
535
|
+
modifiers += ' @index'
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
type: 'DateTime',
|
|
540
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
getTypeScriptType: () => {
|
|
544
|
+
const validation = options?.validation
|
|
545
|
+
const db = options?.db
|
|
546
|
+
const isRequired = validation?.isRequired
|
|
547
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
type: 'Date',
|
|
551
|
+
optional: isNullable,
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
231
557
|
/**
|
|
232
558
|
* Password field (automatically hashed using bcrypt)
|
|
233
559
|
*
|
|
@@ -360,12 +686,23 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
360
686
|
.nullable()
|
|
361
687
|
}
|
|
362
688
|
},
|
|
363
|
-
getPrismaType: () => {
|
|
689
|
+
getPrismaType: (_fieldName: string) => {
|
|
364
690
|
const isRequired = options?.validation?.isRequired
|
|
691
|
+
let modifiers = ''
|
|
692
|
+
|
|
693
|
+
// Optional modifier
|
|
694
|
+
if (!isRequired) {
|
|
695
|
+
modifiers += '?'
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Map modifier
|
|
699
|
+
if (options?.db?.map) {
|
|
700
|
+
modifiers += ` @map("${options.db.map}")`
|
|
701
|
+
}
|
|
365
702
|
|
|
366
703
|
return {
|
|
367
704
|
type: 'String',
|
|
368
|
-
modifiers:
|
|
705
|
+
modifiers: modifiers || undefined,
|
|
369
706
|
}
|
|
370
707
|
},
|
|
371
708
|
getTypeScriptType: () => {
|
|
@@ -404,17 +741,28 @@ export function select<
|
|
|
404
741
|
|
|
405
742
|
return schema
|
|
406
743
|
},
|
|
407
|
-
getPrismaType: () => {
|
|
408
|
-
|
|
744
|
+
getPrismaType: (_fieldName: string) => {
|
|
745
|
+
const isRequired = options.validation?.isRequired
|
|
746
|
+
let modifiers = ''
|
|
747
|
+
|
|
748
|
+
// Required fields don't get the ? modifier
|
|
749
|
+
if (!isRequired) {
|
|
750
|
+
modifiers = '?'
|
|
751
|
+
}
|
|
409
752
|
|
|
410
753
|
// Add default value if provided
|
|
411
754
|
if (options.defaultValue !== undefined) {
|
|
412
755
|
modifiers = ` @default("${options.defaultValue}")`
|
|
413
756
|
}
|
|
414
757
|
|
|
758
|
+
// Map modifier
|
|
759
|
+
if (options.db?.map) {
|
|
760
|
+
modifiers += ` @map("${options.db.map}")`
|
|
761
|
+
}
|
|
762
|
+
|
|
415
763
|
return {
|
|
416
764
|
type: 'String',
|
|
417
|
-
modifiers,
|
|
765
|
+
modifiers: modifiers || undefined,
|
|
418
766
|
}
|
|
419
767
|
},
|
|
420
768
|
getTypeScriptType: () => {
|
|
@@ -539,12 +887,23 @@ export function json<
|
|
|
539
887
|
return baseSchema.optional().nullable()
|
|
540
888
|
}
|
|
541
889
|
},
|
|
542
|
-
getPrismaType: () => {
|
|
890
|
+
getPrismaType: (_fieldName: string) => {
|
|
543
891
|
const isRequired = options?.validation?.isRequired
|
|
892
|
+
let modifiers = ''
|
|
893
|
+
|
|
894
|
+
// Optional modifier
|
|
895
|
+
if (!isRequired) {
|
|
896
|
+
modifiers += '?'
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Map modifier
|
|
900
|
+
if (options?.db?.map) {
|
|
901
|
+
modifiers += ` @map("${options.db.map}")`
|
|
902
|
+
}
|
|
544
903
|
|
|
545
904
|
return {
|
|
546
905
|
type: 'Json',
|
|
547
|
-
modifiers:
|
|
906
|
+
modifiers: modifiers || undefined,
|
|
548
907
|
}
|
|
549
908
|
},
|
|
550
909
|
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', () => {
|