@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.
@@ -110,6 +110,72 @@ describe('Field Types', () => {
110
110
  expect(prismaType.type).toBe('String')
111
111
  expect(prismaType.modifiers).toContain('@index')
112
112
  })
113
+
114
+ test('db.isNullable: true makes optional field explicitly nullable', () => {
115
+ const field = text({ db: { isNullable: true } })
116
+ const prismaType = field.getPrismaType('description')
117
+
118
+ expect(prismaType.type).toBe('String')
119
+ expect(prismaType.modifiers).toContain('?')
120
+ })
121
+
122
+ test('db.isNullable: false makes field non-nullable regardless of validation', () => {
123
+ const field = text({ db: { isNullable: false } })
124
+ const prismaType = field.getPrismaType('phoneNumber')
125
+
126
+ expect(prismaType.type).toBe('String')
127
+ // Non-nullable with no other modifiers → modifiers is undefined
128
+ expect(prismaType.modifiers).toBeUndefined()
129
+ })
130
+
131
+ test('db.isNullable: false on required field keeps it non-nullable', () => {
132
+ const field = text({ validation: { isRequired: true }, db: { isNullable: false } })
133
+ const prismaType = field.getPrismaType('title')
134
+
135
+ expect(prismaType.type).toBe('String')
136
+ // Non-nullable with no other modifiers → modifiers is undefined
137
+ expect(prismaType.modifiers).toBeUndefined()
138
+ })
139
+
140
+ test('db.isNullable: true on required field overrides to nullable', () => {
141
+ const field = text({ validation: { isRequired: true }, db: { isNullable: true } })
142
+ const prismaType = field.getPrismaType('title')
143
+
144
+ expect(prismaType.type).toBe('String')
145
+ expect(prismaType.modifiers).toContain('?')
146
+ })
147
+
148
+ test('db.nativeType generates @db. attribute', () => {
149
+ const field = text({ db: { nativeType: 'Text' } })
150
+ const prismaType = field.getPrismaType('medical')
151
+
152
+ expect(prismaType.type).toBe('String')
153
+ expect(prismaType.modifiers).toContain('@db.Text')
154
+ })
155
+
156
+ test('db.nativeType with nullable field includes both ? and @db. attribute', () => {
157
+ const field = text({ db: { isNullable: true, nativeType: 'Text' } })
158
+ const prismaType = field.getPrismaType('bio')
159
+
160
+ expect(prismaType.type).toBe('String')
161
+ expect(prismaType.modifiers).toBe('? @db.Text')
162
+ })
163
+
164
+ test('db.nativeType with non-nullable field excludes ? but includes @db. attribute', () => {
165
+ const field = text({ db: { isNullable: false, nativeType: 'Text' } })
166
+ const prismaType = field.getPrismaType('content')
167
+
168
+ expect(prismaType.type).toBe('String')
169
+ expect(prismaType.modifiers).toBe('@db.Text')
170
+ })
171
+
172
+ test('db.nativeType with required field generates non-nullable with @db. attribute', () => {
173
+ const field = text({ validation: { isRequired: true }, db: { nativeType: 'Text' } })
174
+ const prismaType = field.getPrismaType('content')
175
+
176
+ expect(prismaType.type).toBe('String')
177
+ expect(prismaType.modifiers).toBe('@db.Text')
178
+ })
113
179
  })
114
180
 
