@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
@@ -18,24 +18,53 @@ export class ValidationError extends Error {
18
18
  }
19
19
  }
20
20
 
21
+ /**
22
+ * Database error with field-specific error information
23
+ * Used for Prisma errors like unique constraint violations
24
+ */
25
+ export class DatabaseError extends Error {
26
+ public fieldErrors: Record<string, string>
27
+ public code?: string
28
+
29
+ constructor(message: string, fieldErrors: Record<string, string> = {}, code?: string) {
30
+ super(message)
31
+ this.name = 'DatabaseError'
32
+ this.fieldErrors = fieldErrors
33
+ this.code = code
34
+ }
35
+ }
36
+
21
37
  /**
22
38
  * Execute resolveInput hook
23
39
  * Allows modification of input data before validation
24
40
  */
25
- export async function executeResolveInput<T = Record<string, unknown>>(
26
- hooks: Hooks<T> | undefined,
27
- args: {
28
- operation: 'create' | 'update'
29
- resolvedData: Partial<T>
30
- item?: T
31
- context: AccessContext
32
- },
33
- ): Promise<Partial<T>> {
41
+ export async function executeResolveInput<
42
+ TOutput = Record<string, unknown>,
43
+ TCreateInput = Record<string, unknown>,
44
+ TUpdateInput = Record<string, unknown>,
45
+ >(
46
+ hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
47
+ args:
48
+ | {
49
+ operation: 'create'
50
+ resolvedData: TCreateInput
51
+ item?: undefined
52
+ context: AccessContext
53
+ }
54
+ | {
55
+ operation: 'update'
56
+ resolvedData: TUpdateInput
57
+ item?: TOutput
58
+ context: AccessContext
59
+ },
60
+ ): Promise<TCreateInput | TUpdateInput> {
34
61
  if (!hooks?.resolveInput) {
35
62
  return args.resolvedData
36
63
  }
37
64
 
38
- const result = await hooks.resolveInput(args)
65
+ // Type assertion is safe because we've constrained the args type
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ const result = await hooks.resolveInput(args as any)
39
68
  return result
40
69
  }
41
70
 
@@ -43,12 +72,16 @@ export async function executeResolveInput<T = Record<string, unknown>>(
43
72
  * Execute validateInput hook
44
73
  * Allows custom validation logic
45
74
  */
46
- export async function executeValidateInput<T = Record<string, unknown>>(
47
- hooks: Hooks<T> | undefined,
75
+ export async function executeValidateInput<
76
+ TOutput = Record<string, unknown>,
77
+ TCreateInput = Record<string, unknown>,
78
+ TUpdateInput = Record<string, unknown>,
79
+ >(
80
+ hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
48
81
  args: {
49
82
  operation: 'create' | 'update'
50
- resolvedData: Partial<T>
51
- item?: T
83
+ resolvedData: TCreateInput | TUpdateInput
84
+ item?: TOutput
52
85
  context: AccessContext
53
86
  },
54
87
  ): Promise<void> {
@@ -76,11 +109,16 @@ export async function executeValidateInput<T = Record<string, unknown>>(
76
109
  * Execute beforeOperation hook
77
110
  * Runs before database operation (cannot modify data)
78
111
  */
79
- export async function executeBeforeOperation<T = Record<string, unknown>>(
80
- hooks: Hooks<T> | undefined,
112
+ export async function executeBeforeOperation<
113
+ TOutput = Record<string, unknown>,
114
+ TCreateInput = Record<string, unknown>,
115
+ TUpdateInput = Record<string, unknown>,
116
+ >(
117
+ hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
81
118
  args: {
82
119
  operation: 'create' | 'update' | 'delete'
83
- item?: T
120
+ resolvedData?: TCreateInput | TUpdateInput
121
+ item?: TOutput
84
122
  context: AccessContext
85
123
  },
86
124
  ): Promise<void> {
@@ -95,11 +133,16 @@ export async function executeBeforeOperation<T = Record<string, unknown>>(
95
133
  * Execute afterOperation hook
96
134
  * Runs after database operation
97
135
  */
98
- export async function executeAfterOperation<T = Record<string, unknown>>(
99
- hooks: Hooks<T> | undefined,
136
+ export async function executeAfterOperation<
137
+ TOutput = Record<string, unknown>,
138
+ TCreateInput = Record<string, unknown>,
139
+ TUpdateInput = Record<string, unknown>,
140
+ >(
141
+ hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
100
142
  args: {
101
143
  operation: 'create' | 'update' | 'delete'
102
- item: T
144
+ resolvedData?: TCreateInput | TUpdateInput
145
+ item: TOutput
103
146
  context: AccessContext
104
147
  },
105
148
  ): Promise<void> {
@@ -101,7 +101,7 @@ describe('getContext', () => {
101
101
  expect(mockPrisma.user.create).toHaveBeenCalledWith({
102
102
  data: { name: 'John', email: 'john@example.com' },
103
103
  })
104
- expect(result).toEqual(mockCreatedUser)
104
+ expect(result).toEqual({ success: true, data: mockCreatedUser })
105
105
  })
106
106
 
107
107
  it('should update an item', async () => {
@@ -121,7 +121,7 @@ describe('getContext', () => {
121
121
 
122
122
  expect(mockPrisma.user.findUnique).toHaveBeenCalled()
123
123
  expect(mockPrisma.user.update).toHaveBeenCalled()
124
- expect(result).toEqual(mockUpdatedUser)
124
+ expect(result).toEqual({ success: true, data: mockUpdatedUser })
125
125
  })
126
126
 
127
127
  it('should delete an item', async () => {
@@ -139,7 +139,7 @@ describe('getContext', () => {
139
139
 
140
140
  expect(mockPrisma.user.findUnique).toHaveBeenCalled()
141
141
  expect(mockPrisma.user.delete).toHaveBeenCalled()
142
- expect(result).toEqual(mockDeletedUser)
142
+ expect(result).toEqual({ success: true, data: mockDeletedUser })
143
143
  })
144
144
 
145
145
  it('should convert listKey to lowercase for db operations', async () => {
@@ -147,16 +147,17 @@ describe('getContext', () => {
147
147
  mockPrisma.post.create.mockResolvedValue(mockCreatedPost)
148
148
 
149
149
  const context = await getContext(config, mockPrisma, null)
150
- await context.serverAction({
150
+ const result = await context.serverAction({
151
151
  listKey: 'Post',
152
152
  action: 'create',
153
153
  data: { title: 'Test Post' },
154
154
  })
155
155
 
156
156
  expect(mockPrisma.post.create).toHaveBeenCalled()
157
+ expect(result).toEqual({ success: true, data: mockCreatedPost })
157
158
  })
158
159
 
159
- it('should return null for unknown action', async () => {
160
+ it('should return error for unknown action', async () => {
160
161
  const context = await getContext(config, mockPrisma, null)
161
162
  const result = await context.serverAction({
162
163
  listKey: 'User',
@@ -164,7 +165,38 @@ describe('getContext', () => {
164
165
  data: {},
165
166
  })
166
167
 
167
- expect(result).toBeNull()
168
+ expect(result).toEqual({ success: false, error: 'Access denied or operation failed' })
169
+ })
170
+
171
+ it('should return error for unknown list', async () => {
172
+ const context = await getContext(config, mockPrisma, null)
173
+ const result = await context.serverAction({
174
+ listKey: 'UnknownList',
175
+ action: 'create',
176
+ data: {},
177
+ })
178
+
179
+ expect(result).toEqual({
180
+ success: false,
181
+ error: 'List "UnknownList" not found in configuration',
182
+ })
183
+ })
184
+
185
+ it('should handle database errors', async () => {
186
+ const dbError = new Error('Database connection failed')
187
+ mockPrisma.user.create.mockRejectedValue(dbError)
188
+
189
+ const context = await getContext(config, mockPrisma, null)
190
+ const result = await context.serverAction({
191
+ listKey: 'User',
192
+ action: 'create',
193
+ data: { name: 'John', email: 'john@example.com' },
194
+ })
195
+
196
+ expect(result).toMatchObject({
197
+ success: false,
198
+ error: 'Database connection failed',
199
+ })
168
200
  })
169
201
  })
170
202