@opensaas/stack-core 0.13.0 → 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 +205 -0
- 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 +257 -98
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +133 -7
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +2 -0
- package/dist/context/nested-operations.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 +303 -113
- package/src/context/index.ts +176 -10
- package/src/context/nested-operations.ts +2 -0
- package/src/index.ts +11 -0
- package/tests/access.test.ts +28 -28
- package/tests/config.test.ts +20 -3
- package/tests/singleton.test.ts +329 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/context/index.ts
CHANGED
|
@@ -92,22 +92,23 @@ async function executeFieldValidateHooks(
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
for (const [fieldKey, fieldConfig] of Object.entries(fields)) {
|
|
95
|
-
//
|
|
96
|
-
|
|
95
|
+
// Support both 'validate' (new) and 'validateInput' (deprecated) for backwards compatibility
|
|
96
|
+
const validateHook = fieldConfig.hooks?.validate ?? fieldConfig.hooks?.validateInput
|
|
97
|
+
if (!validateHook) continue
|
|
97
98
|
|
|
98
99
|
// Execute field hook
|
|
99
100
|
// Type assertion is safe here because hooks are typed correctly in field definitions
|
|
100
101
|
if (operation === 'delete') {
|
|
101
|
-
await
|
|
102
|
+
await validateHook({
|
|
102
103
|
listKey,
|
|
103
104
|
fieldKey,
|
|
104
105
|
operation: 'delete',
|
|
105
106
|
item,
|
|
106
107
|
context,
|
|
107
108
|
addValidationError: addValidationError(fieldKey),
|
|
108
|
-
} as Parameters<typeof
|
|
109
|
+
} as Parameters<typeof validateHook>[0])
|
|
109
110
|
} else if (operation === 'create') {
|
|
110
|
-
await
|
|
111
|
+
await validateHook({
|
|
111
112
|
listKey,
|
|
112
113
|
fieldKey,
|
|
113
114
|
operation: 'create',
|
|
@@ -116,10 +117,10 @@ async function executeFieldValidateHooks(
|
|
|
116
117
|
resolvedData,
|
|
117
118
|
context,
|
|
118
119
|
addValidationError: addValidationError(fieldKey),
|
|
119
|
-
} as Parameters<typeof
|
|
120
|
+
} as Parameters<typeof validateHook>[0])
|
|
120
121
|
} else {
|
|
121
122
|
// operation === 'update'
|
|
122
|
-
await
|
|
123
|
+
await validateHook({
|
|
123
124
|
listKey,
|
|
124
125
|
fieldKey,
|
|
125
126
|
operation: 'update',
|
|
@@ -128,7 +129,7 @@ async function executeFieldValidateHooks(
|
|
|
128
129
|
resolvedData,
|
|
129
130
|
context,
|
|
130
131
|
addValidationError: addValidationError(fieldKey),
|
|
131
|
-
} as Parameters<typeof
|
|
132
|
+
} as Parameters<typeof validateHook>[0])
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -253,6 +254,49 @@ export type ServerActionProps =
|
|
|
253
254
|
| { listKey: string; action: 'update'; id: string; data: Record<string, unknown> }
|
|
254
255
|
| { listKey: string; action: 'delete'; id: string }
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Check if a list is configured as a singleton
|
|
259
|
+
*/
|
|
260
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
261
|
+
function isSingletonList(listConfig: ListConfig<any>): boolean {
|
|
262
|
+
return !!listConfig.isSingleton
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if auto-create is enabled for a singleton list
|
|
267
|
+
* Defaults to true if not explicitly set to false
|
|
268
|
+
*/
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
270
|
+
function shouldAutoCreate(listConfig: ListConfig<any>): boolean {
|
|
271
|
+
if (!listConfig.isSingleton) return false
|
|
272
|
+
if (typeof listConfig.isSingleton === 'boolean') return true
|
|
273
|
+
return listConfig.isSingleton.autoCreate !== false
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Extract default values from field configs
|
|
278
|
+
* Used to auto-create singleton records with sensible defaults
|
|
279
|
+
*/
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
281
|
+
function getDefaultData(listConfig: ListConfig<any>): Record<string, unknown> {
|
|
282
|
+
const data: Record<string, unknown> = {}
|
|
283
|
+
|
|
284
|
+
for (const [fieldKey, fieldConfig] of Object.entries(listConfig.fields)) {
|
|
285
|
+
// Skip virtual fields - they're not stored in database
|
|
286
|
+
if (fieldConfig.virtual) continue
|
|
287
|
+
|
|
288
|
+
// Skip system fields (id, createdAt, updatedAt)
|
|
289
|
+
if (fieldKey === 'id' || fieldKey === 'createdAt' || fieldKey === 'updatedAt') continue
|
|
290
|
+
|
|
291
|
+
// Add default value if present
|
|
292
|
+
if ('defaultValue' in fieldConfig && fieldConfig.defaultValue !== undefined) {
|
|
293
|
+
data[fieldKey] = fieldConfig.defaultValue
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return data
|
|
298
|
+
}
|
|
299
|
+
|
|
256
300
|
/**
|
|
257
301
|
* Parse Prisma error and convert to user-friendly DatabaseError
|
|
258
302
|
*/
|
|
@@ -392,14 +436,23 @@ export function getContext<
|
|
|
392
436
|
for (const [listName, listConfig] of Object.entries(config.lists)) {
|
|
393
437
|
const dbKey = getDbKey(listName)
|
|
394
438
|
|
|
395
|
-
|
|
439
|
+
// Create base operations
|
|
440
|
+
const createOp = createCreate(listName, listConfig, prisma, context, config)
|
|
441
|
+
const operations: Record<string, unknown> = {
|
|
396
442
|
findUnique: createFindUnique(listName, listConfig, prisma, context, config),
|
|
397
443
|
findMany: createFindMany(listName, listConfig, prisma, context, config),
|
|
398
|
-
create:
|
|
444
|
+
create: createOp,
|
|
399
445
|
update: createUpdate(listName, listConfig, prisma, context, config),
|
|
400
446
|
delete: createDelete(listName, listConfig, prisma, context),
|
|
401
447
|
count: createCount(listName, listConfig, prisma, context),
|
|
402
448
|
}
|
|
449
|
+
|
|
450
|
+
// Add get() method for singleton lists
|
|
451
|
+
if (isSingletonList(listConfig)) {
|
|
452
|
+
operations.get = createGet(listName, listConfig, prisma, context, config, createOp)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
db[dbKey] = operations
|
|
403
456
|
}
|
|
404
457
|
|
|
405
458
|
// Execute plugin runtime functions and populate context.plugins
|
|
@@ -617,6 +670,14 @@ function createFindMany<TPrisma extends PrismaClientLike>(
|
|
|
617
670
|
skip?: number
|
|
618
671
|
include?: Record<string, unknown>
|
|
619
672
|
}) => {
|
|
673
|
+
// Check singleton constraint (throw error instead of silently returning empty)
|
|
674
|
+
if (isSingletonList(listConfig)) {
|
|
675
|
+
throw new ValidationError(
|
|
676
|
+
[`Cannot use findMany: ${listName} is a singleton list. Use get() instead.`],
|
|
677
|
+
{},
|
|
678
|
+
)
|
|
679
|
+
}
|
|
680
|
+
|
|
620
681
|
// Check query access (skip if sudo mode)
|
|
621
682
|
let where: Record<string, unknown> | undefined = args?.where
|
|
622
683
|
if (!context._isSudo) {
|
|
@@ -696,6 +757,21 @@ function createCreate<TPrisma extends PrismaClientLike>(
|
|
|
696
757
|
config: OpenSaasConfig,
|
|
697
758
|
) {
|
|
698
759
|
return async (args: { data: Record<string, unknown> }) => {
|
|
760
|
+
// 0. Check singleton constraint (enforce even in sudo mode)
|
|
761
|
+
if (isSingletonList(listConfig)) {
|
|
762
|
+
// Access Prisma model dynamically - required because model names are generated at runtime
|
|
763
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
764
|
+
const model = (prisma as any)[getDbKey(listName)]
|
|
765
|
+
const existingCount = await model.count()
|
|
766
|
+
|
|
767
|
+
if (existingCount > 0) {
|
|
768
|
+
throw new ValidationError(
|
|
769
|
+
[`Cannot create: ${listName} is a singleton list with an existing record`],
|
|
770
|
+
{},
|
|
771
|
+
)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
699
775
|
// 1. Check create access (skip if sudo mode)
|
|
700
776
|
if (!context._isSudo) {
|
|
701
777
|
const createAccess = listConfig.access?.operation?.create
|
|
@@ -759,6 +835,7 @@ function createCreate<TPrisma extends PrismaClientLike>(
|
|
|
759
835
|
const filteredData = await filterWritableFields(resolvedData, listConfig.fields, 'create', {
|
|
760
836
|
session: context.session,
|
|
761
837
|
context: { ...context, _isSudo: context._isSudo },
|
|
838
|
+
inputData: args.data,
|
|
762
839
|
})
|
|
763
840
|
|
|
764
841
|
// 5.5. Process nested relationship operations
|
|
@@ -939,6 +1016,7 @@ function createUpdate<TPrisma extends PrismaClientLike>(
|
|
|
939
1016
|
session: context.session,
|
|
940
1017
|
item,
|
|
941
1018
|
context: { ...context, _isSudo: context._isSudo },
|
|
1019
|
+
inputData: args.data,
|
|
942
1020
|
})
|
|
943
1021
|
|
|
944
1022
|
// 6.5. Process nested relationship operations
|
|
@@ -1029,6 +1107,11 @@ function createDelete<TPrisma extends PrismaClientLike>(
|
|
|
1029
1107
|
context: AccessContext<TPrisma>,
|
|
1030
1108
|
) {
|
|
1031
1109
|
return async (args: { where: { id: string } }) => {
|
|
1110
|
+
// 0. Check singleton constraint (enforce even in sudo mode)
|
|
1111
|
+
if (isSingletonList(listConfig)) {
|
|
1112
|
+
throw new ValidationError([`Cannot delete: ${listName} is a singleton list`], {})
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1032
1115
|
// 1. Fetch the item to pass to access control and hooks
|
|
1033
1116
|
// Access Prisma model dynamically - required because model names are generated at runtime
|
|
1034
1117
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -1176,3 +1259,86 @@ function createCount<TPrisma extends PrismaClientLike>(
|
|
|
1176
1259
|
return count
|
|
1177
1260
|
}
|
|
1178
1261
|
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Create get operation for singleton lists
|
|
1265
|
+
* Returns the single record, or auto-creates it if enabled
|
|
1266
|
+
*/
|
|
1267
|
+
function createGet<TPrisma extends PrismaClientLike>(
|
|
1268
|
+
listName: string,
|
|
1269
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
1270
|
+
listConfig: ListConfig<any>,
|
|
1271
|
+
prisma: TPrisma,
|
|
1272
|
+
context: AccessContext<TPrisma>,
|
|
1273
|
+
config: OpenSaasConfig,
|
|
1274
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1275
|
+
createFn: any,
|
|
1276
|
+
) {
|
|
1277
|
+
return async () => {
|
|
1278
|
+
// First try to find the existing record
|
|
1279
|
+
// Access Prisma model dynamically - required because model names are generated at runtime
|
|
1280
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1281
|
+
const model = (prisma as any)[getDbKey(listName)]
|
|
1282
|
+
|
|
1283
|
+
// Check query access (skip if sudo mode)
|
|
1284
|
+
let where: Record<string, unknown> = {}
|
|
1285
|
+
if (!context._isSudo) {
|
|
1286
|
+
const queryAccess = listConfig.access?.operation?.query
|
|
1287
|
+
const accessResult = await checkAccess(queryAccess, {
|
|
1288
|
+
session: context.session,
|
|
1289
|
+
context,
|
|
1290
|
+
})
|
|
1291
|
+
|
|
1292
|
+
if (accessResult === false) {
|
|
1293
|
+
return null
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Merge access filter (for singleton, we don't have a specific where clause)
|
|
1297
|
+
if (accessResult && typeof accessResult === 'object') {
|
|
1298
|
+
where = accessResult
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Build include with access control filters
|
|
1303
|
+
const accessControlledInclude = await buildIncludeWithAccessControl(
|
|
1304
|
+
listConfig.fields,
|
|
1305
|
+
{
|
|
1306
|
+
session: context.session,
|
|
1307
|
+
context,
|
|
1308
|
+
},
|
|
1309
|
+
config,
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
// Try to find the record
|
|
1313
|
+
const item = await model.findFirst({
|
|
1314
|
+
where,
|
|
1315
|
+
include: accessControlledInclude,
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
// If record exists, return it
|
|
1319
|
+
if (item) {
|
|
1320
|
+
// Filter readable fields and apply resolveOutput hooks
|
|
1321
|
+
const filtered = await filterReadableFields(
|
|
1322
|
+
item,
|
|
1323
|
+
listConfig.fields,
|
|
1324
|
+
{
|
|
1325
|
+
session: context.session,
|
|
1326
|
+
context: { ...context, _isSudo: context._isSudo },
|
|
1327
|
+
},
|
|
1328
|
+
config,
|
|
1329
|
+
0,
|
|
1330
|
+
listName,
|
|
1331
|
+
)
|
|
1332
|
+
return filtered
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// If no record and auto-create is enabled, create it
|
|
1336
|
+
if (shouldAutoCreate(listConfig)) {
|
|
1337
|
+
const defaultData = getDefaultData(listConfig)
|
|
1338
|
+
return await createFn({ data: defaultData })
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// No record and auto-create is disabled
|
|
1342
|
+
return null
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
@@ -140,6 +140,7 @@ async function processNestedCreate(
|
|
|
140
140
|
{
|
|
141
141
|
session: context.session,
|
|
142
142
|
context,
|
|
143
|
+
inputData: item,
|
|
143
144
|
},
|
|
144
145
|
)
|
|
145
146
|
|
|
@@ -309,6 +310,7 @@ async function processNestedUpdate(
|
|
|
309
310
|
session: context.session,
|
|
310
311
|
item,
|
|
311
312
|
context,
|
|
313
|
+
inputData: updateData,
|
|
312
314
|
},
|
|
313
315
|
)
|
|
314
316
|
|
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' } },
|