115
181
  describe('getTypeScriptType', () => {
@@ -203,6 +269,39 @@ describe('Field Types', () => {
203
269
  expect(prismaType.type).toBe('Int')
204
270
  expect(prismaType.modifiers).toBeUndefined()
205
271
  })
272
+
273
+ test('db.isNullable: false makes field non-nullable regardless of validation', () => {
274
+ const field = integer({ db: { isNullable: false } })
275
+ const prismaType = field.getPrismaType('count')
276
+
277
+ expect(prismaType.type).toBe('Int')
278
+ // Non-nullable with no other modifiers → modifiers is undefined
279
+ expect(prismaType.modifiers).toBeUndefined()
280
+ })
281
+
282
+ test('db.isNullable: true on required field overrides to nullable', () => {
283
+ const field = integer({ validation: { isRequired: true }, db: { isNullable: true } })
284
+ const prismaType = field.getPrismaType('count')
285
+
286
+ expect(prismaType.type).toBe('Int')
287
+ expect(prismaType.modifiers).toContain('?')
288
+ })
289
+
290
+ test('db.nativeType generates @db. attribute', () => {
291
+ const field = integer({ db: { nativeType: 'SmallInt' } })
292
+ const prismaType = field.getPrismaType('score')
293
+
294
+ expect(prismaType.type).toBe('Int')
295
+ expect(prismaType.modifiers).toContain('@db.SmallInt')
296
+ })
297
+
298
+ test('db.nativeType with non-nullable field excludes ? but includes @db. attribute', () => {
299
+ const field = integer({ db: { isNullable: false, nativeType: 'BigInt' } })
300
+ const prismaType = field.getPrismaType('largeId')
301
+
302
+ expect(prismaType.type).toBe('Int')
303
+ expect(prismaType.modifiers).toBe('@db.BigInt')
304
+ })
206
305
  })
207
306
 
208
307
  describe('getTypeScriptType', () => {
@@ -251,7 +350,7 @@ describe('Field Types', () => {
251
350
  const prismaType = field.getPrismaType('isActive')
252
351
 
253
352
  expect(prismaType.type).toBe('Boolean')
254
- expect(prismaType.modifiers).toBe(' @default(true)')
353
+ expect(prismaType.modifiers).toBe('@default(true)')
255
354
  })
256
355
 
257
356
  test('returns Boolean type with default false', () => {
@@ -259,7 +358,24 @@ describe('Field Types', () => {
259
358
  const prismaType = field.getPrismaType('isActive')
260
359
 
261
360
  expect(prismaType.type).toBe('Boolean')
262
- expect(prismaType.modifiers).toBe(' @default(false)')
361
+ expect(prismaType.modifiers).toBe('@default(false)')
362
+ })
363
+
364
+ test('db.isNullable: true makes Boolean field nullable', () => {
365
+ const field = checkbox({ db: { isNullable: true } })
366
+ const prismaType = field.getPrismaType('agreed')
367
+
368
+ expect(prismaType.type).toBe('Boolean')
369
+ expect(prismaType.modifiers).toContain('?')
370
+ })
371
+
372
+ test('db.isNullable: true with default value makes nullable Boolean with default', () => {
373
+ const field = checkbox({ defaultValue: false, db: { isNullable: true } })
374
+ const prismaType = field.getPrismaType('agreed')
375
+
376
+ expect(prismaType.type).toBe('Boolean')
377
+ expect(prismaType.modifiers).toContain('?')
378
+ expect(prismaType.modifiers).toContain('@default(false)')
263
379
  })
264
380
  })
265
381
 
@@ -309,7 +425,33 @@ describe('Field Types', () => {
309
425
  const prismaType = field.getPrismaType('createdAt')
310
426
 
311
427
  expect(prismaType.type).toBe('DateTime')
312
- expect(prismaType.modifiers).toBe(' @default(now())')
428
+ expect(prismaType.modifiers).toBe('@default(now())')
429
+ })
430
+
431
+ test('db.isNullable: false makes timestamp non-nullable without default', () => {
432
+ const field = timestamp({ db: { isNullable: false } })
433
+ const prismaType = field.getPrismaType('publishedAt')
434
+
435
+ expect(prismaType.type).toBe('DateTime')
436
+ // Non-nullable with no other modifiers → modifiers is undefined
437
+ expect(prismaType.modifiers).toBeUndefined()
438
+ })
439
+
440
+ test('db.isNullable: true on timestamp with @default(now()) overrides to nullable', () => {
441
+ const field = timestamp({ defaultValue: { kind: 'now' }, db: { isNullable: true } })
442
+ const prismaType = field.getPrismaType('createdAt')
443
+
444
+ expect(prismaType.type).toBe('DateTime')
445
+ expect(prismaType.modifiers).toContain('?')
446
+ expect(prismaType.modifiers).toContain('@default(now())')
447
+ })
448
+
449
+ test('db.nativeType generates @db. attribute', () => {
450
+ const field = timestamp({ db: { nativeType: 'Timestamptz' } })
451
+ const prismaType = field.getPrismaType('scheduledAt')
452
+
453
+ expect(prismaType.type).toBe('DateTime')
454
+ expect(prismaType.modifiers).toContain('@db.Timestamptz')
313
455
  })
314
456
  })
