@opensaas/stack-core 0.1.6 → 0.3.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 +208 -0
- package/CLAUDE.md +46 -1
- package/dist/access/engine.d.ts +15 -8
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +23 -2
- package/dist/access/engine.js.map +1 -1
- package/dist/access/engine.test.d.ts +2 -0
- package/dist/access/engine.test.d.ts.map +1 -0
- package/dist/access/engine.test.js +125 -0
- package/dist/access/engine.test.js.map +1 -0
- package/dist/access/types.d.ts +40 -9
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +38 -18
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +34 -14
- package/dist/config/index.js.map +1 -1
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +6 -0
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +128 -21
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts +14 -2
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +243 -100
- package/dist/context/index.js.map +1 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +9 -8
- package/dist/fields/index.js.map +1 -1
- package/dist/hooks/index.d.ts +28 -12
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +16 -0
- package/dist/hooks/index.js.map +1 -1
- package/package.json +3 -4
- package/src/access/engine.test.ts +145 -0
- package/src/access/engine.ts +35 -11
- package/src/access/types.ts +39 -8
- package/src/config/index.ts +46 -19
- package/src/config/plugin-engine.ts +7 -0
- package/src/config/types.ts +149 -18
- package/src/context/index.ts +298 -110
- package/src/fields/index.ts +8 -7
- package/src/hooks/index.ts +63 -20
- package/tests/context.test.ts +38 -6
- package/tests/field-types.test.ts +728 -0
- package/tests/password-type-distribution.test.ts +0 -1
- package/tests/password-types.test.ts +0 -1
- package/tests/plugin-engine.test.ts +1102 -0
- package/tests/sudo.test.ts +405 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
text,
|
|
4
|
+
integer,
|
|
5
|
+
checkbox,
|
|
6
|
+
timestamp,
|
|
7
|
+
password,
|
|
8
|
+
select,
|
|
9
|
+
relationship,
|
|
10
|
+
json,
|
|
11
|
+
} from '../src/fields/index.js'
|
|
12
|
+
|
|
13
|
+
describe('Field Types', () => {
|
|
14
|
+
describe('text field', () => {
|
|
15
|
+
describe('getZodSchema', () => {
|
|
16
|
+
test('returns optional string schema for non-required field', () => {
|
|
17
|
+
const field = text()
|
|
18
|
+
const schema = field.getZodSchema('title', 'create')
|
|
19
|
+
|
|
20
|
+
expect(schema.safeParse('test').success).toBe(true)
|
|
21
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
22
|
+
expect(schema.safeParse(123).success).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('returns required string schema for required field in create mode', () => {
|
|
26
|
+
const field = text({ validation: { isRequired: true } })
|
|
27
|
+
const schema = field.getZodSchema('title', 'create')
|
|
28
|
+
|
|
29
|
+
expect(schema.safeParse('test').success).toBe(true)
|
|
30
|
+
expect(schema.safeParse('').success).toBe(false)
|
|
31
|
+
expect(schema.safeParse(undefined).success).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('returns optional schema for required field in update mode', () => {
|
|
35
|
+
const field = text({ validation: { isRequired: true } })
|
|
36
|
+
const schema = field.getZodSchema('title', 'update')
|
|
37
|
+
|
|
38
|
+
expect(schema.safeParse('test').success).toBe(true)
|
|
39
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
40
|
+
expect(schema.safeParse('').success).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('validates min length', () => {
|
|
44
|
+
const field = text({ validation: { length: { min: 5 } } })
|
|
45
|
+
const schema = field.getZodSchema('title', 'create')
|
|
46
|
+
|
|
47
|
+
expect(schema.safeParse('test').success).toBe(false)
|
|
48
|
+
expect(schema.safeParse('testing').success).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('validates max length', () => {
|
|
52
|
+
const field = text({ validation: { length: { max: 10 } } })
|
|
53
|
+
const schema = field.getZodSchema('title', 'create')
|
|
54
|
+
|
|
55
|
+
expect(schema.safeParse('short').success).toBe(true)
|
|
56
|
+
expect(schema.safeParse('this is way too long').success).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('validates min and max length together', () => {
|
|
60
|
+
const field = text({ validation: { length: { min: 3, max: 10 } } })
|
|
61
|
+
const schema = field.getZodSchema('title', 'create')
|
|
62
|
+
|
|
63
|
+
expect(schema.safeParse('ab').success).toBe(false)
|
|
64
|
+
expect(schema.safeParse('abc').success).toBe(true)
|
|
65
|
+
expect(schema.safeParse('abcdefghij').success).toBe(true)
|
|
66
|
+
expect(schema.safeParse('abcdefghijk').success).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('includes formatted field name in error messages', () => {
|
|
70
|
+
const field = text({ validation: { isRequired: true } })
|
|
71
|
+
const schema = field.getZodSchema('firstName', 'create')
|
|
72
|
+
const result = schema.safeParse('')
|
|
73
|
+
|
|
74
|
+
expect(result.success).toBe(false)
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
expect(result.error.issues[0].message).toContain('First Name')
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('getPrismaType', () => {
|
|
82
|
+
test('returns String type for basic text field', () => {
|
|
83
|
+
const field = text()
|
|
84
|
+
const prismaType = field.getPrismaType('title')
|
|
85
|
+
|
|
86
|
+
expect(prismaType.type).toBe('String')
|
|
87
|
+
expect(prismaType.modifiers).toBe('?')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('returns required String for required field', () => {
|
|
91
|
+
const field = text({ validation: { isRequired: true } })
|
|
92
|
+
const prismaType = field.getPrismaType('title')
|
|
93
|
+
|
|
94
|
+
expect(prismaType.type).toBe('String')
|
|
95
|
+
expect(prismaType.modifiers).toBeUndefined()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('includes @unique modifier', () => {
|
|
99
|
+
const field = text({ isIndexed: 'unique' })
|
|
100
|
+
const prismaType = field.getPrismaType('email')
|
|
101
|
+
|
|
102
|
+
expect(prismaType.type).toBe('String')
|
|
103
|
+
expect(prismaType.modifiers).toContain('@unique')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('includes @index modifier', () => {
|
|
107
|
+
const field = text({ isIndexed: true })
|
|
108
|
+
const prismaType = field.getPrismaType('slug')
|
|
109
|
+
|
|
110
|
+
expect(prismaType.type).toBe('String')
|
|
111
|
+
expect(prismaType.modifiers).toContain('@index')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('getTypeScriptType', () => {
|
|
116
|
+
test('returns optional string type for non-required field', () => {
|
|
117
|
+
const field = text()
|
|
118
|
+
const tsType = field.getTypeScriptType()
|
|
119
|
+
|
|
120
|
+
expect(tsType.type).toBe('string')
|
|
121
|
+
expect(tsType.optional).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('returns required string type for required field', () => {
|
|
125
|
+
const field = text({ validation: { isRequired: true } })
|
|
126
|
+
const tsType = field.getTypeScriptType()
|
|
127
|
+
|
|
128
|
+
expect(tsType.type).toBe('string')
|
|
129
|
+
expect(tsType.optional).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('integer field', () => {
|
|
135
|
+
describe('getZodSchema', () => {
|
|
136
|
+
test('returns optional number schema for non-required field', () => {
|
|
137
|
+
const field = integer()
|
|
138
|
+
const schema = field.getZodSchema('age', 'create')
|
|
139
|
+
|
|
140
|
+
expect(schema.safeParse(25).success).toBe(true)
|
|
141
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
142
|
+
expect(schema.safeParse('25').success).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('returns required number schema for required field in create mode', () => {
|
|
146
|
+
const field = integer({ validation: { isRequired: true } })
|
|
147
|
+
const schema = field.getZodSchema('age', 'create')
|
|
148
|
+
|
|
149
|
+
expect(schema.safeParse(25).success).toBe(true)
|
|
150
|
+
expect(schema.safeParse(undefined).success).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('returns optional schema for required field in update mode', () => {
|
|
154
|
+
const field = integer({ validation: { isRequired: true } })
|
|
155
|
+
const schema = field.getZodSchema('age', 'update')
|
|
156
|
+
|
|
157
|
+
expect(schema.safeParse(25).success).toBe(true)
|
|
158
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('validates min value', () => {
|
|
162
|
+
const field = integer({ validation: { min: 0 } })
|
|
163
|
+
const schema = field.getZodSchema('age', 'create')
|
|
164
|
+
|
|
165
|
+
expect(schema.safeParse(-1).success).toBe(false)
|
|
166
|
+
expect(schema.safeParse(0).success).toBe(true)
|
|
167
|
+
expect(schema.safeParse(1).success).toBe(true)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('validates max value', () => {
|
|
171
|
+
const field = integer({ validation: { max: 100 } })
|
|
172
|
+
const schema = field.getZodSchema('age', 'create')
|
|
173
|
+
|
|
174
|
+
expect(schema.safeParse(99).success).toBe(true)
|
|
175
|
+
expect(schema.safeParse(100).success).toBe(true)
|
|
176
|
+
expect(schema.safeParse(101).success).toBe(false)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('validates min and max together', () => {
|
|
180
|
+
const field = integer({ validation: { min: 18, max: 65 } })
|
|
181
|
+
const schema = field.getZodSchema('age', 'create')
|
|
182
|
+
|
|
183
|
+
expect(schema.safeParse(17).success).toBe(false)
|
|
184
|
+
expect(schema.safeParse(18).success).toBe(true)
|
|
185
|
+
expect(schema.safeParse(65).success).toBe(true)
|
|
186
|
+
expect(schema.safeParse(66).success).toBe(false)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('getPrismaType', () => {
|
|
191
|
+
test('returns Int type for basic integer field', () => {
|
|
192
|
+
const field = integer()
|
|
193
|
+
const prismaType = field.getPrismaType('age')
|
|
194
|
+
|
|
195
|
+
expect(prismaType.type).toBe('Int')
|
|
196
|
+
expect(prismaType.modifiers).toBe('?')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('returns required Int for required field', () => {
|
|
200
|
+
const field = integer({ validation: { isRequired: true } })
|
|
201
|
+
const prismaType = field.getPrismaType('age')
|
|
202
|
+
|
|
203
|
+
expect(prismaType.type).toBe('Int')
|
|
204
|
+
expect(prismaType.modifiers).toBeUndefined()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('getTypeScriptType', () => {
|
|
209
|
+
test('returns optional number type for non-required field', () => {
|
|
210
|
+
const field = integer()
|
|
211
|
+
const tsType = field.getTypeScriptType()
|
|
212
|
+
|
|
213
|
+
expect(tsType.type).toBe('number')
|
|
214
|
+
expect(tsType.optional).toBe(true)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('returns required number type for required field', () => {
|
|
218
|
+
const field = integer({ validation: { isRequired: true } })
|
|
219
|
+
const tsType = field.getTypeScriptType()
|
|
220
|
+
|
|
221
|
+
expect(tsType.type).toBe('number')
|
|
222
|
+
expect(tsType.optional).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('checkbox field', () => {
|
|
228
|
+
describe('getZodSchema', () => {
|
|
229
|
+
test('returns optional boolean schema', () => {
|
|
230
|
+
const field = checkbox()
|
|
231
|
+
const schema = field.getZodSchema('isActive', 'create')
|
|
232
|
+
|
|
233
|
+
expect(schema.safeParse(true).success).toBe(true)
|
|
234
|
+
expect(schema.safeParse(false).success).toBe(true)
|
|
235
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
236
|
+
expect(schema.safeParse('true').success).toBe(false)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('getPrismaType', () => {
|
|
241
|
+
test('returns Boolean type without default', () => {
|
|
242
|
+
const field = checkbox()
|
|
243
|
+
const prismaType = field.getPrismaType('isActive')
|
|
244
|
+
|
|
245
|
+
expect(prismaType.type).toBe('Boolean')
|
|
246
|
+
expect(prismaType.modifiers).toBeUndefined()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('returns Boolean type with default true', () => {
|
|
250
|
+
const field = checkbox({ defaultValue: true })
|
|
251
|
+
const prismaType = field.getPrismaType('isActive')
|
|
252
|
+
|
|
253
|
+
expect(prismaType.type).toBe('Boolean')
|
|
254
|
+
expect(prismaType.modifiers).toBe(' @default(true)')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('returns Boolean type with default false', () => {
|
|
258
|
+
const field = checkbox({ defaultValue: false })
|
|
259
|
+
const prismaType = field.getPrismaType('isActive')
|
|
260
|
+
|
|
261
|
+
expect(prismaType.type).toBe('Boolean')
|
|
262
|
+
expect(prismaType.modifiers).toBe(' @default(false)')
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe('getTypeScriptType', () => {
|
|
267
|
+
test('returns optional boolean type without default', () => {
|
|
268
|
+
const field = checkbox()
|
|
269
|
+
const tsType = field.getTypeScriptType()
|
|
270
|
+
|
|
271
|
+
expect(tsType.type).toBe('boolean')
|
|
272
|
+
expect(tsType.optional).toBe(true)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('returns required boolean type with default', () => {
|
|
276
|
+
const field = checkbox({ defaultValue: false })
|
|
277
|
+
const tsType = field.getTypeScriptType()
|
|
278
|
+
|
|
279
|
+
expect(tsType.type).toBe('boolean')
|
|
280
|
+
expect(tsType.optional).toBe(false)
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('timestamp field', () => {
|
|
286
|
+
describe('getZodSchema', () => {
|
|
287
|
+
test('accepts Date objects and ISO datetime strings', () => {
|
|
288
|
+
const field = timestamp()
|
|
289
|
+
const schema = field.getZodSchema('createdAt', 'create')
|
|
290
|
+
|
|
291
|
+
expect(schema.safeParse(new Date()).success).toBe(true)
|
|
292
|
+
expect(schema.safeParse('2024-01-01T00:00:00Z').success).toBe(true)
|
|
293
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
294
|
+
expect(schema.safeParse('invalid').success).toBe(false)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
describe('getPrismaType', () => {
|
|
299
|
+
test('returns optional DateTime type', () => {
|
|
300
|
+
const field = timestamp()
|
|
301
|
+
const prismaType = field.getPrismaType('createdAt')
|
|
302
|
+
|
|
303
|
+
expect(prismaType.type).toBe('DateTime')
|
|
304
|
+
expect(prismaType.modifiers).toBe('?')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('returns DateTime type with @default(now())', () => {
|
|
308
|
+
const field = timestamp({ defaultValue: { kind: 'now' } })
|
|
309
|
+
const prismaType = field.getPrismaType('createdAt')
|
|
310
|
+
|
|
311
|
+
expect(prismaType.type).toBe('DateTime')
|
|
312
|
+
expect(prismaType.modifiers).toBe(' @default(now())')
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
describe('getTypeScriptType', () => {
|
|
317
|
+
test('returns optional Date type without default', () => {
|
|
318
|
+
const field = timestamp()
|
|
319
|
+
const tsType = field.getTypeScriptType()
|
|
320
|
+
|
|
321
|
+
expect(tsType.type).toBe('Date')
|
|
322
|
+
expect(tsType.optional).toBe(true)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test('returns required Date type with default now', () => {
|
|
326
|
+
const field = timestamp({ defaultValue: { kind: 'now' } })
|
|
327
|
+
const tsType = field.getTypeScriptType()
|
|
328
|
+
|
|
329
|
+
expect(tsType.type).toBe('Date')
|
|
330
|
+
expect(tsType.optional).toBe(false)
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
describe('password field', () => {
|
|
336
|
+
describe('getZodSchema', () => {
|
|
337
|
+
test('returns required string schema for required field in create mode', () => {
|
|
338
|
+
const field = password({ validation: { isRequired: true } })
|
|
339
|
+
const schema = field.getZodSchema('password', 'create')
|
|
340
|
+
|
|
341
|
+
expect(schema.safeParse('secret123').success).toBe(true)
|
|
342
|
+
expect(schema.safeParse('').success).toBe(false)
|
|
343
|
+
expect(schema.safeParse(undefined).success).toBe(false)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('returns optional schema for required field in update mode', () => {
|
|
347
|
+
const field = password({ validation: { isRequired: true } })
|
|
348
|
+
const schema = field.getZodSchema('password', 'update')
|
|
349
|
+
|
|
350
|
+
expect(schema.safeParse('newpassword').success).toBe(true)
|
|
351
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
352
|
+
expect(schema.safeParse('').success).toBe(false)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('returns optional string schema for non-required field', () => {
|
|
356
|
+
const field = password()
|
|
357
|
+
const schema = field.getZodSchema('password', 'create')
|
|
358
|
+
|
|
359
|
+
expect(schema.safeParse('secret123').success).toBe(true)
|
|
360
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
describe('getPrismaType', () => {
|
|
365
|
+
test('returns String type for password field', () => {
|
|
366
|
+
const field = password()
|
|
367
|
+
const prismaType = field.getPrismaType('password')
|
|
368
|
+
|
|
369
|
+
expect(prismaType.type).toBe('String')
|
|
370
|
+
expect(prismaType.modifiers).toBe('?')
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('returns required String for required password', () => {
|
|
374
|
+
const field = password({ validation: { isRequired: true } })
|
|
375
|
+
const prismaType = field.getPrismaType('password')
|
|
376
|
+
|
|
377
|
+
expect(prismaType.type).toBe('String')
|
|
378
|
+
expect(prismaType.modifiers).toBeUndefined()
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('getTypeScriptType', () => {
|
|
383
|
+
test('returns optional string type', () => {
|
|
384
|
+
const field = password()
|
|
385
|
+
const tsType = field.getTypeScriptType()
|
|
386
|
+
|
|
387
|
+
expect(tsType.type).toBe('string')
|
|
388
|
+
expect(tsType.optional).toBe(true)
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe('hooks', () => {
|
|
393
|
+
test('has resolveInput hook defined', () => {
|
|
394
|
+
const field = password()
|
|
395
|
+
|
|
396
|
+
expect(field.hooks).toBeDefined()
|
|
397
|
+
expect(field.hooks?.resolveInput).toBeDefined()
|
|
398
|
+
expect(typeof field.hooks?.resolveInput).toBe('function')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('has resolveOutput hook defined', () => {
|
|
402
|
+
const field = password()
|
|
403
|
+
|
|
404
|
+
expect(field.hooks).toBeDefined()
|
|
405
|
+
expect(field.hooks?.resolveOutput).toBeDefined()
|
|
406
|
+
expect(typeof field.hooks?.resolveOutput).toBe('function')
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
describe('typePatch', () => {
|
|
411
|
+
test('has type patch configured', () => {
|
|
412
|
+
const field = password()
|
|
413
|
+
|
|
414
|
+
expect(field.typePatch).toBeDefined()
|
|
415
|
+
expect(field.typePatch?.resultType).toBe("import('@opensaas/stack-core').HashedPassword")
|
|
416
|
+
expect(field.typePatch?.patchScope).toBe('scalars-only')
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
describe('select field', () => {
|
|
422
|
+
describe('constructor', () => {
|
|
423
|
+
test('throws error when no options provided', () => {
|
|
424
|
+
expect(() => {
|
|
425
|
+
// @ts-expect-error - Testing invalid input
|
|
426
|
+
select()
|
|
427
|
+
}).toThrow('option')
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('throws error when empty options array provided', () => {
|
|
431
|
+
expect(() => {
|
|
432
|
+
select({ options: [] })
|
|
433
|
+
}).toThrow('Select field must have at least one option')
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test('accepts valid options', () => {
|
|
437
|
+
const field = select({
|
|
438
|
+
options: [
|
|
439
|
+
{ label: 'Draft', value: 'draft' },
|
|
440
|
+
{ label: 'Published', value: 'published' },
|
|
441
|
+
],
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
expect(field.options).toHaveLength(2)
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
describe('getZodSchema', () => {
|
|
449
|
+
test('validates against enum values', () => {
|
|
450
|
+
const field = select({
|
|
451
|
+
options: [
|
|
452
|
+
{ label: 'Draft', value: 'draft' },
|
|
453
|
+
{ label: 'Published', value: 'published' },
|
|
454
|
+
],
|
|
455
|
+
})
|
|
456
|
+
const schema = field.getZodSchema('status', 'create')
|
|
457
|
+
|
|
458
|
+
expect(schema.safeParse('draft').success).toBe(true)
|
|
459
|
+
expect(schema.safeParse('published').success).toBe(true)
|
|
460
|
+
expect(schema.safeParse('invalid').success).toBe(false)
|
|
461
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
test('requires value when isRequired in create mode', () => {
|
|
465
|
+
const field = select({
|
|
466
|
+
options: [
|
|
467
|
+
{ label: 'Draft', value: 'draft' },
|
|
468
|
+
{ label: 'Published', value: 'published' },
|
|
469
|
+
],
|
|
470
|
+
validation: { isRequired: true },
|
|
471
|
+
})
|
|
472
|
+
const schema = field.getZodSchema('status', 'create')
|
|
473
|
+
|
|
474
|
+
expect(schema.safeParse('draft').success).toBe(true)
|
|
475
|
+
expect(schema.safeParse(undefined).success).toBe(false)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
test('allows undefined in update mode even when required', () => {
|
|
479
|
+
const field = select({
|
|
480
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
481
|
+
validation: { isRequired: true },
|
|
482
|
+
})
|
|
483
|
+
const schema = field.getZodSchema('status', 'update')
|
|
484
|
+
|
|
485
|
+
expect(schema.safeParse('draft').success).toBe(true)
|
|
486
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
describe('getPrismaType', () => {
|
|
491
|
+
test('returns String type with optional modifier', () => {
|
|
492
|
+
const field = select({
|
|
493
|
+
options: [{ label: 'Option', value: 'option' }],
|
|
494
|
+
})
|
|
495
|
+
const prismaType = field.getPrismaType('status')
|
|
496
|
+
|
|
497
|
+
expect(prismaType.type).toBe('String')
|
|
498
|
+
expect(prismaType.modifiers).toBe('?')
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
test('includes @default modifier when defaultValue provided', () => {
|
|
502
|
+
const field = select({
|
|
503
|
+
options: [
|
|
504
|
+
{ label: 'Draft', value: 'draft' },
|
|
505
|
+
{ label: 'Published', value: 'published' },
|
|
506
|
+
],
|
|
507
|
+
defaultValue: 'draft',
|
|
508
|
+
})
|
|
509
|
+
const prismaType = field.getPrismaType('status')
|
|
510
|
+
|
|
511
|
+
expect(prismaType.type).toBe('String')
|
|
512
|
+
expect(prismaType.modifiers).toBe(' @default("draft")')
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
describe('getTypeScriptType', () => {
|
|
517
|
+
test('returns union type from options', () => {
|
|
518
|
+
const field = select({
|
|
519
|
+
options: [
|
|
520
|
+
{ label: 'Draft', value: 'draft' },
|
|
521
|
+
{ label: 'Published', value: 'published' },
|
|
522
|
+
],
|
|
523
|
+
})
|
|
524
|
+
const tsType = field.getTypeScriptType()
|
|
525
|
+
|
|
526
|
+
expect(tsType.type).toBe("'draft' | 'published'")
|
|
527
|
+
expect(tsType.optional).toBe(true)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
test('returns required type when isRequired', () => {
|
|
531
|
+
const field = select({
|
|
532
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
533
|
+
validation: { isRequired: true },
|
|
534
|
+
})
|
|
535
|
+
const tsType = field.getTypeScriptType()
|
|
536
|
+
|
|
537
|
+
expect(tsType.optional).toBe(false)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
test('returns optional type when has defaultValue', () => {
|
|
541
|
+
const field = select({
|
|
542
|
+
options: [{ label: 'Draft', value: 'draft' }],
|
|
543
|
+
defaultValue: 'draft',
|
|
544
|
+
})
|
|
545
|
+
const tsType = field.getTypeScriptType()
|
|
546
|
+
|
|
547
|
+
expect(tsType.optional).toBe(true)
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
describe('relationship field', () => {
|
|
553
|
+
describe('constructor', () => {
|
|
554
|
+
test('throws error when no ref provided', () => {
|
|
555
|
+
expect(() => {
|
|
556
|
+
// @ts-expect-error - Testing invalid input
|
|
557
|
+
relationship()
|
|
558
|
+
}).toThrow('ref')
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
test('throws error when ref format is invalid', () => {
|
|
562
|
+
expect(() => {
|
|
563
|
+
relationship({ ref: 'InvalidFormat' })
|
|
564
|
+
}).toThrow('Invalid relationship ref format')
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
test('accepts valid ref format', () => {
|
|
568
|
+
const field = relationship({ ref: 'User.posts' })
|
|
569
|
+
|
|
570
|
+
expect(field.ref).toBe('User.posts')
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
test('accepts many option', () => {
|
|
574
|
+
const field = relationship({ ref: 'Post.author', many: true })
|
|
575
|
+
|
|
576
|
+
expect(field.ref).toBe('Post.author')
|
|
577
|
+
expect(field.many).toBe(true)
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
describe('json field', () => {
|
|
583
|
+
describe('getZodSchema', () => {
|
|
584
|
+
test('accepts any value for non-required field', () => {
|
|
585
|
+
const field = json()
|
|
586
|
+
const schema = field.getZodSchema('metadata', 'create')
|
|
587
|
+
|
|
588
|
+
expect(schema.safeParse({ key: 'value' }).success).toBe(true)
|
|
589
|
+
expect(schema.safeParse([1, 2, 3]).success).toBe(true)
|
|
590
|
+
expect(schema.safeParse('string').success).toBe(true)
|
|
591
|
+
expect(schema.safeParse(123).success).toBe(true)
|
|
592
|
+
expect(schema.safeParse(null).success).toBe(true)
|
|
593
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
test('accepts value for required field in create mode', () => {
|
|
597
|
+
const field = json({ validation: { isRequired: true } })
|
|
598
|
+
const schema = field.getZodSchema('metadata', 'create')
|
|
599
|
+
|
|
600
|
+
expect(schema.safeParse({ key: 'value' }).success).toBe(true)
|
|
601
|
+
// JSON field with isRequired still accepts undefined due to z.unknown() behavior
|
|
602
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
test('allows undefined for required field in update mode', () => {
|
|
606
|
+
const field = json({ validation: { isRequired: true } })
|
|
607
|
+
const schema = field.getZodSchema('metadata', 'update')
|
|
608
|
+
|
|
609
|
+
expect(schema.safeParse({ key: 'value' }).success).toBe(true)
|
|
610
|
+
expect(schema.safeParse(undefined).success).toBe(true)
|
|
611
|
+
})
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
describe('getPrismaType', () => {
|
|
615
|
+
test('returns Json type with optional modifier', () => {
|
|
616
|
+
const field = json()
|
|
617
|
+
const prismaType = field.getPrismaType('metadata')
|
|
618
|
+
|
|
619
|
+
expect(prismaType.type).toBe('Json')
|
|
620
|
+
expect(prismaType.modifiers).toBe('?')
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
test('returns required Json type for required field', () => {
|
|
624
|
+
const field = json({ validation: { isRequired: true } })
|
|
625
|
+
const prismaType = field.getPrismaType('metadata')
|
|
626
|
+
|
|
627
|
+
expect(prismaType.type).toBe('Json')
|
|
628
|
+
expect(prismaType.modifiers).toBeUndefined()
|
|
629
|
+
})
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
describe('getTypeScriptType', () => {
|
|
633
|
+
test('returns optional unknown type', () => {
|
|
634
|
+
const field = json()
|
|
635
|
+
const tsType = field.getTypeScriptType()
|
|
636
|
+
|
|
637
|
+
expect(tsType.type).toBe('unknown')
|
|
638
|
+
expect(tsType.optional).toBe(true)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test('returns required unknown type for required field', () => {
|
|
642
|
+
const field = json({ validation: { isRequired: true } })
|
|
643
|
+
const tsType = field.getTypeScriptType()
|
|
644
|
+
|
|
645
|
+
expect(tsType.type).toBe('unknown')
|
|
646
|
+
expect(tsType.optional).toBe(false)
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
describe('field name formatting', () => {
|
|
652
|
+
test('formats camelCase to human-readable', () => {
|
|
653
|
+
const field = text({ validation: { isRequired: true } })
|
|
654
|
+
const schema = field.getZodSchema('firstName', 'create')
|
|
655
|
+
const result = schema.safeParse('')
|
|
656
|
+
|
|
657
|
+
expect(result.success).toBe(false)
|
|
658
|
+
if (!result.success) {
|
|
659
|
+
expect(result.error.issues[0].message).toContain('First Name')
|
|
660
|
+
}
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
test('formats single word field names', () => {
|
|
664
|
+
const field = text({ validation: { isRequired: true } })
|
|
665
|
+
const schema = field.getZodSchema('email', 'create')
|
|
666
|
+
const result = schema.safeParse('')
|
|
667
|
+
|
|
668
|
+
expect(result.success).toBe(false)
|
|
669
|
+
if (!result.success) {
|
|
670
|
+
expect(result.error.issues[0].message).toContain('Email')
|
|
671
|
+
}
|
|
672
|
+
})
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
describe('edge cases', () => {
|
|
676
|
+
test('text field with only min length set', () => {
|
|
677
|
+
const field = text({ validation: { length: { min: 5 } } })
|
|
678
|
+
const schema = field.getZodSchema('field', 'create')
|
|
679
|
+
|
|
680
|
+
expect(schema.safeParse('1234').success).toBe(false)
|
|
681
|
+
expect(schema.safeParse('12345').success).toBe(true)
|
|
682
|
+
expect(schema.safeParse('a'.repeat(1000)).success).toBe(true)
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
test('text field with only max length set', () => {
|
|
686
|
+
const field = text({ validation: { length: { max: 10 } } })
|
|
687
|
+
const schema = field.getZodSchema('field', 'create')
|
|
688
|
+
|
|
689
|
+
expect(schema.safeParse('').success).toBe(true)
|
|
690
|
+
expect(schema.safeParse('short').success).toBe(true)
|
|
691
|
+
expect(schema.safeParse('way too long text').success).toBe(false)
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
test('integer field with zero as min value', () => {
|
|
695
|
+
const field = integer({ validation: { min: 0 } })
|
|
696
|
+
const schema = field.getZodSchema('count', 'create')
|
|
697
|
+
|
|
698
|
+
expect(schema.safeParse(-1).success).toBe(false)
|
|
699
|
+
expect(schema.safeParse(0).success).toBe(true)
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
test('integer field with negative min and max', () => {
|
|
703
|
+
const field = integer({ validation: { min: -100, max: -10 } })
|
|
704
|
+
const schema = field.getZodSchema('temperature', 'create')
|
|
705
|
+
|
|
706
|
+
expect(schema.safeParse(-101).success).toBe(false)
|
|
707
|
+
expect(schema.safeParse(-50).success).toBe(true)
|
|
708
|
+
expect(schema.safeParse(-9).success).toBe(false)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
test('select field with single option', () => {
|
|
712
|
+
const field = select({
|
|
713
|
+
options: [{ label: 'Only Option', value: 'only' }],
|
|
714
|
+
})
|
|
715
|
+
const schema = field.getZodSchema('choice', 'create')
|
|
716
|
+
|
|
717
|
+
expect(schema.safeParse('only').success).toBe(true)
|
|
718
|
+
expect(schema.safeParse('other').success).toBe(false)
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
test('relationship field with complex ref', () => {
|
|
722
|
+
const field = relationship({ ref: 'BlogPost.author' })
|
|
723
|
+
|
|
724
|
+
expect(field.ref).toBe('BlogPost.author')
|
|
725
|
+
expect(field.type).toBe('relationship')
|
|
726
|
+
})
|
|
727
|
+
})
|
|
728
|
+
})
|