@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.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +208 -0
  3. package/CLAUDE.md +46 -1
  4. package/dist/access/engine.d.ts +15 -8
  5. package/dist/access/engine.d.ts.map +1 -1
  6. package/dist/access/engine.js +23 -2
  7. package/dist/access/engine.js.map +1 -1
  8. package/dist/access/engine.test.d.ts +2 -0
  9. package/dist/access/engine.test.d.ts.map +1 -0
  10. package/dist/access/engine.test.js +125 -0
  11. package/dist/access/engine.test.js.map +1 -0
  12. package/dist/access/types.d.ts +40 -9
  13. package/dist/access/types.d.ts.map +1 -1
  14. package/dist/config/index.d.ts +38 -18
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/index.js +34 -14
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/config/plugin-engine.d.ts.map +1 -1
  19. package/dist/config/plugin-engine.js +6 -0
  20. package/dist/config/plugin-engine.js.map +1 -1
  21. package/dist/config/types.d.ts +128 -21
  22. package/dist/config/types.d.ts.map +1 -1
  23. package/dist/context/index.d.ts +14 -2
  24. package/dist/context/index.d.ts.map +1 -1
  25. package/dist/context/index.js +243 -100
  26. package/dist/context/index.js.map +1 -1
  27. package/dist/fields/index.d.ts.map +1 -1
  28. package/dist/fields/index.js +9 -8
  29. package/dist/fields/index.js.map +1 -1
  30. package/dist/hooks/index.d.ts +28 -12
  31. package/dist/hooks/index.d.ts.map +1 -1
  32. package/dist/hooks/index.js +16 -0
  33. package/dist/hooks/index.js.map +1 -1
  34. package/package.json +3 -4
  35. package/src/access/engine.test.ts +145 -0
  36. package/src/access/engine.ts +35 -11
  37. package/src/access/types.ts +39 -8
  38. package/src/config/index.ts +46 -19
  39. package/src/config/plugin-engine.ts +7 -0
  40. package/src/config/types.ts +149 -18
  41. package/src/context/index.ts +298 -110
  42. package/src/fields/index.ts +8 -7
  43. package/src/hooks/index.ts +63 -20
  44. package/tests/context.test.ts +38 -6
  45. package/tests/field-types.test.ts +728 -0
  46. package/tests/password-type-distribution.test.ts +0 -1
  47. package/tests/password-types.test.ts +0 -1
  48. package/tests/plugin-engine.test.ts +1102 -0
  49. package/tests/sudo.test.ts +405 -0
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,405 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { getContext } from '../src/context/index.js'
3
+ import { config, list } from '../src/config/index.js'
4
+ import { text, integer } from '../src/fields/index.js'
5
+ import type { PrismaClient } from '@prisma/client'
6
+
7
+ describe('Sudo Context', () => {
8
+ // Mock Prisma client
9
+ const mockPrisma = {
10
+ post: {
11
+ findFirst: vi.fn(),
12
+ findMany: vi.fn(),
13
+ findUnique: vi.fn(),
14
+ create: vi.fn(),
15
+ update: vi.fn(),
16
+ delete: vi.fn(),
17
+ count: vi.fn(),
18
+ },
19
+ } as unknown as PrismaClient
20
+
21
+ // Track hook execution
22
+ const hookExecutions: string[] = []
23
+
24
+ const testConfig = config({
25
+ db: {
26
+ provider: 'sqlite',
27
+ },
28
+ lists: {
29
+ Post: list({
30
+ fields: {
31
+ title: text({
32
+ validation: { isRequired: true },
33
+ hooks: {
34
+ resolveInput: async ({ inputValue }) => {
35
+ hookExecutions.push('field-resolveInput')
36
+ return inputValue
37
+ },
38
+ beforeOperation: async () => {
39
+ hookExecutions.push('field-beforeOperation')
40
+ },
41
+ afterOperation: async () => {
42
+ hookExecutions.push('field-afterOperation')
43
+ },
44
+ },
45
+ }),
46
+ secretField: text({
47
+ access: {
48
+ read: async () => false,
49
+ create: async () => false,
50
+ update: async () => false,
51
+ },
52
+ }),
53
+ views: integer({ defaultValue: 0 }),
54
+ },
55
+ access: {
56
+ operation: {
57
+ query: async () => false,
58
+ create: async () => false,
59
+ update: async () => false,
60
+ delete: async () => false,
61
+ },
62
+ },
63
+ hooks: {
64
+ resolveInput: async ({ resolvedData }) => {
65
+ hookExecutions.push('list-resolveInput')
66
+ return resolvedData
67
+ },
68
+ validateInput: async () => {
69
+ hookExecutions.push('list-validateInput')
70
+ },
71
+ beforeOperation: async () => {
72
+ hookExecutions.push('list-beforeOperation')
73
+ },
74
+ afterOperation: async () => {
75
+ hookExecutions.push('list-afterOperation')
76
+ },
77
+ },
78
+ }),
79
+ },
80
+ })
81
+
82
+ beforeEach(() => {
83
+ vi.clearAllMocks()
84
+ hookExecutions.length = 0
85
+ })
86
+
87
+ describe('Query Operations', () => {
88
+ it('should bypass operation-level access control with sudo()', async () => {
89
+ const context = getContext(testConfig, mockPrisma, null)
90
+ const sudoContext = context.sudo()
91
+
92
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
93
+
94
+ // Regular context should return empty array (access denied)
95
+ const regularResult = await context.db.post.findMany()
96
+ expect(regularResult).toEqual([])
97
+
98
+ // Sudo context should return results
99
+ const sudoResult = await sudoContext.db.post.findMany()
100
+ expect(sudoResult).toHaveLength(1)
101
+ expect(sudoResult[0].title).toBe('Test Post')
102
+ })
103
+
104
+ it('should bypass field-level read access control with sudo()', async () => {
105
+ const context = getContext(testConfig, mockPrisma, null)
106
+ const sudoContext = context.sudo()
107
+
108
+ mockPrisma.post.findFirst.mockResolvedValue({
109
+ id: '1',
110
+ title: 'Test Post',
111
+ secretField: 'secret-value',
112
+ views: 10,
113
+ })
114
+
115
+ // Sudo context should return all fields including secretField
116
+ const sudoResult = await sudoContext.db.post.findUnique({ where: { id: '1' } })
117
+ expect(sudoResult?.secretField).toBe('secret-value')
118
+ })
119
+
120
+ it('should execute field afterOperation hooks with sudo()', async () => {
121
+ const context = getContext(testConfig, mockPrisma, null)
122
+ const sudoContext = context.sudo()
123
+
124
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
125
+
126
+ await sudoContext.db.post.findMany()
127
+
128
+ expect(hookExecutions).toContain('field-afterOperation')
129
+ })
130
+ })
131
+
132
+ describe('Create Operations', () => {
133
+ it('should bypass operation-level access control with sudo()', async () => {
134
+ const context = getContext(testConfig, mockPrisma, null)
135
+ const sudoContext = context.sudo()
136
+
137
+ const mockPost = { id: '1', title: 'New Post', views: 0 }
138
+ mockPrisma.post.create.mockResolvedValue(mockPost)
139
+
140
+ // Regular context should return null (access denied)
141
+ const regularResult = await context.db.post.create({
142
+ data: { title: 'New Post' },
143
+ })
144
+ expect(regularResult).toBeNull()
145
+
146
+ // Sudo context should create successfully
147
+ hookExecutions.length = 0
148
+ const sudoResult = await sudoContext.db.post.create({
149
+ data: { title: 'New Post' },
150
+ })
151
+ expect(sudoResult).toMatchObject({ title: 'New Post' })
152
+ expect(mockPrisma.post.create).toHaveBeenCalledWith({
153
+ data: { title: 'New Post' },
154
+ })
155
+ })
156
+
157
+ it('should bypass field-level write access control with sudo()', async () => {
158
+ const context = getContext(testConfig, mockPrisma, null)
159
+ const sudoContext = context.sudo()
160
+
161
+ const mockPost = { id: '1', title: 'New Post', secretField: 'secret', views: 0 }
162
+ mockPrisma.post.create.mockResolvedValue(mockPost)
163
+
164
+ // Sudo context should allow writing to secretField
165
+ await sudoContext.db.post.create({
166
+ data: { title: 'New Post', secretField: 'secret' },
167
+ })
168
+
169
+ // Verify that secretField was passed to Prisma
170
+ expect(mockPrisma.post.create).toHaveBeenCalledWith({
171
+ data: { title: 'New Post', secretField: 'secret' },
172
+ })
173
+ })
174
+
175
+ it('should execute all hooks with sudo()', async () => {
176
+ const context = getContext(testConfig, mockPrisma, null)
177
+ const sudoContext = context.sudo()
178
+
179
+ const mockPost = { id: '1', title: 'New Post', views: 0 }
180
+ mockPrisma.post.create.mockResolvedValue(mockPost)
181
+
182
+ await sudoContext.db.post.create({
183
+ data: { title: 'New Post' },
184
+ })
185
+
186
+ // Verify all hooks were executed
187
+ expect(hookExecutions).toContain('list-resolveInput')
188
+ expect(hookExecutions).toContain('field-resolveInput')
189
+ expect(hookExecutions).toContain('list-validateInput')
190
+ expect(hookExecutions).toContain('field-beforeOperation')
191
+ expect(hookExecutions).toContain('list-beforeOperation')
192
+ expect(hookExecutions).toContain('list-afterOperation')
193
+ expect(hookExecutions).toContain('field-afterOperation')
194
+ })
195
+
196
+ it('should still validate required fields with sudo()', async () => {
197
+ const context = getContext(testConfig, mockPrisma, null)
198
+ const sudoContext = context.sudo()
199
+
200
+ // Should throw validation error for missing required field
201
+ await expect(
202
+ sudoContext.db.post.create({
203
+ data: { views: 10 },
204
+ }),
205
+ ).rejects.toThrow('Title must be text')
206
+ })
207
+ })
208
+
209
+ describe('Update Operations', () => {
210
+ it('should bypass operation-level access control with sudo()', async () => {
211
+ const context = getContext(testConfig, mockPrisma, null)
212
+ const sudoContext = context.sudo()
213
+
214
+ const existingPost = { id: '1', title: 'Old Title', views: 5 }
215
+ const updatedPost = { id: '1', title: 'New Title', views: 5 }
216
+
217
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
218
+ mockPrisma.post.findFirst.mockResolvedValue(null)
219
+ mockPrisma.post.update.mockResolvedValue(updatedPost)
220
+
221
+ // Regular context should return null (access denied)
222
+ const regularResult = await context.db.post.update({
223
+ where: { id: '1' },
224
+ data: { title: 'New Title' },
225
+ })
226
+ expect(regularResult).toBeNull()
227
+
228
+ // Reset mocks
229
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
230
+ hookExecutions.length = 0
231
+
232
+ // Sudo context should update successfully
233
+ const sudoResult = await sudoContext.db.post.update({
234
+ where: { id: '1' },
235
+ data: { title: 'New Title' },
236
+ })
237
+ expect(sudoResult).toMatchObject({ title: 'New Title' })
238
+ })
239
+
240
+ it('should bypass field-level write access control with sudo()', async () => {
241
+ const context = getContext(testConfig, mockPrisma, null)
242
+ const sudoContext = context.sudo()
243
+
244
+ const existingPost = { id: '1', title: 'Old Title', secretField: 'old-secret', views: 5 }
245
+ const updatedPost = { id: '1', title: 'Old Title', secretField: 'new-secret', views: 5 }
246
+
247
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
248
+ mockPrisma.post.update.mockResolvedValue(updatedPost)
249
+
250
+ // Sudo context should allow updating secretField
251
+ await sudoContext.db.post.update({
252
+ where: { id: '1' },
253
+ data: { secretField: 'new-secret' },
254
+ })
255
+
256
+ // Verify that secretField was passed to Prisma
257
+ expect(mockPrisma.post.update).toHaveBeenCalledWith({
258
+ where: { id: '1' },
259
+ data: { secretField: 'new-secret' },
260
+ })
261
+ })
262
+
263
+ it('should execute all hooks with sudo()', async () => {
264
+ const context = getContext(testConfig, mockPrisma, null)
265
+ const sudoContext = context.sudo()
266
+
267
+ const existingPost = { id: '1', title: 'Old Title', views: 5 }
268
+ const updatedPost = { id: '1', title: 'New Title', views: 5 }
269
+
270
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
271
+ mockPrisma.post.update.mockResolvedValue(updatedPost)
272
+
273
+ await sudoContext.db.post.update({
274
+ where: { id: '1' },
275
+ data: { title: 'New Title' },
276
+ })
277
+
278
+ // Verify all hooks were executed
279
+ expect(hookExecutions).toContain('list-resolveInput')
280
+ expect(hookExecutions).toContain('field-resolveInput')
281
+ expect(hookExecutions).toContain('list-validateInput')
282
+ expect(hookExecutions).toContain('field-beforeOperation')
283
+ expect(hookExecutions).toContain('list-beforeOperation')
284
+ expect(hookExecutions).toContain('list-afterOperation')
285
+ expect(hookExecutions).toContain('field-afterOperation')
286
+ })
287
+ })
288
+
289
+ describe('Delete Operations', () => {
290
+ it('should bypass operation-level access control with sudo()', async () => {
291
+ const context = getContext(testConfig, mockPrisma, null)
292
+ const sudoContext = context.sudo()
293
+
294
+ const existingPost = { id: '1', title: 'Post to Delete', views: 5 }
295
+
296
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
297
+ mockPrisma.post.findFirst.mockResolvedValue(null)
298
+ mockPrisma.post.delete.mockResolvedValue(existingPost)
299
+
300
+ // Regular context should return null (access denied)
301
+ const regularResult = await context.db.post.delete({
302
+ where: { id: '1' },
303
+ })
304
+ expect(regularResult).toBeNull()
305
+
306
+ // Reset mocks
307
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
308
+ hookExecutions.length = 0
309
+
310
+ // Sudo context should delete successfully
311
+ const sudoResult = await sudoContext.db.post.delete({
312
+ where: { id: '1' },
313
+ })
314
+ expect(sudoResult).toMatchObject({ title: 'Post to Delete' })
315
+ })
316
+
317
+ it('should execute all hooks with sudo()', async () => {
318
+ const context = getContext(testConfig, mockPrisma, null)
319
+ const sudoContext = context.sudo()
320
+
321
+ const existingPost = { id: '1', title: 'Post to Delete', views: 5 }
322
+
323
+ mockPrisma.post.findUnique.mockResolvedValue(existingPost)
324
+ mockPrisma.post.delete.mockResolvedValue(existingPost)
325
+
326
+ await sudoContext.db.post.delete({
327
+ where: { id: '1' },
328
+ })
329
+
330
+ // Verify hooks were executed
331
+ expect(hookExecutions).toContain('field-beforeOperation')
332
+ expect(hookExecutions).toContain('list-beforeOperation')
333
+ expect(hookExecutions).toContain('list-afterOperation')
334
+ expect(hookExecutions).toContain('field-afterOperation')
335
+ })
336
+ })
337
+
338
+ describe('Count Operations', () => {
339
+ it('should bypass operation-level access control with sudo()', async () => {
340
+ const context = getContext(testConfig, mockPrisma, null)
341
+ const sudoContext = context.sudo()
342
+
343
+ mockPrisma.post.count.mockResolvedValue(10)
344
+
345
+ // Regular context should return 0 (access denied)
346
+ const regularResult = await context.db.post.count()
347
+ expect(regularResult).toBe(0)
348
+
349
+ // Sudo context should return actual count
350
+ const sudoResult = await sudoContext.db.post.count()
351
+ expect(sudoResult).toBe(10)
352
+ })
353
+ })
354
+
355
+ describe('Sudo Context Properties', () => {
356
+ it('should maintain the same session object', async () => {
357
+ const session = { userId: 'user-123', role: 'admin' }
358
+ const context = getContext(testConfig, mockPrisma, session)
359
+ const sudoContext = context.sudo()
360
+
361
+ expect(sudoContext.session).toEqual(session)
362
+ expect(sudoContext.session).toBe(context.session)
363
+ })
364
+
365
+ it('should maintain the same prisma client', async () => {
366
+ const context = getContext(testConfig, mockPrisma, null)
367
+ const sudoContext = context.sudo()
368
+
369
+ expect(sudoContext.prisma).toBe(context.prisma)
370
+ })
371
+
372
+ it('should maintain the same storage utilities', async () => {
373
+ const context = getContext(testConfig, mockPrisma, null)
374
+ const sudoContext = context.sudo()
375
+
376
+ expect(sudoContext.storage).toBe(context.storage)
377
+ })
378
+
379
+ it('should allow chaining sudo() calls', async () => {
380
+ const context = getContext(testConfig, mockPrisma, null)
381
+ const sudoContext1 = context.sudo()
382
+ const sudoContext2 = sudoContext1.sudo()
383
+
384
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
385
+
386
+ const result = await sudoContext2.db.post.findMany()
387
+ expect(result).toHaveLength(1)
388
+ })
389
+
390
+ it('should create independent contexts', async () => {
391
+ const context = getContext(testConfig, mockPrisma, null)
392
+ const sudoContext = context.sudo()
393
+
394
+ mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
395
+
396
+ // Regular context still denies access
397
+ const regularResult = await context.db.post.findMany()
398
+ expect(regularResult).toEqual([])
399
+
400
+ // Sudo context allows access
401
+ const sudoResult = await sudoContext.db.post.findMany()
402
+ expect(sudoResult).toHaveLength(1)
403
+ })
404
+ })
405
+ })