315
457
 
@@ -377,6 +519,31 @@ describe('Field Types', () => {
377
519
  expect(prismaType.type).toBe('String')
378
520
  expect(prismaType.modifiers).toBeUndefined()
379
521
  })
522
+
523
+ test('db.isNullable: false makes password non-nullable regardless of validation', () => {
524
+ const field = password({ db: { isNullable: false } })
525
+ const prismaType = field.getPrismaType('password')
526
+
527
+ expect(prismaType.type).toBe('String')
528
+ // Non-nullable with no other modifiers → modifiers is undefined
529
+ expect(prismaType.modifiers).toBeUndefined()
530
+ })
531
+
532
+ test('db.isNullable: true on required password overrides to nullable', () => {
533
+ const field = password({ validation: { isRequired: true }, db: { isNullable: true } })
534
+ const prismaType = field.getPrismaType('password')
535
+
536
+ expect(prismaType.type).toBe('String')
537
+ expect(prismaType.modifiers).toContain('?')
538
+ })
539
+
540
+ test('db.nativeType generates @db. attribute', () => {
541
+ const field = password({ db: { nativeType: 'Text' } })
542
+ const prismaType = field.getPrismaType('password')
543
+
544
+ expect(prismaType.type).toBe('String')
545
+ expect(prismaType.modifiers).toContain('@db.Text')
546
+ })
380
547
  })
381
548
 
382
549
  describe('getTypeScriptType', () => {
@@ -641,6 +808,23 @@ describe('Field Types', () => {
641
808
  expect(prismaType.type).toBe('Json')
642
809
  expect(prismaType.modifiers).toBeUndefined()
643
810
  })
811
+
812
+ test('db.isNullable: false makes Json field non-nullable regardless of validation', () => {
813
+ const field = json({ db: { isNullable: false } })
814
+ const prismaType = field.getPrismaType('settings')
815
+
816
+ expect(prismaType.type).toBe('Json')
817
+ // Non-nullable with no other modifiers → modifiers is undefined
818
+ expect(prismaType.modifiers).toBeUndefined()
819
+ })
820
+
821
+ test('db.isNullable: true on required field overrides to nullable', () => {
822
+ const field = json({ validation: { isRequired: true }, db: { isNullable: true } })
823
+ const prismaType = field.getPrismaType('settings')
824
+
825
+ expect(prismaType.type).toBe('Json')
826
+ expect(prismaType.modifiers).toContain('?')
827
+ })
644
828
  })
645
829
 
