@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
package/src/hooks/index.ts
CHANGED
|
@@ -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<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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<
|
|
47
|
-
|
|
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:
|
|
51
|
-
item?:
|
|
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<
|
|
80
|
-
|
|
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
|
-
|
|
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<
|
|
99
|
-
|
|
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
|
-
|
|
144
|
+
resolvedData?: TCreateInput | TUpdateInput
|
|
145
|
+
item: TOutput
|
|
103
146
|
context: AccessContext
|
|
104
147
|
},
|
|
105
148
|
): Promise<void> {
|
package/tests/context.test.ts
CHANGED
|
@@ -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
|
|
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).
|
|
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
|
|