@opensaas/stack-core 0.18.1 → 0.19.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 +153 -0
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +41 -18
- package/dist/access/engine.js.map +1 -1
- package/dist/access/types.d.ts +9 -0
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/types.d.ts +69 -41
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +5 -1
- package/dist/context/index.js.map +1 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +109 -32
- package/dist/fields/index.js.map +1 -1
- package/dist/fields/select.test.d.ts +2 -0
- package/dist/fields/select.test.d.ts.map +1 -0
- package/dist/fields/select.test.js +194 -0
- package/dist/fields/select.test.js.map +1 -0
- package/dist/validation/schema.test.js +45 -0
- package/dist/validation/schema.test.js.map +1 -1
- package/package.json +2 -2
- package/src/access/engine.ts +44 -22
- package/src/access/types.ts +7 -0
- package/src/config/types.ts +69 -40
- package/src/context/index.ts +14 -1
- package/src/fields/index.ts +129 -33
- package/src/fields/select.test.ts +237 -0
- package/src/validation/schema.test.ts +51 -0
- package/tests/access-relationships.test.ts +17 -0
- package/tests/field-types.test.ts +187 -3
- package/tests/singleton.test.ts +14 -13
- package/tsconfig.tsbuildinfo +1 -1
package/src/fields/index.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function text<
|
|
|
97
97
|
|
|
98
98
|
return {
|
|
99
99
|
type: 'String',
|
|
100
|
-
modifiers: modifiers || undefined,
|
|
100
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
103
|
getTypeScriptType: () => {
|
|
@@ -145,22 +145,30 @@ export function integer<
|
|
|
145
145
|
: withMax
|
|
146
146
|
},
|
|
147
147
|
getPrismaType: (_fieldName: string) => {
|
|
148
|
-
const
|
|
148
|
+
const validation = options?.validation
|
|
149
|
+
const db = options?.db
|
|
150
|
+
const isRequired = validation?.isRequired
|
|
151
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
149
152
|
let modifiers = ''
|
|
150
153
|
|
|
151
154
|
// Optional modifier
|
|
152
|
-
if (
|
|
155
|
+
if (isNullable) {
|
|
153
156
|
modifiers += '?'
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
// Native type modifier (e.g., @db.SmallInt, @db.BigInt)
|
|
160
|
+
if (db?.nativeType) {
|
|
161
|
+
modifiers += ` @db.${db.nativeType}`
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
// Map modifier
|
|
157
|
-
if (
|
|
158
|
-
modifiers += ` @map("${
|
|
165
|
+
if (db?.map) {
|
|
166
|
+
modifiers += ` @map("${db.map}")`
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
return {
|
|
162
170
|
type: 'Int',
|
|
163
|
-
modifiers: modifiers || undefined,
|
|
171
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
164
172
|
}
|
|
165
173
|
},
|
|
166
174
|
getTypeScriptType: () => {
|
|
@@ -353,21 +361,28 @@ export function checkbox<
|
|
|
353
361
|
return z.boolean().optional().nullable()
|
|
354
362
|
},
|
|
355
363
|
getPrismaType: (_fieldName: string) => {
|
|
364
|
+
const db = options?.db
|
|
356
365
|
const hasDefault = options?.defaultValue !== undefined
|
|
357
366
|
let modifiers = ''
|
|
358
367
|
|
|
368
|
+
// Nullable modifier - checkbox fields are non-nullable by default (must be true or false)
|
|
369
|
+
// Use db.isNullable: true to allow NULL values in the database
|
|
370
|
+
if (db?.isNullable === true) {
|
|
371
|
+
modifiers += '?'
|
|
372
|
+
}
|
|
373
|
+
|
|
359
374
|
if (hasDefault) {
|
|
360
|
-
modifiers
|
|
375
|
+
modifiers += ` @default(${options.defaultValue})`
|
|
361
376
|
}
|
|
362
377
|
|
|
363
378
|
// Map modifier
|
|
364
|
-
if (
|
|
365
|
-
modifiers += ` @map("${
|
|
379
|
+
if (db?.map) {
|
|
380
|
+
modifiers += ` @map("${db.map}")`
|
|
366
381
|
}
|
|
367
382
|
|
|
368
383
|
return {
|
|
369
384
|
type: 'Boolean',
|
|
370
|
-
modifiers: modifiers || undefined,
|
|
385
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
371
386
|
}
|
|
372
387
|
},
|
|
373
388
|
getTypeScriptType: () => {
|
|
@@ -392,26 +407,41 @@ export function timestamp<
|
|
|
392
407
|
return z.union([z.date(), z.iso.datetime()]).optional().nullable()
|
|
393
408
|
},
|
|
394
409
|
getPrismaType: (_fieldName: string) => {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
// Check for default value
|
|
398
|
-
if (
|
|
410
|
+
const db = options?.db
|
|
411
|
+
const hasDefaultNow =
|
|
399
412
|
options?.defaultValue &&
|
|
400
413
|
typeof options.defaultValue === 'object' &&
|
|
401
414
|
'kind' in options.defaultValue &&
|
|
402
415
|
options.defaultValue.kind === 'now'
|
|
403
|
-
|
|
404
|
-
|
|
416
|
+
|
|
417
|
+
// Nullability: explicit db.isNullable overrides the default (nullable unless @default(now()))
|
|
418
|
+
const isNullable = db?.isNullable ?? !hasDefaultNow
|
|
419
|
+
|
|
420
|
+
let modifiers = ''
|
|
421
|
+
|
|
422
|
+
// Optional modifier
|
|
423
|
+
if (isNullable) {
|
|
424
|
+
modifiers += '?'
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Default value
|
|
428
|
+
if (hasDefaultNow) {
|
|
429
|
+
modifiers += ' @default(now())'
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Native type modifier (e.g., @db.Timestamptz for PostgreSQL)
|
|
433
|
+
if (db?.nativeType) {
|
|
434
|
+
modifiers += ` @db.${db.nativeType}`
|
|
405
435
|
}
|
|
406
436
|
|
|
407
437
|
// Map modifier
|
|
408
|
-
if (
|
|
409
|
-
modifiers += ` @map("${
|
|
438
|
+
if (db?.map) {
|
|
439
|
+
modifiers += ` @map("${db.map}")`
|
|
410
440
|
}
|
|
411
441
|
|
|
412
442
|
return {
|
|
413
443
|
type: 'DateTime',
|
|
414
|
-
modifiers,
|
|
444
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
415
445
|
}
|
|
416
446
|
},
|
|
417
447
|
getTypeScriptType: () => {
|
|
@@ -623,8 +653,10 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
623
653
|
hooks: {
|
|
624
654
|
// Hash password before writing to database
|
|
625
655
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
626
|
-
resolveInput: async ({
|
|
656
|
+
resolveInput: async ({ inputData, fieldKey }: { inputData: any; fieldKey: string }) => {
|
|
627
657
|
// Skip if undefined or null (allows partial updates)
|
|
658
|
+
const inputValue = inputData[fieldKey]
|
|
659
|
+
console.log('Password resolveInput called with value:', inputValue)
|
|
628
660
|
if (inputValue === undefined || inputValue === null) {
|
|
629
661
|
return inputValue
|
|
630
662
|
}
|
|
@@ -640,7 +672,7 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
640
672
|
}
|
|
641
673
|
|
|
642
674
|
// Hash the password
|
|
643
|
-
return await hashPassword(inputValue)
|
|
675
|
+
return (await hashPassword(inputValue)).toString()
|
|
644
676
|
},
|
|
645
677
|
// Wrap password with HashedPassword class after reading from database
|
|
646
678
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field builder hooks must be generic
|
|
@@ -687,22 +719,30 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
687
719
|
}
|
|
688
720
|
},
|
|
689
721
|
getPrismaType: (_fieldName: string) => {
|
|
690
|
-
const
|
|
722
|
+
const validation = options?.validation
|
|
723
|
+
const db = options?.db
|
|
724
|
+
const isRequired = validation?.isRequired
|
|
725
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
691
726
|
let modifiers = ''
|
|
692
727
|
|
|
693
728
|
// Optional modifier
|
|
694
|
-
if (
|
|
729
|
+
if (isNullable) {
|
|
695
730
|
modifiers += '?'
|
|
696
731
|
}
|
|
697
732
|
|
|
733
|
+
// Native type modifier (e.g., @db.Text)
|
|
734
|
+
if (db?.nativeType) {
|
|
735
|
+
modifiers += ` @db.${db.nativeType}`
|
|
736
|
+
}
|
|
737
|
+
|
|
698
738
|
// Map modifier
|
|
699
|
-
if (
|
|
700
|
-
modifiers += ` @map("${
|
|
739
|
+
if (db?.map) {
|
|
740
|
+
modifiers += ` @map("${db.map}")`
|
|
701
741
|
}
|
|
702
742
|
|
|
703
743
|
return {
|
|
704
744
|
type: 'String',
|
|
705
|
-
modifiers: modifiers || undefined,
|
|
745
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
706
746
|
}
|
|
707
747
|
},
|
|
708
748
|
getTypeScriptType: () => {
|
|
@@ -716,6 +756,11 @@ export function password<TTypeInfo extends import('../config/types.js').TypeInfo
|
|
|
716
756
|
}
|
|
717
757
|
}
|
|
718
758
|
|
|
759
|
+
/**
|
|
760
|
+
* Valid Prisma enum value pattern: starts with a letter, followed by letters, digits, or underscores
|
|
761
|
+
*/
|
|
762
|
+
const PRISMA_ENUM_VALUE_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/
|
|
763
|
+
|
|
719
764
|
/**
|
|
720
765
|
* Select field (enum-like)
|
|
721
766
|
*/
|
|
@@ -726,6 +771,20 @@ export function select<
|
|
|
726
771
|
throw new Error('Select field must have at least one option')
|
|
727
772
|
}
|
|
728
773
|
|
|
774
|
+
const isNativeEnum = options.db?.type === 'enum'
|
|
775
|
+
|
|
776
|
+
if (isNativeEnum) {
|
|
777
|
+
const invalidValues = options.options
|
|
778
|
+
.map((opt) => opt.value)
|
|
779
|
+
.filter((value) => !PRISMA_ENUM_VALUE_PATTERN.test(value))
|
|
780
|
+
|
|
781
|
+
if (invalidValues.length > 0) {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`Enum select field values must be valid Prisma identifiers (letters, numbers, and underscores, starting with a letter). Invalid values: ${invalidValues.join(', ')}`,
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
729
788
|
return {
|
|
730
789
|
type: 'select',
|
|
731
790
|
...options,
|
|
@@ -741,10 +800,39 @@ export function select<
|
|
|
741
800
|
|
|
742
801
|
return schema
|
|
743
802
|
},
|
|
744
|
-
getPrismaType: (
|
|
803
|
+
getPrismaType: (fieldName: string, _provider?: string, listName?: string) => {
|
|
745
804
|
const isRequired = options.validation?.isRequired
|
|
746
805
|
let modifiers = ''
|
|
747
806
|
|
|
807
|
+
if (isNativeEnum) {
|
|
808
|
+
// Derive enum name from list name + field name in PascalCase
|
|
809
|
+
const capitalizedField = fieldName.charAt(0).toUpperCase() + fieldName.slice(1)
|
|
810
|
+
const enumName = listName ? `${listName}${capitalizedField}` : capitalizedField
|
|
811
|
+
|
|
812
|
+
// Required fields don't get the ? modifier
|
|
813
|
+
if (!isRequired) {
|
|
814
|
+
modifiers = '?'
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Add default value if provided (no quotes for enum values)
|
|
818
|
+
if (options.defaultValue !== undefined) {
|
|
819
|
+
modifiers = ` @default(${options.defaultValue})`
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Map modifier
|
|
823
|
+
if (options.db?.map) {
|
|
824
|
+
modifiers += ` @map("${options.db.map}")`
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return {
|
|
828
|
+
type: enumName,
|
|
829
|
+
modifiers: modifiers || undefined,
|
|
830
|
+
enumValues: options.options.map((opt) => opt.value),
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// String type (default)
|
|
835
|
+
|
|
748
836
|
// Required fields don't get the ? modifier
|
|
749
837
|
if (!isRequired) {
|
|
750
838
|
modifiers = '?'
|
|
@@ -766,7 +854,7 @@ export function select<
|
|
|
766
854
|
}
|
|
767
855
|
},
|
|
768
856
|
getTypeScriptType: () => {
|
|
769
|
-
// Generate union type from options
|
|
857
|
+
// Generate union type from options (same for both string and enum db types)
|
|
770
858
|
const unionType = options.options.map((opt) => `'${opt.value}'`).join(' | ')
|
|
771
859
|
|
|
772
860
|
return {
|
|
@@ -888,22 +976,30 @@ export function json<
|
|
|
888
976
|
}
|
|
889
977
|
},
|
|
890
978
|
getPrismaType: (_fieldName: string) => {
|
|
891
|
-
const
|
|
979
|
+
const validation = options?.validation
|
|
980
|
+
const db = options?.db
|
|
981
|
+
const isRequired = validation?.isRequired
|
|
982
|
+
const isNullable = db?.isNullable ?? !isRequired
|
|
892
983
|
let modifiers = ''
|
|
893
984
|
|
|
894
985
|
// Optional modifier
|
|
895
|
-
if (
|
|
986
|
+
if (isNullable) {
|
|
896
987
|
modifiers += '?'
|
|
897
988
|
}
|
|
898
989
|
|
|
990
|
+
// Native type modifier
|
|
991
|
+
if (db?.nativeType) {
|
|
992
|
+
modifiers += ` @db.${db.nativeType}`
|
|
993
|
+
}
|
|
994
|
+
|
|
899
995
|
// Map modifier
|
|
900
|
-
if (
|
|
901
|
-
modifiers += ` @map("${
|
|
996
|
+
if (db?.map) {
|
|
997
|
+
modifiers += ` @map("${db.map}")`
|
|
902
998
|
}
|
|
903
999
|
|
|
904
1000
|
return {
|
|
905
1001
|
type: 'Json',
|
|
906
|
-
modifiers: modifiers || undefined,
|
|
1002
|
+
modifiers: modifiers.trimStart() || undefined,
|
|
907
1003
|
}
|
|
908
1004
|
},
|
|
909
1005
|
getTypeScriptType: () => {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { select } from './index.js'
|
|
3
|
+
|
|
4
|
+
describe('select field builder', () => {
|
|
5
|
+
describe('string type (default)', () => {
|
|
6
|
+
it('should throw when no options are provided', () => {
|
|
7
|
+
expect(() => select({ options: [] })).toThrow('Select field must have at least one option')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should return String prisma type for default select field', () => {
|
|
11
|
+
const field = select({
|
|
12
|
+
options: [
|
|
13
|
+
{ label: 'Draft', value: 'draft' },
|
|
14
|
+
{ label: 'Published', value: 'published' },
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
19
|
+
expect(result.type).toBe('String')
|
|
20
|
+
expect(result.enumValues).toBeUndefined()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should add ? modifier for optional string select', () => {
|
|
24
|
+
const field = select({
|
|
25
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
29
|
+
expect(result.modifiers).toBe('?')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should not add ? modifier for required string select', () => {
|
|
33
|
+
const field = select({
|
|
34
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
35
|
+
validation: { isRequired: true },
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
39
|
+
expect(result.modifiers).toBeUndefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should generate quoted default value for string select', () => {
|
|
43
|
+
const field = select({
|
|
44
|
+
options: [
|
|
45
|
+
{ label: 'Draft', value: 'draft' },
|
|
46
|
+
{ label: 'Published', value: 'published' },
|
|
47
|
+
],
|
|
48
|
+
defaultValue: 'draft',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
52
|
+
expect(result.modifiers).toBe(' @default("draft")')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should generate union TypeScript type from options', () => {
|
|
56
|
+
const field = select({
|
|
57
|
+
options: [
|
|
58
|
+
{ label: 'Draft', value: 'draft' },
|
|
59
|
+
{ label: 'Published', value: 'published' },
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const result = field.getTypeScriptType!()
|
|
64
|
+
expect(result.type).toBe("'draft' | 'published'")
|
|
65
|
+
expect(result.optional).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should mark TypeScript type as non-optional when required', () => {
|
|
69
|
+
const field = select({
|
|
70
|
+
options: [
|
|
71
|
+
{ label: 'Draft', value: 'draft' },
|
|
72
|
+
{ label: 'Published', value: 'published' },
|
|
73
|
+
],
|
|
74
|
+
validation: { isRequired: true },
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const result = field.getTypeScriptType!()
|
|
78
|
+
expect(result.optional).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('enum type (db.type: enum)', () => {
|
|
83
|
+
it('should throw for values that are not valid Prisma identifiers (hyphens)', () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
select({
|
|
86
|
+
options: [{ label: 'In Progress', value: 'in-progress' }],
|
|
87
|
+
db: { type: 'enum' },
|
|
88
|
+
}),
|
|
89
|
+
).toThrow(/valid Prisma identifiers/)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should throw for values starting with a digit', () => {
|
|
93
|
+
expect(() =>
|
|
94
|
+
select({
|
|
95
|
+
options: [{ label: 'First', value: '1st' }],
|
|
96
|
+
db: { type: 'enum' },
|
|
97
|
+
}),
|
|
98
|
+
).toThrow(/valid Prisma identifiers/)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should throw for values with spaces', () => {
|
|
102
|
+
expect(() =>
|
|
103
|
+
select({
|
|
104
|
+
options: [{ label: 'In Progress', value: 'in progress' }],
|
|
105
|
+
db: { type: 'enum' },
|
|
106
|
+
}),
|
|
107
|
+
).toThrow(/valid Prisma identifiers/)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should accept values with underscores', () => {
|
|
111
|
+
expect(() =>
|
|
112
|
+
select({
|
|
113
|
+
options: [
|
|
114
|
+
{ label: 'In Progress', value: 'in_progress' },
|
|
115
|
+
{ label: 'Done', value: 'done' },
|
|
116
|
+
],
|
|
117
|
+
db: { type: 'enum' },
|
|
118
|
+
}),
|
|
119
|
+
).not.toThrow()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should return derived enum name from listName + fieldName', () => {
|
|
123
|
+
const field = select({
|
|
124
|
+
options: [
|
|
125
|
+
{ label: 'Draft', value: 'draft' },
|
|
126
|
+
{ label: 'Published', value: 'published' },
|
|
127
|
+
],
|
|
128
|
+
db: { type: 'enum' },
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
132
|
+
expect(result.type).toBe('PostStatus')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should capitalize fieldName when deriving enum name', () => {
|
|
136
|
+
const field = select({
|
|
137
|
+
options: [
|
|
138
|
+
{ label: 'Article', value: 'article' },
|
|
139
|
+
{ label: 'Video', value: 'video' },
|
|
140
|
+
],
|
|
141
|
+
db: { type: 'enum' },
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const result = field.getPrismaType!('contentType', 'sqlite', 'Post')
|
|
145
|
+
expect(result.type).toBe('PostContentType')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should fall back to capitalized fieldName when listName is not provided', () => {
|
|
149
|
+
const field = select({
|
|
150
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
151
|
+
db: { type: 'enum' },
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const result = field.getPrismaType!('status')
|
|
155
|
+
expect(result.type).toBe('Status')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should return enumValues in getPrismaType result', () => {
|
|
159
|
+
const field = select({
|
|
160
|
+
options: [
|
|
161
|
+
{ label: 'Draft', value: 'draft' },
|
|
162
|
+
{ label: 'Published', value: 'published' },
|
|
163
|
+
],
|
|
164
|
+
db: { type: 'enum' },
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
168
|
+
expect(result.enumValues).toEqual(['draft', 'published'])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should add ? modifier for optional enum field', () => {
|
|
172
|
+
const field = select({
|
|
173
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
174
|
+
db: { type: 'enum' },
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
178
|
+
expect(result.modifiers).toBe('?')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should not add ? modifier for required enum field', () => {
|
|
182
|
+
const field = select({
|
|
183
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
184
|
+
db: { type: 'enum' },
|
|
185
|
+
validation: { isRequired: true },
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
189
|
+
expect(result.modifiers).toBeUndefined()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should generate unquoted default value for enum field', () => {
|
|
193
|
+
const field = select({
|
|
194
|
+
options: [
|
|
195
|
+
{ label: 'Draft', value: 'draft' },
|
|
196
|
+
{ label: 'Published', value: 'published' },
|
|
197
|
+
],
|
|
198
|
+
db: { type: 'enum' },
|
|
199
|
+
defaultValue: 'draft',
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
203
|
+
expect(result.modifiers).toBe(' @default(draft)')
|
|
204
|
+
// Explicitly check there are no quotes
|
|
205
|
+
expect(result.modifiers).not.toContain('"')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should include @map modifier for enum field with map option', () => {
|
|
209
|
+
const field = select({
|
|
210
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
211
|
+
db: { type: 'enum', map: 'post_status' },
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const result = field.getPrismaType!('status', 'sqlite', 'Post')
|
|
215
|
+
expect(result.modifiers).toContain('@map("post_status")')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('should generate same union TypeScript type as string select', () => {
|
|
219
|
+
const enumField = select({
|
|
220
|
+
options: [
|
|
221
|
+
{ label: 'Draft', value: 'draft' },
|
|
222
|
+
{ label: 'Published', value: 'published' },
|
|
223
|
+
],
|
|
224
|
+
db: { type: 'enum' },
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const stringField = select({
|
|
228
|
+
options: [
|
|
229
|
+
{ label: 'Draft', value: 'draft' },
|
|
230
|
+
{ label: 'Published', value: 'published' },
|
|
231
|
+
],
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
expect(enumField.getTypeScriptType!()).toEqual(stringField.getTypeScriptType!())
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
})
|
|
@@ -49,6 +49,22 @@ describe('Zod Schema Generation', () => {
|
|
|
49
49
|
expect(schema).toBeDefined()
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
+
it('should generate schema for enum select field', () => {
|
|
53
|
+
const fields: Record<string, FieldConfig> = {
|
|
54
|
+
status: select({
|
|
55
|
+
options: [
|
|
56
|
+
{ label: 'Draft', value: 'draft' },
|
|
57
|
+
{ label: 'Published', value: 'published' },
|
|
58
|
+
],
|
|
59
|
+
db: { type: 'enum' },
|
|
60
|
+
validation: { isRequired: true },
|
|
61
|
+
}),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const schema = generateZodSchema(fields, 'create')
|
|
65
|
+
expect(schema).toBeDefined()
|
|
66
|
+
})
|
|
67
|
+
|
|
52
68
|
it('should make fields optional in update mode', () => {
|
|
53
69
|
const fields: Record<string, FieldConfig> = {
|
|
54
70
|
name: text({ validation: { isRequired: true } }),
|
|
@@ -149,6 +165,41 @@ describe('Zod Schema Generation', () => {
|
|
|
149
165
|
}
|
|
150
166
|
})
|
|
151
167
|
|
|
168
|
+
it('should pass validation for valid enum select value', () => {
|
|
169
|
+
const fields: Record<string, FieldConfig> = {
|
|
170
|
+
status: select({
|
|
171
|
+
options: [
|
|
172
|
+
{ label: 'Draft', value: 'draft' },
|
|
173
|
+
{ label: 'Published', value: 'published' },
|
|
174
|
+
],
|
|
175
|
+
db: { type: 'enum' },
|
|
176
|
+
validation: { isRequired: true },
|
|
177
|
+
}),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result = validateWithZod({ status: 'draft' }, fields, 'create')
|
|
181
|
+
expect(result.success).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should fail validation for invalid enum select value', () => {
|
|
185
|
+
const fields: Record<string, FieldConfig> = {
|
|
186
|
+
status: select({
|
|
187
|
+
options: [
|
|
188
|
+
{ label: 'Draft', value: 'draft' },
|
|
189
|
+
{ label: 'Published', value: 'published' },
|
|
190
|
+
],
|
|
191
|
+
db: { type: 'enum' },
|
|
192
|
+
validation: { isRequired: true },
|
|
193
|
+
}),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = validateWithZod({ status: 'archived' }, fields, 'create')
|
|
197
|
+
expect(result.success).toBe(false)
|
|
198
|
+
if (!result.success) {
|
|
199
|
+
expect(result.errors.status).toBeDefined()
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
152
203
|
it('should skip system fields in validation', () => {
|
|
153
204
|
const fields: Record<string, FieldConfig> = {
|
|
154
205
|
id: text(),
|
|
@@ -7,6 +7,23 @@ describe('Relationship Access Control', () => {
|
|
|
7
7
|
session: null,
|
|
8
8
|
prisma: {},
|
|
9
9
|
db: {},
|
|
10
|
+
storage: {
|
|
11
|
+
uploadFile: async () => {
|
|
12
|
+
throw new Error('Not implemented')
|
|
13
|
+
},
|
|
14
|
+
uploadImage: async () => {
|
|
15
|
+
throw new Error('Not implemented')
|
|
16
|
+
},
|
|
17
|
+
deleteFile: async () => {
|
|
18
|
+
throw new Error('Not implemented')
|
|
19
|
+
},
|
|
20
|
+
deleteImage: async () => {
|
|
21
|
+
throw new Error('Not implemented')
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
plugins: {},
|
|
25
|
+
_isSudo: false,
|
|
26
|
+
_resolveOutputCounter: { depth: 0 },
|
|
10
27
|
}
|
|
11
28
|
|
|
12
29
|
describe('getRelatedListConfig', () => {
|