646
830
  describe('getTypeScriptType', () => {
@@ -78,7 +78,7 @@ describe('Singleton Lists', () => {
78
78
  it('should allow creating the first record', async () => {
79
79
  mockPrisma.settings.count.mockResolvedValue(0)
80
80
  mockPrisma.settings.create.mockResolvedValue({
81
- id: '1',
81
+ id: 1,
82
82
  siteName: 'Test Site',
83
83
  maintenanceMode: false,
84
84
  maxUploadSize: 10,
@@ -146,7 +146,7 @@ describe('Singleton Lists', () => {
146
146
 
147
147
  it('should return existing record on get()', async () => {
148
148
  const mockSettings = {
149
- id: '1',
149
+ id: 1,
150
150
  siteName: 'My Site',
151
151
  maintenanceMode: false,
152
152
  maxUploadSize: 10,
@@ -168,7 +168,7 @@ describe('Singleton Lists', () => {
168
168
  mockPrisma.settings.findFirst.mockResolvedValue(null)
169
169
  mockPrisma.settings.count.mockResolvedValue(0)
170
170
  mockPrisma.settings.create.mockResolvedValue({
171
- id: '1',
171
+ id: 1,
172
172
  siteName: 'My Site',
173
173
  maintenanceMode: false,
174
174
  maxUploadSize: 10,
@@ -183,6 +183,7 @@ describe('Singleton Lists', () => {
183
183
  expect(result?.siteName).toBe('My Site')
184
184
  expect(mockPrisma.settings.create).toHaveBeenCalledWith({
185
185
  data: {
186
+ id: 1,
186
187
  siteName: 'My Site',
187
188
  maintenanceMode: false,
188
189
  maxUploadSize: 10,
@@ -207,7 +208,7 @@ describe('Singleton Lists', () => {
207
208
  describe('delete operation', () => {
208
209
  it('should block delete on singleton lists', async () => {
209
210
  mockPrisma.settings.findUnique.mockResolvedValue({
210
- id: '1',
211
+ id: 1,
211
212
  siteName: 'My Site',
212
213
  maintenanceMode: false,
213
214
  maxUploadSize: 10,
@@ -215,18 +216,18 @@ describe('Singleton Lists', () => {
215
216
 
216
217
  const context = getContext(config, mockPrisma, null)
217
218
 
218
- await expect(context.db.settings.delete({ where: { id: '1' } })).rejects.toThrow(
219
+ await expect(context.db.settings.delete({ where: { id: 1 } })).rejects.toThrow(
219
220
  ValidationError,
220
221
  )
221
222
 
222
- await expect(context.db.settings.delete({ where: { id: '1' } })).rejects.toThrow(
223
+ await expect(context.db.settings.delete({ where: { id: 1 } })).rejects.toThrow(
223
224
  'singleton list',
224
225
  )
225
226
  })
226
227
 
227
228
  it('should block delete even in sudo mode', async () => {
228
229
  mockPrisma.settings.findUnique.mockResolvedValue({
229
- id: '1',
230
+ id: 1,
230
231
  siteName: 'My Site',
231
232
  maintenanceMode: false,
232
233
  maxUploadSize: 10,
@@ -235,7 +236,7 @@ describe('Singleton Lists', () => {
235
236
  const context = getContext(config, mockPrisma, null)
236
237
  const sudoContext = context.sudo()
237
238
 
238
- await expect(sudoContext.db.settings.delete({ where: { id: '1' } })).rejects.toThrow(
239
+ await expect(sudoContext.db.settings.delete({ where: { id: 1 } })).rejects.toThrow(
239
240
  ValidationError,
240
241
  )
241
242
  })
@@ -267,14 +268,14 @@ describe('Singleton Lists', () => {
267
268
  describe('update operation', () => {
268
269
  it('should allow updating the singleton record', async () => {
269
270
  mockPrisma.settings.findUnique.mockResolvedValue({
270
- id: '1',
271
+ id: 1,
271
272
  siteName: 'My Site',
272
273
  maintenanceMode: false,
273
274
  maxUploadSize: 10,
274
275
  })
275
276
 
276
277
  mockPrisma.settings.update.mockResolvedValue({
277
- id: '1',
278
+ id: 1,
278
279
  siteName: 'Updated Site',
279
280
  maintenanceMode: true,
280
281
  maxUploadSize: 20,
@@ -285,7 +286,7 @@ describe('Singleton Lists', () => {
285
286
  const context = getContext(config, mockPrisma, null)
286
287
 
287
288
  const result = await context.db.settings.update({
288
- where: { id: '1' },
289
+ where: { id: 1 },
289
290
  data: { siteName: 'Updated Site', maintenanceMode: true, maxUploadSize: 20 },
290
291
  })
291
292
 
@@ -298,7 +299,7 @@ describe('Singleton Lists', () => {
298
299
  describe('findUnique operation', () => {
299
300
  it('should allow findUnique on singleton lists', async () => {
300
301
  mockPrisma.settings.findFirst.mockResolvedValue({
301
- id: '1',
302
+ id: 1,
302
303
  siteName: 'My Site',
303
304
  maintenanceMode: false,
304
305
  maxUploadSize: 10,
@@ -307,7 +308,7 @@ describe('Singleton Lists', () => {
307
308
  })
308
309
 
309
310
  const context = getContext(config, mockPrisma, null)
310
- const result = await context.db.settings.findUnique({ where: { id: '1' } })
311
+ const result = await context.db.settings.findUnique({ where: { id: 1 } })
311
312
 
312
313
  expect(result).toBeDefined()
313
314
  expect(result?.siteName).toBe('My Site')