@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
|
@@ -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('
|
|
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('
|
|
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('
|
|
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', () => {
|
package/tests/singleton.test.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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')
|