feat: filter query preset constraints (#12485)

You can now specify exactly who can change the constraints within a
query preset.

For example, you want to ensure that only "admins" are allowed to set a
preset to "everyone".

To do this, you can use the new `queryPresets.filterConstraints`
property. When a user lacks the permission to change a constraint, the
option will either be hidden from them or disabled if it is already set.

```ts
import { buildConfig } from 'payload'

const config = buildConfig({
  // ...
  queryPresets: {
    // ...
    filterConstraints: ({ req, options }) =>
      !req.user?.roles?.includes('admin')
        ? options.filter(
            (option) =>
              (typeof option === 'string' ? option : option.value) !==
              'everyone',
          )
        : options,
  },
})
```

The `filterConstraints` functions takes the same arguments as
`reduceOptions` property on select fields introduced in #12487.
This commit is contained in:
Jacob Fletcher
2025-05-27 16:55:37 -04:00
committed by GitHub
parent 032375b016
commit 0204f0dcbc
9 changed files with 195 additions and 42 deletions

View File

@@ -11,6 +11,7 @@ import { seed } from './seed.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
// eslint-disable-next-line no-restricted-exports
export default buildConfigWithDefaults({
admin: {
importMap: {
@@ -26,6 +27,12 @@ export default buildConfigWithDefaults({
read: ({ req: { user } }) => Boolean(user?.roles?.length && !user?.roles?.includes('user')),
update: ({ req: { user } }) => Boolean(user?.roles?.length && !user?.roles?.includes('user')),
},
filterConstraints: ({ req, options }) =>
!req.user?.roles?.includes('admin')
? options.filter(
(option) => (typeof option === 'string' ? option : option.value) !== 'onlyAdmins',
)
: options,
constraints: {
read: [
{
@@ -43,6 +50,11 @@ export default buildConfigWithDefaults({
value: 'noone',
access: () => false,
},
{
label: 'Only Admins',
value: 'onlyAdmins',
access: ({ req: { user } }) => Boolean(user?.roles?.includes('admin')),
},
],
update: [
{
@@ -55,6 +67,11 @@ export default buildConfigWithDefaults({
},
}),
},
{
label: 'Only Admins',
value: 'onlyAdmins',
access: ({ req: { user } }) => Boolean(user?.roles?.includes('admin')),
},
],
},
},

View File

@@ -545,6 +545,89 @@ describe('Query Presets', () => {
}
})
it('should only allow admins to select the "onlyAdmins" preset (via `filterOptions`)', async () => {
try {
const presetForAdminsCreatedByEditor = await payload.create({
collection: queryPresetsCollectionSlug,
user: editorUser,
overrideAccess: false,
data: {
title: 'Admins (Created by Editor)',
where: {
text: {
equals: 'example page',
},
},
access: {
read: {
constraint: 'onlyAdmins',
},
update: {
constraint: 'onlyAdmins',
},
},
relatedCollection: 'pages',
},
})
expect(presetForAdminsCreatedByEditor).toBeFalsy()
} catch (error: unknown) {
expect((error as Error).message).toBe(
'The following fields are invalid: Sharing settings > Read > Specify who can read this Preset, Sharing settings > Update > Specify who can update this Preset',
)
}
const presetForAdminsCreatedByAdmin = await payload.create({
collection: queryPresetsCollectionSlug,
user: adminUser,
overrideAccess: false,
data: {
title: 'Admins (Created by Admin)',
where: {
text: {
equals: 'example page',
},
},
access: {
read: {
constraint: 'onlyAdmins',
},
update: {
constraint: 'onlyAdmins',
},
},
relatedCollection: 'pages',
},
})
expect(presetForAdminsCreatedByAdmin).toBeDefined()
// attempt to update the preset using an editor user
try {
const presetUpdatedByEditorUser = await payload.update({
collection: queryPresetsCollectionSlug,
id: presetForAdminsCreatedByAdmin.id,
user: editorUser,
overrideAccess: false,
data: {
title: 'From `onlyAdmins` to `onlyMe` (Updated by Editor)',
access: {
read: {
constraint: 'onlyMe',
},
update: {
constraint: 'onlyMe',
},
},
},
})
expect(presetUpdatedByEditorUser).toBeFalsy()
} catch (error: unknown) {
expect((error as Error).message).toBe('You are not allowed to perform this action.')
}
})
it('should respect access when set to "specificRoles"', async () => {
const presetForSpecificRoles = await payload.create({
collection: queryPresetsCollectionSlug,

View File

@@ -227,12 +227,12 @@ export interface PayloadQueryPreset {
isShared?: boolean | null;
access?: {
read?: {
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles' | 'noone') | null;
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles' | 'noone' | 'onlyAdmins') | null;
users?: (string | User)[] | null;
roles?: ('admin' | 'editor' | 'user')[] | null;
};
update?: {
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles' | 'onlyAdmins') | null;
users?: (string | User)[] | null;
roles?: ('admin' | 'editor' | 'user')[] | null;
};