@opensaas/stack-core 0.12.1 → 0.14.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 +291 -0
- package/README.md +6 -3
- package/dist/access/engine.d.ts +2 -0
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +8 -6
- package/dist/access/engine.js.map +1 -1
- package/dist/access/engine.test.js +4 -0
- package/dist/access/engine.test.js.map +1 -1
- package/dist/access/types.d.ts +31 -4
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +12 -10
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +37 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +341 -82
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +330 -60
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +38 -25
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/hooks/index.d.ts +45 -7
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +10 -4
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/engine.test.ts +4 -0
- package/src/access/engine.ts +10 -7
- package/src/access/types.ts +45 -4
- package/src/config/index.ts +65 -9
- package/src/config/types.ts +402 -91
- package/src/context/index.ts +421 -82
- package/src/context/nested-operations.ts +40 -25
- package/src/hooks/index.ts +66 -14
- package/src/index.ts +11 -0
- package/tests/access.test.ts +28 -28
- package/tests/config.test.ts +20 -3
- package/tests/nested-access-and-hooks.test.ts +8 -3
- package/tests/singleton.test.ts +329 -0
- package/tests/sudo.test.ts +2 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,7 +3,7 @@ import type { AccessContext } from '../access/types.js'
|
|
|
3
3
|
import { checkAccess, filterWritableFields, getRelatedListConfig } from '../access/index.js'
|
|
4
4
|
import {
|
|
5
5
|
executeResolveInput,
|
|
6
|
-
|
|
6
|
+
executeValidate,
|
|
7
7
|
validateFieldRules,
|
|
8
8
|
ValidationError,
|
|
9
9
|
} from '../hooks/index.js'
|
|
@@ -15,7 +15,9 @@ import { getDbKey } from '../lib/case-utils.js'
|
|
|
15
15
|
*/
|
|
16
16
|
async function executeFieldResolveInputHooks(
|
|
17
17
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
-
|
|
18
|
+
inputData: Record<string, any>,
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
resolvedData: Record<string, any>,
|
|
19
21
|
fields: Record<string, FieldConfig>,
|
|
20
22
|
operation: 'create' | 'update',
|
|
21
23
|
context: AccessContext,
|
|
@@ -23,26 +25,28 @@ async function executeFieldResolveInputHooks(
|
|
|
23
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
26
|
item?: any,
|
|
25
27
|
): Promise<Record<string, unknown>> {
|
|
26
|
-
|
|
28
|
+
let result = { ...resolvedData }
|
|
27
29
|
|
|
28
|
-
for (const [
|
|
30
|
+
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
29
31
|
// Skip if field not in data
|
|
30
|
-
if (!(
|
|
32
|
+
if (!(fieldKey in result)) continue
|
|
31
33
|
|
|
32
34
|
// Skip if no hooks defined
|
|
33
35
|
if (!fieldConfig.hooks?.resolveInput) continue
|
|
34
36
|
|
|
35
37
|
// Execute field hook
|
|
36
38
|
const transformedValue = await fieldConfig.hooks.resolveInput({
|
|
37
|
-
inputValue: result[fieldName],
|
|
38
|
-
operation,
|
|
39
|
-
fieldName,
|
|
40
39
|
listKey,
|
|
40
|
+
fieldKey,
|
|
41
|
+
operation,
|
|
42
|
+
inputData,
|
|
41
43
|
item,
|
|
44
|
+
resolvedData: { ...result }, // Pass a copy to avoid mutation affecting recorded args
|
|
42
45
|
context,
|
|
43
|
-
})
|
|
46
|
+
} as Parameters<typeof fieldConfig.hooks.resolveInput>[0])
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
// Create new object with updated field to avoid mutating the passed reference
|
|
49
|
+
result = { ...result, [fieldKey]: transformedValue }
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
return result
|
|
@@ -83,17 +87,7 @@ async function processNestedCreate(
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
// 2.
|
|
87
|
-
let resolvedData = await executeResolveInput(relatedListConfig.hooks, {
|
|
88
|
-
operation: 'create',
|
|
89
|
-
resolvedData: item,
|
|
90
|
-
item: undefined,
|
|
91
|
-
context,
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// 2.5. Execute field-level resolveInput hooks
|
|
95
|
-
// We need to get the list name for this related config
|
|
96
|
-
// Since we don't have it directly, we'll need to find it from the config
|
|
90
|
+
// 2. Get the list name for this related config
|
|
97
91
|
let relatedListName = ''
|
|
98
92
|
for (const [listKey, listCfg] of Object.entries(config.lists)) {
|
|
99
93
|
if (listCfg === relatedListConfig) {
|
|
@@ -102,7 +96,19 @@ async function processNestedCreate(
|
|
|
102
96
|
}
|
|
103
97
|
}
|
|
104
98
|
|
|
99
|
+
// 3. Execute list-level resolveInput hook
|
|
100
|
+
let resolvedData = await executeResolveInput(relatedListConfig.hooks, {
|
|
101
|
+
listKey: relatedListName,
|
|
102
|
+
operation: 'create',
|
|
103
|
+
inputData: item,
|
|
104
|
+
resolvedData: item,
|
|
105
|
+
item: undefined,
|
|
106
|
+
context,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// 4. Execute field-level resolveInput hooks
|
|
105
110
|
resolvedData = await executeFieldResolveInputHooks(
|
|
111
|
+
item,
|
|
106
112
|
resolvedData,
|
|
107
113
|
relatedListConfig.fields,
|
|
108
114
|
'create',
|
|
@@ -110,9 +116,11 @@ async function processNestedCreate(
|
|
|
110
116
|
relatedListName,
|
|
111
117
|
)
|
|
112
118
|
|
|
113
|
-
//
|
|
114
|
-
await
|
|
119
|
+
// 5. Execute validate hook
|
|
120
|
+
await executeValidate(relatedListConfig.hooks, {
|
|
121
|
+
listKey: relatedListName,
|
|
115
122
|
operation: 'create',
|
|
123
|
+
inputData: item,
|
|
116
124
|
resolvedData,
|
|
117
125
|
item: undefined,
|
|
118
126
|
context,
|
|
@@ -132,6 +140,7 @@ async function processNestedCreate(
|
|
|
132
140
|
{
|
|
133
141
|
session: context.session,
|
|
134
142
|
context,
|
|
143
|
+
inputData: item,
|
|
135
144
|
},
|
|
136
145
|
)
|
|
137
146
|
|
|
@@ -257,7 +266,9 @@ async function processNestedUpdate(
|
|
|
257
266
|
// Execute list-level resolveInput hook
|
|
258
267
|
const updateData = (update as Record<string, unknown>).data as Record<string, unknown>
|
|
259
268
|
let resolvedData = await executeResolveInput(relatedListConfig.hooks, {
|
|
269
|
+
listKey: relatedListName,
|
|
260
270
|
operation: 'update',
|
|
271
|
+
inputData: updateData,
|
|
261
272
|
resolvedData: updateData,
|
|
262
273
|
item,
|
|
263
274
|
context,
|
|
@@ -265,6 +276,7 @@ async function processNestedUpdate(
|
|
|
265
276
|
|
|
266
277
|
// Execute field-level resolveInput hooks
|
|
267
278
|
resolvedData = await executeFieldResolveInputHooks(
|
|
279
|
+
updateData,
|
|
268
280
|
resolvedData,
|
|
269
281
|
relatedListConfig.fields,
|
|
270
282
|
'update',
|
|
@@ -273,9 +285,11 @@ async function processNestedUpdate(
|
|
|
273
285
|
item,
|
|
274
286
|
)
|
|
275
287
|
|
|
276
|
-
// Execute
|
|
277
|
-
await
|
|
288
|
+
// Execute validate hook
|
|
289
|
+
await executeValidate(relatedListConfig.hooks, {
|
|
290
|
+
listKey: relatedListName,
|
|
278
291
|
operation: 'update',
|
|
292
|
+
inputData: updateData,
|
|
279
293
|
resolvedData,
|
|
280
294
|
item,
|
|
281
295
|
context,
|
|
@@ -296,6 +310,7 @@ async function processNestedUpdate(
|
|
|
296
310
|
session: context.session,
|
|
297
311
|
item,
|
|
298
312
|
context,
|
|
313
|
+
inputData: updateData,
|
|
299
314
|
},
|
|
300
315
|
)
|
|
301
316
|
|
package/src/hooks/index.ts
CHANGED
|
@@ -46,13 +46,17 @@ export async function executeResolveInput<
|
|
|
46
46
|
hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
|
|
47
47
|
args:
|
|
48
48
|
| {
|
|
49
|
+
listKey: string
|
|
49
50
|
operation: 'create'
|
|
51
|
+
inputData: TCreateInput
|
|
50
52
|
resolvedData: TCreateInput
|
|
51
53
|
item: undefined
|
|
52
54
|
context: AccessContext
|
|
53
55
|
}
|
|
54
56
|
| {
|
|
57
|
+
listKey: string
|
|
55
58
|
operation: 'update'
|
|
59
|
+
inputData: TUpdateInput
|
|
56
60
|
resolvedData: TUpdateInput
|
|
57
61
|
item: TOutput
|
|
58
62
|
context: AccessContext
|
|
@@ -67,10 +71,10 @@ export async function executeResolveInput<
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
|
-
* Execute
|
|
74
|
+
* Execute validate hook (supports both 'validate' and deprecated 'validateInput')
|
|
71
75
|
* Allows custom validation logic
|
|
72
76
|
*/
|
|
73
|
-
export async function
|
|
77
|
+
export async function executeValidate<
|
|
74
78
|
TOutput = Record<string, unknown>,
|
|
75
79
|
TCreateInput = Record<string, unknown>,
|
|
76
80
|
TUpdateInput = Record<string, unknown>,
|
|
@@ -78,19 +82,31 @@ export async function executeValidateInput<
|
|
|
78
82
|
hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
|
|
79
83
|
args:
|
|
80
84
|
| {
|
|
85
|
+
listKey: string
|
|
81
86
|
operation: 'create'
|
|
87
|
+
inputData: TCreateInput
|
|
82
88
|
resolvedData: TCreateInput
|
|
83
89
|
item: undefined
|
|
84
90
|
context: AccessContext
|
|
85
91
|
}
|
|
86
92
|
| {
|
|
93
|
+
listKey: string
|
|
87
94
|
operation: 'update'
|
|
95
|
+
inputData: TUpdateInput
|
|
88
96
|
resolvedData: TUpdateInput
|
|
89
97
|
item: TOutput
|
|
90
98
|
context: AccessContext
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
listKey: string
|
|
102
|
+
operation: 'delete'
|
|
103
|
+
item: TOutput
|
|
104
|
+
context: AccessContext
|
|
91
105
|
},
|
|
92
106
|
): Promise<void> {
|
|
93
|
-
|
|
107
|
+
// Support both 'validate' (new) and 'validateInput' (deprecated) for backwards compatibility
|
|
108
|
+
const validateHook = hooks?.validate || hooks?.validateInput
|
|
109
|
+
if (!validateHook) {
|
|
94
110
|
return
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -100,29 +116,50 @@ export async function executeValidateInput<
|
|
|
100
116
|
errors.push(msg)
|
|
101
117
|
}
|
|
102
118
|
|
|
103
|
-
await
|
|
119
|
+
await validateHook({
|
|
104
120
|
...args,
|
|
105
121
|
addValidationError,
|
|
106
|
-
})
|
|
122
|
+
} as Parameters<typeof validateHook>[0])
|
|
107
123
|
|
|
108
124
|
if (errors.length > 0) {
|
|
109
125
|
throw new ValidationError(errors)
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
|
|
129
|
+
/**
|
|
130
|
+
* @deprecated Use executeValidate instead. This alias is provided for backwards compatibility.
|
|
131
|
+
*/
|
|
132
|
+
export const executeValidateInput = executeValidate
|
|
133
|
+
|
|
113
134
|
/**
|
|
114
135
|
* Execute beforeOperation hook
|
|
115
136
|
* Runs before database operation (cannot modify data)
|
|
116
137
|
*/
|
|
117
|
-
export async function executeBeforeOperation<
|
|
118
|
-
|
|
138
|
+
export async function executeBeforeOperation<
|
|
139
|
+
TOutput = Record<string, unknown>,
|
|
140
|
+
TCreateInput = Record<string, unknown>,
|
|
141
|
+
TUpdateInput = Record<string, unknown>,
|
|
142
|
+
>(
|
|
143
|
+
hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
|
|
119
144
|
args:
|
|
120
145
|
| {
|
|
146
|
+
listKey: string
|
|
121
147
|
operation: 'create'
|
|
148
|
+
inputData: TCreateInput
|
|
149
|
+
resolvedData: TCreateInput
|
|
122
150
|
context: AccessContext
|
|
123
151
|
}
|
|
124
152
|
| {
|
|
125
|
-
|
|
153
|
+
listKey: string
|
|
154
|
+
operation: 'update'
|
|
155
|
+
inputData: TUpdateInput
|
|
156
|
+
item: TOutput
|
|
157
|
+
resolvedData: TUpdateInput
|
|
158
|
+
context: AccessContext
|
|
159
|
+
}
|
|
160
|
+
| {
|
|
161
|
+
listKey: string
|
|
162
|
+
operation: 'delete'
|
|
126
163
|
item: TOutput
|
|
127
164
|
context: AccessContext
|
|
128
165
|
},
|
|
@@ -131,25 +168,40 @@ export async function executeBeforeOperation<TOutput = Record<string, unknown>>(
|
|
|
131
168
|
return
|
|
132
169
|
}
|
|
133
170
|
|
|
134
|
-
await hooks.beforeOperation(args)
|
|
171
|
+
await hooks.beforeOperation(args as Parameters<typeof hooks.beforeOperation>[0])
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
/**
|
|
138
175
|
* Execute afterOperation hook
|
|
139
176
|
* Runs after database operation
|
|
140
177
|
*/
|
|
141
|
-
export async function executeAfterOperation<
|
|
142
|
-
|
|
178
|
+
export async function executeAfterOperation<
|
|
179
|
+
TOutput = Record<string, unknown>,
|
|
180
|
+
TCreateInput = Record<string, unknown>,
|
|
181
|
+
TUpdateInput = Record<string, unknown>,
|
|
182
|
+
>(
|
|
183
|
+
hooks: Hooks<TOutput, TCreateInput, TUpdateInput> | undefined,
|
|
143
184
|
args:
|
|
144
185
|
| {
|
|
186
|
+
listKey: string
|
|
145
187
|
operation: 'create'
|
|
188
|
+
inputData: TCreateInput
|
|
146
189
|
item: TOutput
|
|
147
|
-
|
|
190
|
+
resolvedData: TCreateInput
|
|
148
191
|
context: AccessContext
|
|
149
192
|
}
|
|
150
193
|
| {
|
|
151
|
-
|
|
194
|
+
listKey: string
|
|
195
|
+
operation: 'update'
|
|
196
|
+
inputData: TUpdateInput
|
|
197
|
+
originalItem: TOutput
|
|
152
198
|
item: TOutput
|
|
199
|
+
resolvedData: TUpdateInput
|
|
200
|
+
context: AccessContext
|
|
201
|
+
}
|
|
202
|
+
| {
|
|
203
|
+
listKey: string
|
|
204
|
+
operation: 'delete'
|
|
153
205
|
originalItem: TOutput
|
|
154
206
|
context: AccessContext
|
|
155
207
|
},
|
|
@@ -158,7 +210,7 @@ export async function executeAfterOperation<TOutput = Record<string, unknown>>(
|
|
|
158
210
|
return
|
|
159
211
|
}
|
|
160
212
|
|
|
161
|
-
await hooks.afterOperation(args)
|
|
213
|
+
await hooks.afterOperation(args as Parameters<typeof hooks.afterOperation>[0])
|
|
162
214
|
}
|
|
163
215
|
|
|
164
216
|
/**
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,17 @@ export type {
|
|
|
38
38
|
Plugin,
|
|
39
39
|
PluginContext,
|
|
40
40
|
GeneratedFiles,
|
|
41
|
+
// List-level hook argument types
|
|
42
|
+
ResolveInputHookArgs,
|
|
43
|
+
ValidateHookArgs,
|
|
44
|
+
BeforeOperationHookArgs,
|
|
45
|
+
AfterOperationHookArgs,
|
|
46
|
+
// Field-level hook argument types
|
|
47
|
+
FieldResolveInputHookArgs,
|
|
48
|
+
FieldValidateHookArgs,
|
|
49
|
+
FieldBeforeOperationHookArgs,
|
|
50
|
+
FieldAfterOperationHookArgs,
|
|
51
|
+
FieldResolveOutputHookArgs,
|
|
41
52
|
} from './config/index.js'
|
|
42
53
|
|
|
43
54
|
// Access control
|
package/tests/access.test.ts
CHANGED
|
@@ -192,60 +192,57 @@ describe('Access Control', () => {
|
|
|
192
192
|
expect(result).toBe(true)
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it('should
|
|
196
|
-
const
|
|
195
|
+
it('should receive inputData for create operations', async () => {
|
|
196
|
+
const inputData = { title: 'Test', authorId: '123' }
|
|
197
197
|
const fieldAccess: FieldAccess = {
|
|
198
|
-
|
|
198
|
+
create: vi.fn(async ({ inputData: data }) => {
|
|
199
|
+
// Field access can validate inputData
|
|
200
|
+
return data?.authorId === '123'
|
|
201
|
+
}),
|
|
199
202
|
}
|
|
200
203
|
|
|
201
|
-
const result = await checkFieldAccess(fieldAccess, '
|
|
204
|
+
const result = await checkFieldAccess(fieldAccess, 'create', {
|
|
202
205
|
session: null,
|
|
203
|
-
item,
|
|
204
206
|
context: mockContext,
|
|
207
|
+
inputData,
|
|
205
208
|
})
|
|
206
209
|
|
|
207
210
|
expect(result).toBe(true)
|
|
211
|
+
expect(fieldAccess.create).toHaveBeenCalledWith(expect.objectContaining({ inputData }))
|
|
208
212
|
})
|
|
209
213
|
|
|
210
|
-
it('should
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
read: vi.fn(async () => ({ userId: '123' })),
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const result = await checkFieldAccess(fieldAccess, 'read', {
|
|
217
|
-
session: null,
|
|
218
|
-
item,
|
|
219
|
-
context: mockContext,
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
expect(result).toBe(false)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('should work with equals condition', async () => {
|
|
226
|
-
const item = { status: 'active' }
|
|
214
|
+
it('should receive inputData for update operations', async () => {
|
|
215
|
+
const inputData = { title: 'Updated', authorId: '123' }
|
|
216
|
+
const item = { id: '1', authorId: '123' }
|
|
227
217
|
const fieldAccess: FieldAccess = {
|
|
228
|
-
|
|
218
|
+
update: vi.fn(async ({ inputData: data, item: existingItem }) => {
|
|
219
|
+
// Field access can validate inputData and check existing item
|
|
220
|
+
return data?.authorId === existingItem?.authorId
|
|
221
|
+
}),
|
|
229
222
|
}
|
|
230
223
|
|
|
231
|
-
const result = await checkFieldAccess(fieldAccess, '
|
|
224
|
+
const result = await checkFieldAccess(fieldAccess, 'update', {
|
|
232
225
|
session: null,
|
|
233
226
|
item,
|
|
234
227
|
context: mockContext,
|
|
228
|
+
inputData,
|
|
235
229
|
})
|
|
236
230
|
|
|
237
231
|
expect(result).toBe(true)
|
|
232
|
+
expect(fieldAccess.update).toHaveBeenCalledWith(expect.objectContaining({ inputData, item }))
|
|
238
233
|
})
|
|
239
234
|
|
|
240
|
-
it('should
|
|
241
|
-
const item = { status: 'active' }
|
|
235
|
+
it('should not receive inputData for read operations', async () => {
|
|
242
236
|
const fieldAccess: FieldAccess = {
|
|
243
|
-
read: vi.fn(async () =>
|
|
237
|
+
read: vi.fn(async ({ inputData }) => {
|
|
238
|
+
// inputData should be undefined for read operations
|
|
239
|
+
expect(inputData).toBeUndefined()
|
|
240
|
+
return true
|
|
241
|
+
}),
|
|
244
242
|
}
|
|
245
243
|
|
|
246
244
|
const result = await checkFieldAccess(fieldAccess, 'read', {
|
|
247
245
|
session: null,
|
|
248
|
-
item,
|
|
249
246
|
context: mockContext,
|
|
250
247
|
})
|
|
251
248
|
|
|
@@ -487,12 +484,15 @@ describe('Access Control', () => {
|
|
|
487
484
|
session: { userId: '123' },
|
|
488
485
|
item,
|
|
489
486
|
context: mockContext,
|
|
487
|
+
inputData: data,
|
|
490
488
|
})
|
|
491
489
|
|
|
492
490
|
expect(accessFn).toHaveBeenCalledWith({
|
|
493
491
|
session: { userId: '123' },
|
|
494
492
|
item,
|
|
495
493
|
context: mockContext,
|
|
494
|
+
inputData: data,
|
|
495
|
+
operation: 'update',
|
|
496
496
|
})
|
|
497
497
|
})
|
|
498
498
|
})
|
package/tests/config.test.ts
CHANGED
|
@@ -76,7 +76,7 @@ describe('config helpers', () => {
|
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
describe('list', () => {
|
|
79
|
-
it('should return
|
|
79
|
+
it('should return normalized list config', () => {
|
|
80
80
|
const testList: ListConfig = {
|
|
81
81
|
fields: {
|
|
82
82
|
name: { type: 'text' },
|
|
@@ -85,7 +85,9 @@ describe('config helpers', () => {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const result = list(testList)
|
|
88
|
-
|
|
88
|
+
// list() normalizes access control, so it creates a new object
|
|
89
|
+
expect(result.fields).toEqual(testList.fields)
|
|
90
|
+
expect(result.access).toBeUndefined()
|
|
89
91
|
})
|
|
90
92
|
|
|
91
93
|
it('should support text fields', () => {
|
|
@@ -161,7 +163,7 @@ describe('config helpers', () => {
|
|
|
161
163
|
expect(testList.fields.author.type).toBe('relationship')
|
|
162
164
|
})
|
|
163
165
|
|
|
164
|
-
it('should support access control', () => {
|
|
166
|
+
it('should support access control object form', () => {
|
|
165
167
|
const testList = list({
|
|
166
168
|
fields: { name: { type: 'text' } },
|
|
167
169
|
access: {
|
|
@@ -177,6 +179,21 @@ describe('config helpers', () => {
|
|
|
177
179
|
expect(testList.access?.operation).toBeDefined()
|
|
178
180
|
})
|
|
179
181
|
|
|
182
|
+
it('should support access control function shorthand', () => {
|
|
183
|
+
const isAdmin = () => true
|
|
184
|
+
const testList = list({
|
|
185
|
+
fields: { name: { type: 'text' } },
|
|
186
|
+
access: isAdmin,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Function shorthand should be normalized to object form
|
|
190
|
+
expect(testList.access?.operation).toBeDefined()
|
|
191
|
+
expect(testList.access?.operation?.query).toBe(isAdmin)
|
|
192
|
+
expect(testList.access?.operation?.create).toBe(isAdmin)
|
|
193
|
+
expect(testList.access?.operation?.update).toBe(isAdmin)
|
|
194
|
+
expect(testList.access?.operation?.delete).toBe(isAdmin)
|
|
195
|
+
})
|
|
196
|
+
|
|
180
197
|
it('should support hooks', () => {
|
|
181
198
|
const testList = list({
|
|
182
199
|
fields: { name: { type: 'text' } },
|
|
@@ -50,7 +50,10 @@ describe('Nested Operations - Access Control and Hooks', () => {
|
|
|
50
50
|
|
|
51
51
|
describe('Nested Create Operations', () => {
|
|
52
52
|
it('should run hooks and access control for nested create', async () => {
|
|
53
|
-
const userResolveInputHook = vi.fn(async ({
|
|
53
|
+
const userResolveInputHook = vi.fn(async ({ resolvedData, fieldKey }) => {
|
|
54
|
+
const value = resolvedData[fieldKey]
|
|
55
|
+
return typeof value === 'string' ? value.toUpperCase() : value
|
|
56
|
+
})
|
|
54
57
|
const userListResolveInputHook = vi.fn(async ({ resolvedData }) => resolvedData)
|
|
55
58
|
const userValidateInputHook = vi.fn(async () => {})
|
|
56
59
|
const postResolveInputHook = vi.fn(async ({ resolvedData }) => resolvedData)
|
|
@@ -140,9 +143,11 @@ describe('Nested Operations - Access Control and Hooks', () => {
|
|
|
140
143
|
|
|
141
144
|
expect(userResolveInputHook).toHaveBeenCalledWith(
|
|
142
145
|
expect.objectContaining({
|
|
143
|
-
|
|
146
|
+
fieldKey: 'name',
|
|
144
147
|
operation: 'create',
|
|
145
|
-
|
|
148
|
+
resolvedData: expect.objectContaining({
|
|
149
|
+
name: 'john',
|
|
150
|
+
}),
|
|
146
151
|
}),
|
|
147
152
|
)
|
|
148
153
|
|