feat: prevent query preset lockout (#12322)
Prevents an accidental lockout of query preset documents. An "accidental lockout" occurs when the user sets access control on a preset and excludes themselves. This can happen in a variety of scenarios, including: - You select `specificUsers` without specifying yourself - You select `specificRoles` without specifying a role that you are a part of - Etc. #### How it works To make this happen, we use a custom validation function that executes access against the user's proposed changes. If those changes happen to remove access for them, we throw a validation error and prevent that change from ever taking place. This means that only a user with proper access can remove another user from the preset. You cannot remove yourself. To do this, we create a temporary record in the database that we can query against. We use transactions to ensure that the temporary record is not persisted once our work is completed. Since not all Payload projects have transactions enabled, we flag these temporary records with the `isTemp` field. Once created, we query the temp document to determine its permissions. If any of the operations throw an error, this means the user can no longer act on them, and we throw a validation error. #### Alternative Approach A previous approach that was explored was to add an `owner` field to the presets collection. This way, the "owner" of the preset would be able to completely bypass all access control, effectively eliminating the possibility of a lockout event. But this doesn't work for other users who may have update access. E.g. they could still accidentally remove themselves from the read or update operation, preventing them from accessing that preset after submitting the form. We need a solution that works for all users, not just the owner.
This commit is contained in:
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -63,6 +63,13 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal"
|
"type": "node-terminal"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "pnpm tsx --no-deprecation test/dev.ts query-presets",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"name": "Run Dev Query Presets",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
|
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export type BeforeChangeRichTextHookArgs<
|
|||||||
previousSiblingDoc?: TData
|
previousSiblingDoc?: TData
|
||||||
/** The previous value of the field, before changes */
|
/** The previous value of the field, before changes */
|
||||||
previousValue?: TValue
|
previousValue?: TValue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The original siblingData with locales (not modified by any hooks).
|
* The original siblingData with locales (not modified by any hooks).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export default async function createLocal<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const req = await createLocalReq(options, payload)
|
const req = await createLocalReq(options, payload)
|
||||||
|
|
||||||
req.file = file ?? (await getFileByPath(filePath))
|
req.file = file ?? (await getFileByPath(filePath))
|
||||||
|
|
||||||
return createOperation<TSlug, TSelect>({
|
return createOperation<TSlug, TSelect>({
|
||||||
|
|||||||
@@ -113,6 +113,16 @@ export const getQueryPresetsConfig = (config: Config): CollectionConfig => ({
|
|||||||
: [],
|
: [],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'isTemp',
|
||||||
|
type: 'checkbox',
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"This is a tempoary field used to determine if updating the preset would remove the user's access to it. When `true`, this record will be deleted after running the preset's `validate` function.",
|
||||||
|
disabled: true,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeValidate: [
|
beforeValidate: [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Field } from '../fields/config/types.js'
|
|||||||
|
|
||||||
import { fieldAffectsData } from '../fields/config/types.js'
|
import { fieldAffectsData } from '../fields/config/types.js'
|
||||||
import { toWords } from '../utilities/formatLabels.js'
|
import { toWords } from '../utilities/formatLabels.js'
|
||||||
|
import { preventLockout } from './preventLockout.js'
|
||||||
import { operations, type QueryPresetConstraint } from './types.js'
|
import { operations, type QueryPresetConstraint } from './types.js'
|
||||||
|
|
||||||
export const getConstraints = (config: Config): Field => ({
|
export const getConstraints = (config: Config): Field => ({
|
||||||
@@ -101,4 +102,5 @@ export const getConstraints = (config: Config): Field => ({
|
|||||||
label: () => toWords(operation),
|
label: () => toWords(operation),
|
||||||
})),
|
})),
|
||||||
label: 'Sharing settings',
|
label: 'Sharing settings',
|
||||||
|
validate: preventLockout,
|
||||||
})
|
})
|
||||||
|
|||||||
92
packages/payload/src/query-presets/preventLockout.ts
Normal file
92
packages/payload/src/query-presets/preventLockout.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import type { Validate } from '../fields/config/types.js'
|
||||||
|
|
||||||
|
import { APIError } from '../errors/APIError.js'
|
||||||
|
import { createLocalReq } from '../utilities/createLocalReq.js'
|
||||||
|
import { initTransaction } from '../utilities/initTransaction.js'
|
||||||
|
import { killTransaction } from '../utilities/killTransaction.js'
|
||||||
|
import { queryPresetsCollectionSlug } from './config.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents "accidental lockouts" where a user makes an update that removes their own access to the preset.
|
||||||
|
* This is effectively an access control function proxied through a `validate` function.
|
||||||
|
* How it works:
|
||||||
|
* 1. Creates a temporary record with the incoming data
|
||||||
|
* 2. Attempts to read and update that record with the incoming user
|
||||||
|
* 3. If either of those fail, throws an error to the user
|
||||||
|
* 4. Once finished, prevents the temp record from persisting to the database
|
||||||
|
*/
|
||||||
|
export const preventLockout: Validate = async (
|
||||||
|
value,
|
||||||
|
{ data, overrideAccess, req: incomingReq },
|
||||||
|
) => {
|
||||||
|
// Use context to ensure an infinite loop doesn't occur
|
||||||
|
if (!incomingReq.context._preventLockout && !overrideAccess) {
|
||||||
|
const req = await createLocalReq(
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
_preventLockout: true,
|
||||||
|
},
|
||||||
|
req: {
|
||||||
|
user: incomingReq.user,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
incomingReq.payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Might be `null` if no transactions are enabled
|
||||||
|
const transaction = await initTransaction(req)
|
||||||
|
|
||||||
|
// create a temp record to validate the constraints, using the req
|
||||||
|
const tempPreset = await req.payload.create({
|
||||||
|
collection: queryPresetsCollectionSlug,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
isTemp: true,
|
||||||
|
},
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
|
let canUpdate = false
|
||||||
|
let canRead = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
await req.payload.findByID({
|
||||||
|
id: tempPreset.id,
|
||||||
|
collection: queryPresetsCollectionSlug,
|
||||||
|
overrideAccess: false,
|
||||||
|
req,
|
||||||
|
user: req.user,
|
||||||
|
})
|
||||||
|
|
||||||
|
canRead = true
|
||||||
|
|
||||||
|
await req.payload.update({
|
||||||
|
id: tempPreset.id,
|
||||||
|
collection: queryPresetsCollectionSlug,
|
||||||
|
data: tempPreset,
|
||||||
|
overrideAccess: false,
|
||||||
|
req,
|
||||||
|
user: req.user,
|
||||||
|
})
|
||||||
|
|
||||||
|
canUpdate = true
|
||||||
|
} catch (_err) {
|
||||||
|
if (!canRead || !canUpdate) {
|
||||||
|
throw new APIError('Cannot remove yourself from this preset.', 403, {}, true)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (transaction) {
|
||||||
|
await killTransaction(req)
|
||||||
|
} else {
|
||||||
|
// delete the temp record
|
||||||
|
await req.payload.delete({
|
||||||
|
id: tempPreset.id,
|
||||||
|
collection: queryPresetsCollectionSlug,
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true as unknown as true
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
|
|||||||
overrideAccess: true,
|
overrideAccess: true,
|
||||||
req,
|
req,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operation === 'readVersions') {
|
if (operation === 'readVersions') {
|
||||||
const paginatedRes = await payload.findVersions({
|
const paginatedRes = await payload.findVersions({
|
||||||
...options,
|
...options,
|
||||||
@@ -91,6 +92,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
|
|||||||
pagination: false,
|
pagination: false,
|
||||||
where: combineQueries(where, { id: { equals: id } }),
|
where: combineQueries(where, { id: { equals: id } }),
|
||||||
})
|
})
|
||||||
|
|
||||||
return paginatedRes?.docs?.[0] || undefined
|
return paginatedRes?.docs?.[0] || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
|
|||||||
docBeingAccessed = doc
|
docBeingAccessed = doc
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// awaiting the promise to ensure docBeingAccessed is assigned before it is used
|
// awaiting the promise to ensure docBeingAccessed is assigned before it is used
|
||||||
await docBeingAccessed
|
await docBeingAccessed
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export const useQueryPresets = ({
|
|||||||
const filterOptions = useMemo(
|
const filterOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
'payload-query-presets': {
|
'payload-query-presets': {
|
||||||
|
isTemp: {
|
||||||
|
not_equals: true,
|
||||||
|
},
|
||||||
relatedCollection: {
|
relatedCollection: {
|
||||||
equals: collectionSlug,
|
equals: collectionSlug,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const QueryPresetsWhereField: JSONFieldClientComponent = ({
|
|||||||
{value
|
{value
|
||||||
? transformWhereToNaturalLanguage(
|
? transformWhereToNaturalLanguage(
|
||||||
value as Where,
|
value as Where,
|
||||||
getTranslation(collectionConfig.labels.plural, i18n),
|
getTranslation(collectionConfig?.labels?.plural, i18n),
|
||||||
)
|
)
|
||||||
: 'No where query'}
|
: 'No where query'}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,4 @@ export const Pages: CollectionConfig = {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
versions: {
|
|
||||||
drafts: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,4 @@ export const Posts: CollectionConfig = {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
versions: {
|
|
||||||
drafts: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ export default buildConfigWithDefaults({
|
|||||||
// plural: 'Reports',
|
// plural: 'Reports',
|
||||||
// },
|
// },
|
||||||
access: {
|
access: {
|
||||||
read: ({ req: { user } }) =>
|
read: ({ req: { user } }) => Boolean(user?.roles?.length && !user?.roles?.includes('user')),
|
||||||
user ? user && !user?.roles?.some((role) => role === 'anonymous') : false,
|
update: ({ req: { user } }) => Boolean(user?.roles?.length && !user?.roles?.includes('user')),
|
||||||
update: ({ req: { user } }) =>
|
|
||||||
user ? user && !user?.roles?.some((role) => role === 'anonymous') : false,
|
|
||||||
},
|
},
|
||||||
constraints: {
|
constraints: {
|
||||||
read: [
|
read: [
|
||||||
@@ -60,7 +58,7 @@ export default buildConfigWithDefaults({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collections: [Pages, Users, Posts],
|
collections: [Pages, Posts, Users],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||||
await seed(payload)
|
await seed(payload)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as path from 'path'
|
|||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||||
import type { Config } from './payload-types.js'
|
import type { Config, PayloadQueryPreset } from './payload-types.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
@@ -39,6 +39,13 @@ let serverURL: string
|
|||||||
let everyoneID: string | undefined
|
let everyoneID: string | undefined
|
||||||
let context: BrowserContext
|
let context: BrowserContext
|
||||||
let user: any
|
let user: any
|
||||||
|
let ownerUser: any
|
||||||
|
|
||||||
|
let seededData: {
|
||||||
|
everyone: PayloadQueryPreset
|
||||||
|
onlyMe: PayloadQueryPreset
|
||||||
|
specificUsers: PayloadQueryPreset
|
||||||
|
}
|
||||||
|
|
||||||
describe('Query Presets', () => {
|
describe('Query Presets', () => {
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
@@ -60,6 +67,19 @@ describe('Query Presets', () => {
|
|||||||
})
|
})
|
||||||
?.then((res) => res.user) // TODO: this type is wrong
|
?.then((res) => res.user) // TODO: this type is wrong
|
||||||
|
|
||||||
|
ownerUser = await payload
|
||||||
|
.find({
|
||||||
|
collection: 'users',
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
equals: 'Owner',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
depth: 0,
|
||||||
|
})
|
||||||
|
?.then((res) => res.docs[0])
|
||||||
|
|
||||||
initPageConsoleErrorCatch(page)
|
initPageConsoleErrorCatch(page)
|
||||||
|
|
||||||
await ensureCompilationIsDone({ page, serverURL })
|
await ensureCompilationIsDone({ page, serverURL })
|
||||||
@@ -83,7 +103,7 @@ describe('Query Presets', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const [, everyone] = await Promise.all([
|
const [, everyone, onlyMe, specificUsers] = await Promise.all([
|
||||||
payload.delete({
|
payload.delete({
|
||||||
collection: 'payload-preferences',
|
collection: 'payload-preferences',
|
||||||
where: {
|
where: {
|
||||||
@@ -106,18 +126,24 @@ describe('Query Presets', () => {
|
|||||||
}),
|
}),
|
||||||
payload.create({
|
payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
data: seedData.everyone,
|
data: seedData.everyone({ ownerUserID: ownerUser?.id || '' }),
|
||||||
}),
|
}),
|
||||||
payload.create({
|
payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
data: seedData.onlyMe,
|
data: seedData.onlyMe({ ownerUserID: ownerUser?.id || '' }),
|
||||||
}),
|
}),
|
||||||
payload.create({
|
payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
data: seedData.specificUsers({ userID: user?.id || '' }),
|
data: seedData.specificUsers({ ownerUserID: ownerUser?.id || '', adminUserID: user.id }),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
seededData = {
|
||||||
|
everyone,
|
||||||
|
onlyMe,
|
||||||
|
specificUsers,
|
||||||
|
}
|
||||||
|
|
||||||
everyoneID = everyone.id
|
everyoneID = everyone.id
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in beforeEach:', error)
|
console.error('Error in beforeEach:', error)
|
||||||
@@ -126,12 +152,12 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
test('should select preset and apply filters', async () => {
|
test('should select preset and apply filters', async () => {
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
|
|
||||||
await assertURLParams({
|
await assertURLParams({
|
||||||
page,
|
page,
|
||||||
columns: seedData.everyone.columns,
|
columns: seededData.everyone.columns,
|
||||||
where: seedData.everyone.where,
|
where: seededData.everyone.where,
|
||||||
presetID: everyoneID,
|
presetID: everyoneID,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -140,14 +166,14 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
test('should clear selected preset and reset filters', async () => {
|
test('should clear selected preset and reset filters', async () => {
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
await clearSelectedPreset({ page })
|
await clearSelectedPreset({ page })
|
||||||
expect(true).toBe(true)
|
expect(true).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should delete a preset, clear selection, and reset changes', async () => {
|
test('should delete a preset, clear selection, and reset changes', async () => {
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
await openListMenu({ page })
|
await openListMenu({ page })
|
||||||
|
|
||||||
await clickListMenuItem({ page, menuItemLabel: 'Delete' })
|
await clickListMenuItem({ page, menuItemLabel: 'Delete' })
|
||||||
@@ -172,21 +198,21 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
modal.locator('tbody tr td button', {
|
modal.locator('tbody tr td button', {
|
||||||
hasText: exactText(seedData.everyone.title),
|
hasText: exactText(seededData.everyone.title),
|
||||||
}),
|
}),
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should save last used preset to preferences and load on initial render', async () => {
|
test('should save last used preset to preferences and load on initial render', async () => {
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
|
|
||||||
await page.reload()
|
await page.reload()
|
||||||
|
|
||||||
await assertURLParams({
|
await assertURLParams({
|
||||||
page,
|
page,
|
||||||
columns: seedData.everyone.columns,
|
columns: seededData.everyone.columns,
|
||||||
where: seedData.everyone.where,
|
where: seededData.everyone.where,
|
||||||
// presetID: everyoneID,
|
// presetID: everyoneID,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -209,7 +235,7 @@ describe('Query Presets', () => {
|
|||||||
}),
|
}),
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
|
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
|
|
||||||
await openListMenu({ page })
|
await openListMenu({ page })
|
||||||
|
|
||||||
@@ -249,7 +275,7 @@ describe('Query Presets', () => {
|
|||||||
}),
|
}),
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
|
|
||||||
await selectPreset({ page, presetTitle: seedData.onlyMe.title })
|
await selectPreset({ page, presetTitle: seededData.onlyMe.title })
|
||||||
|
|
||||||
await toggleColumn(page, { columnLabel: 'ID' })
|
await toggleColumn(page, { columnLabel: 'ID' })
|
||||||
|
|
||||||
@@ -271,7 +297,7 @@ describe('Query Presets', () => {
|
|||||||
test('should conditionally render "update for everyone" label based on if preset is shared', async () => {
|
test('should conditionally render "update for everyone" label based on if preset is shared', async () => {
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
|
|
||||||
await selectPreset({ page, presetTitle: seedData.onlyMe.title })
|
await selectPreset({ page, presetTitle: seededData.onlyMe.title })
|
||||||
|
|
||||||
await toggleColumn(page, { columnLabel: 'ID' })
|
await toggleColumn(page, { columnLabel: 'ID' })
|
||||||
|
|
||||||
@@ -284,7 +310,7 @@ describe('Query Presets', () => {
|
|||||||
}),
|
}),
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
|
|
||||||
await toggleColumn(page, { columnLabel: 'ID' })
|
await toggleColumn(page, { columnLabel: 'ID' })
|
||||||
|
|
||||||
@@ -300,7 +326,7 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
test('should reset active changes', async () => {
|
test('should reset active changes', async () => {
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
|
|
||||||
const { columnContainer } = await toggleColumn(page, { columnLabel: 'ID' })
|
const { columnContainer } = await toggleColumn(page, { columnLabel: 'ID' })
|
||||||
|
|
||||||
@@ -318,7 +344,7 @@ describe('Query Presets', () => {
|
|||||||
test('should only enter modified state when changes are made to an active preset', async () => {
|
test('should only enter modified state when changes are made to an active preset', async () => {
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
||||||
await toggleColumn(page, { columnLabel: 'ID' })
|
await toggleColumn(page, { columnLabel: 'ID' })
|
||||||
await expect(page.locator('.list-controls__modified')).toBeVisible()
|
await expect(page.locator('.list-controls__modified')).toBeVisible()
|
||||||
@@ -337,14 +363,14 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
await page.goto(pagesUrl.list)
|
await page.goto(pagesUrl.list)
|
||||||
|
|
||||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
await selectPreset({ page, presetTitle: seededData.everyone.title })
|
||||||
await clickListMenuItem({ page, menuItemLabel: 'Edit' })
|
await clickListMenuItem({ page, menuItemLabel: 'Edit' })
|
||||||
|
|
||||||
const drawer = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
const drawer = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
||||||
const titleValue = drawer.locator('input[name="title"]')
|
const titleValue = drawer.locator('input[name="title"]')
|
||||||
await expect(titleValue).toHaveValue(seedData.everyone.title)
|
await expect(titleValue).toHaveValue(seededData.everyone.title)
|
||||||
|
|
||||||
const newTitle = `${seedData.everyone.title} (Updated)`
|
const newTitle = `${seededData.everyone.title} (Updated)`
|
||||||
await drawer.locator('input[name="title"]').fill(newTitle)
|
await drawer.locator('input[name="title"]').fill(newTitle)
|
||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
@@ -391,9 +417,9 @@ describe('Query Presets', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('only shows query presets related to the underlying collection', async () => {
|
test('only shows query presets related to the underlying collection', async () => {
|
||||||
// no results on `users` collection
|
// no results on `posts` collection
|
||||||
const postsUrl = new AdminUrlUtil(serverURL, 'posts')
|
const postsURL = new AdminUrlUtil(serverURL, 'posts')
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsURL.list)
|
||||||
const drawer = await openQueryPresetDrawer({ page })
|
const drawer = await openQueryPresetDrawer({ page })
|
||||||
await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(0)
|
await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(0)
|
||||||
await expect(drawer.locator('.collection-list__no-results')).toBeVisible()
|
await expect(drawer.locator('.collection-list__no-results')).toBeVisible()
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ export const roles: Field = {
|
|||||||
label: 'Admin',
|
label: 'Admin',
|
||||||
value: 'admin',
|
value: 'admin',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Editor',
|
||||||
|
value: 'editor',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'User',
|
label: 'User',
|
||||||
value: 'user',
|
value: 'user',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Anonymous',
|
|
||||||
value: 'anonymous',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { NextRESTClient } from 'helpers/NextRESTClient.js'
|
|
||||||
import type { Payload, User } from 'payload'
|
import type { Payload, User } from 'payload'
|
||||||
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
@@ -10,10 +9,9 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
|||||||
const queryPresetsCollectionSlug = 'payload-query-presets'
|
const queryPresetsCollectionSlug = 'payload-query-presets'
|
||||||
|
|
||||||
let payload: Payload
|
let payload: Payload
|
||||||
let restClient: NextRESTClient
|
let adminUser: User
|
||||||
let user: User
|
let editorUser: User
|
||||||
let user2: User
|
let publicUser: User
|
||||||
let anonymousUser: User
|
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -21,9 +19,9 @@ const dirname = path.dirname(filename)
|
|||||||
describe('Query Presets', () => {
|
describe('Query Presets', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// @ts-expect-error: initPayloadInt does not have a proper type definition
|
// @ts-expect-error: initPayloadInt does not have a proper type definition
|
||||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
;({ payload } = await initPayloadInt(dirname))
|
||||||
|
|
||||||
user = await payload
|
adminUser = await payload
|
||||||
.login({
|
.login({
|
||||||
collection: 'users',
|
collection: 'users',
|
||||||
data: {
|
data: {
|
||||||
@@ -33,7 +31,7 @@ describe('Query Presets', () => {
|
|||||||
})
|
})
|
||||||
?.then((result) => result.user)
|
?.then((result) => result.user)
|
||||||
|
|
||||||
user2 = await payload
|
editorUser = await payload
|
||||||
.login({
|
.login({
|
||||||
collection: 'users',
|
collection: 'users',
|
||||||
data: {
|
data: {
|
||||||
@@ -43,11 +41,11 @@ describe('Query Presets', () => {
|
|||||||
})
|
})
|
||||||
?.then((result) => result.user)
|
?.then((result) => result.user)
|
||||||
|
|
||||||
anonymousUser = await payload
|
publicUser = await payload
|
||||||
.login({
|
.login({
|
||||||
collection: 'users',
|
collection: 'users',
|
||||||
data: {
|
data: {
|
||||||
email: 'anonymous@email.com',
|
email: 'public@email.com',
|
||||||
password: regularUser.password,
|
password: regularUser.password,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -155,7 +153,8 @@ describe('Query Presets', () => {
|
|||||||
it('should respect access when set to "specificUsers"', async () => {
|
it('should respect access when set to "specificUsers"', async () => {
|
||||||
const presetForSpecificUsers = await payload.create({
|
const presetForSpecificUsers = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user,
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Specific Users',
|
title: 'Specific Users',
|
||||||
where: {
|
where: {
|
||||||
@@ -166,11 +165,11 @@ describe('Query Presets', () => {
|
|||||||
access: {
|
access: {
|
||||||
read: {
|
read: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificUsers',
|
||||||
users: [user.id],
|
users: [adminUser.id],
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificUsers',
|
||||||
users: [user.id],
|
users: [adminUser.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relatedCollection: 'pages',
|
relatedCollection: 'pages',
|
||||||
@@ -180,7 +179,7 @@ describe('Query Presets', () => {
|
|||||||
const foundPresetWithUser1 = await payload.findByID({
|
const foundPresetWithUser1 = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForSpecificUsers.id,
|
id: presetForSpecificUsers.id,
|
||||||
})
|
})
|
||||||
@@ -188,53 +187,53 @@ describe('Query Presets', () => {
|
|||||||
expect(foundPresetWithUser1.id).toBe(presetForSpecificUsers.id)
|
expect(foundPresetWithUser1.id).toBe(presetForSpecificUsers.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const foundPresetWithUser2 = await payload.findByID({
|
const foundPresetWithEditorUser = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForSpecificUsers.id,
|
id: presetForSpecificUsers.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(foundPresetWithUser2).toBeFalsy()
|
expect(foundPresetWithEditorUser).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect((error as Error).message).toBe('Not Found')
|
expect((error as Error).message).toBe('Not Found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const presetUpdatedByUser1 = await payload.update({
|
const presetUpdatedByAdminUser = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForSpecificUsers.id,
|
id: presetForSpecificUsers.id,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Specific Users (Updated)',
|
title: 'Specific Users (Updated)',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(presetUpdatedByUser1.title).toBe('Specific Users (Updated)')
|
expect(presetUpdatedByAdminUser.title).toBe('Specific Users (Updated)')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const presetUpdatedByUser2 = await payload.update({
|
const presetUpdatedByEditorUser = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForSpecificUsers.id,
|
id: presetForSpecificUsers.id,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Specific Users (Updated)',
|
title: 'Specific Users (Updated)',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(presetUpdatedByUser2).toBeFalsy()
|
expect(presetUpdatedByEditorUser).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respect access when set to "onlyMe"', async () => {
|
it('should respect access when set to "onlyMe"', async () => {
|
||||||
// create a new doc so that the creating user is the owner
|
|
||||||
const presetForOnlyMe = await payload.create({
|
const presetForOnlyMe = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user,
|
overrideAccess: false,
|
||||||
|
user: adminUser,
|
||||||
data: {
|
data: {
|
||||||
title: 'Only Me',
|
title: 'Only Me',
|
||||||
where: {
|
where: {
|
||||||
@@ -257,7 +256,7 @@ describe('Query Presets', () => {
|
|||||||
const foundPresetWithUser1 = await payload.findByID({
|
const foundPresetWithUser1 = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForOnlyMe.id,
|
id: presetForOnlyMe.id,
|
||||||
})
|
})
|
||||||
@@ -265,15 +264,15 @@ describe('Query Presets', () => {
|
|||||||
expect(foundPresetWithUser1.id).toBe(presetForOnlyMe.id)
|
expect(foundPresetWithUser1.id).toBe(presetForOnlyMe.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const foundPresetWithUser2 = await payload.findByID({
|
const foundPresetWithEditorUser = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForOnlyMe.id,
|
id: presetForOnlyMe.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(foundPresetWithUser2).toBeFalsy()
|
expect(foundPresetWithEditorUser).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect((error as Error).message).toBe('Not Found')
|
expect((error as Error).message).toBe('Not Found')
|
||||||
}
|
}
|
||||||
@@ -281,7 +280,7 @@ describe('Query Presets', () => {
|
|||||||
const presetUpdatedByUser1 = await payload.update({
|
const presetUpdatedByUser1 = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForOnlyMe.id,
|
id: presetForOnlyMe.id,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Only Me (Updated)',
|
title: 'Only Me (Updated)',
|
||||||
@@ -291,17 +290,17 @@ describe('Query Presets', () => {
|
|||||||
expect(presetUpdatedByUser1.title).toBe('Only Me (Updated)')
|
expect(presetUpdatedByUser1.title).toBe('Only Me (Updated)')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const presetUpdatedByUser2 = await payload.update({
|
const presetUpdatedByEditorUser = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForOnlyMe.id,
|
id: presetForOnlyMe.id,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Only Me (Updated)',
|
title: 'Only Me (Updated)',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(presetUpdatedByUser2).toBeFalsy()
|
expect(presetUpdatedByEditorUser).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||||
}
|
}
|
||||||
@@ -310,7 +309,8 @@ describe('Query Presets', () => {
|
|||||||
it('should respect access when set to "everyone"', async () => {
|
it('should respect access when set to "everyone"', async () => {
|
||||||
const presetForEveryone = await payload.create({
|
const presetForEveryone = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user,
|
overrideAccess: false,
|
||||||
|
user: adminUser,
|
||||||
data: {
|
data: {
|
||||||
title: 'Everyone',
|
title: 'Everyone',
|
||||||
where: {
|
where: {
|
||||||
@@ -336,27 +336,27 @@ describe('Query Presets', () => {
|
|||||||
const foundPresetWithUser1 = await payload.findByID({
|
const foundPresetWithUser1 = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForEveryone.id,
|
id: presetForEveryone.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(foundPresetWithUser1.id).toBe(presetForEveryone.id)
|
expect(foundPresetWithUser1.id).toBe(presetForEveryone.id)
|
||||||
|
|
||||||
const foundPresetWithUser2 = await payload.findByID({
|
const foundPresetWithEditorUser = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForEveryone.id,
|
id: presetForEveryone.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(foundPresetWithUser2.id).toBe(presetForEveryone.id)
|
expect(foundPresetWithEditorUser.id).toBe(presetForEveryone.id)
|
||||||
|
|
||||||
const presetUpdatedByUser1 = await payload.update({
|
const presetUpdatedByUser1 = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForEveryone.id,
|
id: presetForEveryone.id,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Everyone (Update 1)',
|
title: 'Everyone (Update 1)',
|
||||||
@@ -365,17 +365,105 @@ describe('Query Presets', () => {
|
|||||||
|
|
||||||
expect(presetUpdatedByUser1.title).toBe('Everyone (Update 1)')
|
expect(presetUpdatedByUser1.title).toBe('Everyone (Update 1)')
|
||||||
|
|
||||||
const presetUpdatedByUser2 = await payload.update({
|
const presetUpdatedByEditorUser = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForEveryone.id,
|
id: presetForEveryone.id,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Everyone (Update 2)',
|
title: 'Everyone (Update 2)',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(presetUpdatedByUser2.title).toBe('Everyone (Update 2)')
|
expect(presetUpdatedByEditorUser.title).toBe('Everyone (Update 2)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent accidental lockout', async () => {
|
||||||
|
// attempt to create a preset without access to read or update
|
||||||
|
try {
|
||||||
|
const presetWithoutAccess = await payload.create({
|
||||||
|
collection: queryPresetsCollectionSlug,
|
||||||
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
|
data: {
|
||||||
|
title: 'Prevent Lockout',
|
||||||
|
relatedCollection: 'pages',
|
||||||
|
access: {
|
||||||
|
read: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(presetWithoutAccess).toBeFalsy()
|
||||||
|
} catch (error: unknown) {
|
||||||
|
expect((error as Error).message).toBe('Cannot remove yourself from this preset.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const presetWithUser1 = await payload.create({
|
||||||
|
collection: queryPresetsCollectionSlug,
|
||||||
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
|
data: {
|
||||||
|
title: 'Prevent Lockout',
|
||||||
|
relatedCollection: 'pages',
|
||||||
|
access: {
|
||||||
|
read: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [adminUser.id],
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [adminUser.id],
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [adminUser.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// attempt to update the preset to lock the user out of access
|
||||||
|
try {
|
||||||
|
const presetUpdatedByUser1 = await payload.update({
|
||||||
|
collection: queryPresetsCollectionSlug,
|
||||||
|
id: presetWithUser1.id,
|
||||||
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
|
data: {
|
||||||
|
title: 'Prevent Lockout (Updated)',
|
||||||
|
access: {
|
||||||
|
read: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
constraint: 'specificUsers',
|
||||||
|
users: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(presetUpdatedByUser1).toBeFalsy()
|
||||||
|
} catch (error: unknown) {
|
||||||
|
expect((error as Error).message).toBe('Cannot remove yourself from this preset.')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -383,7 +471,8 @@ describe('Query Presets', () => {
|
|||||||
it('should respect top-level access control overrides', async () => {
|
it('should respect top-level access control overrides', async () => {
|
||||||
const preset = await payload.create({
|
const preset = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user,
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Top-Level Access Control Override',
|
title: 'Top-Level Access Control Override',
|
||||||
relatedCollection: 'pages',
|
relatedCollection: 'pages',
|
||||||
@@ -404,7 +493,7 @@ describe('Query Presets', () => {
|
|||||||
const foundPresetWithUser1 = await payload.findByID({
|
const foundPresetWithUser1 = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: preset.id,
|
id: preset.id,
|
||||||
})
|
})
|
||||||
@@ -412,15 +501,15 @@ describe('Query Presets', () => {
|
|||||||
expect(foundPresetWithUser1.id).toBe(preset.id)
|
expect(foundPresetWithUser1.id).toBe(preset.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const foundPresetWithAnonymousUser = await payload.findByID({
|
const foundPresetWithPublicUser = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user: anonymousUser,
|
user: publicUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: preset.id,
|
id: preset.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(foundPresetWithAnonymousUser).toBeFalsy()
|
expect(foundPresetWithPublicUser).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||||
}
|
}
|
||||||
@@ -429,7 +518,8 @@ describe('Query Presets', () => {
|
|||||||
it('should respect access when set to "specificRoles"', async () => {
|
it('should respect access when set to "specificRoles"', async () => {
|
||||||
const presetForSpecificRoles = await payload.create({
|
const presetForSpecificRoles = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user,
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Specific Roles',
|
title: 'Specific Roles',
|
||||||
where: {
|
where: {
|
||||||
@@ -454,7 +544,7 @@ describe('Query Presets', () => {
|
|||||||
const foundPresetWithUser1 = await payload.findByID({
|
const foundPresetWithUser1 = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForSpecificRoles.id,
|
id: presetForSpecificRoles.id,
|
||||||
})
|
})
|
||||||
@@ -462,15 +552,15 @@ describe('Query Presets', () => {
|
|||||||
expect(foundPresetWithUser1.id).toBe(presetForSpecificRoles.id)
|
expect(foundPresetWithUser1.id).toBe(presetForSpecificRoles.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const foundPresetWithUser2 = await payload.findByID({
|
const foundPresetWithEditorUser = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForSpecificRoles.id,
|
id: presetForSpecificRoles.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(foundPresetWithUser2).toBeFalsy()
|
expect(foundPresetWithEditorUser).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect((error as Error).message).toBe('Not Found')
|
expect((error as Error).message).toBe('Not Found')
|
||||||
}
|
}
|
||||||
@@ -478,7 +568,7 @@ describe('Query Presets', () => {
|
|||||||
const presetUpdatedByUser1 = await payload.update({
|
const presetUpdatedByUser1 = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForSpecificRoles.id,
|
id: presetForSpecificRoles.id,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Specific Roles (Updated)',
|
title: 'Specific Roles (Updated)',
|
||||||
@@ -488,17 +578,17 @@ describe('Query Presets', () => {
|
|||||||
expect(presetUpdatedByUser1.title).toBe('Specific Roles (Updated)')
|
expect(presetUpdatedByUser1.title).toBe('Specific Roles (Updated)')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const presetUpdatedByUser2 = await payload.update({
|
const presetUpdatedByEditorUser = await payload.update({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
id: presetForSpecificRoles.id,
|
id: presetForSpecificRoles.id,
|
||||||
user: user2,
|
user: editorUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Specific Roles (Updated)',
|
title: 'Specific Roles (Updated)',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(presetUpdatedByUser2).toBeFalsy()
|
expect(presetUpdatedByEditorUser).toBeFalsy()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||||
}
|
}
|
||||||
@@ -508,7 +598,7 @@ describe('Query Presets', () => {
|
|||||||
// create a preset with the read constraint set to "noone"
|
// create a preset with the read constraint set to "noone"
|
||||||
const presetForNoone = await payload.create({
|
const presetForNoone = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user,
|
user: adminUser,
|
||||||
data: {
|
data: {
|
||||||
relatedCollection: 'pages',
|
relatedCollection: 'pages',
|
||||||
title: 'Noone',
|
title: 'Noone',
|
||||||
@@ -529,7 +619,7 @@ describe('Query Presets', () => {
|
|||||||
const foundPresetWithUser1 = await payload.findByID({
|
const foundPresetWithUser1 = await payload.findByID({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
user,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
id: presetForNoone.id,
|
id: presetForNoone.id,
|
||||||
})
|
})
|
||||||
@@ -545,7 +635,8 @@ describe('Query Presets', () => {
|
|||||||
try {
|
try {
|
||||||
const result = await payload.create({
|
const result = await payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
user,
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Disabled Query Presets',
|
title: 'Disabled Query Presets',
|
||||||
relatedCollection: 'pages',
|
relatedCollection: 'pages',
|
||||||
@@ -563,7 +654,8 @@ describe('Query Presets', () => {
|
|||||||
it('transforms "where" query objects into the "and" / "or" format', async () => {
|
it('transforms "where" query objects into the "and" / "or" format', async () => {
|
||||||
const result = await payload.create({
|
const result = await payload.create({
|
||||||
collection: queryPresetsCollectionSlug,
|
collection: queryPresetsCollectionSlug,
|
||||||
user,
|
user: adminUser,
|
||||||
|
overrideAccess: false,
|
||||||
data: {
|
data: {
|
||||||
title: 'Where Object Formatting',
|
title: 'Where Object Formatting',
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ export interface Config {
|
|||||||
blocks: {};
|
blocks: {};
|
||||||
collections: {
|
collections: {
|
||||||
pages: Page;
|
pages: Page;
|
||||||
users: User;
|
|
||||||
posts: Post;
|
posts: Post;
|
||||||
|
users: User;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
@@ -78,8 +78,8 @@ export interface Config {
|
|||||||
collectionsJoins: {};
|
collectionsJoins: {};
|
||||||
collectionsSelect: {
|
collectionsSelect: {
|
||||||
pages: PagesSelect<false> | PagesSelect<true>;
|
pages: PagesSelect<false> | PagesSelect<true>;
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
|
||||||
posts: PostsSelect<false> | PostsSelect<true>;
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
@@ -126,7 +126,16 @@ export interface Page {
|
|||||||
text?: string | null;
|
text?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts".
|
||||||
|
*/
|
||||||
|
export interface Post {
|
||||||
|
id: string;
|
||||||
|
text?: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -135,7 +144,7 @@ export interface Page {
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
roles?: ('admin' | 'editor' | 'user')[] | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -147,17 +156,6 @@ export interface User {
|
|||||||
lockUntil?: string | null;
|
lockUntil?: string | null;
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "posts".
|
|
||||||
*/
|
|
||||||
export interface Post {
|
|
||||||
id: string;
|
|
||||||
text?: string | null;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
_status?: ('draft' | 'published') | null;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
@@ -169,13 +167,13 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'pages';
|
relationTo: 'pages';
|
||||||
value: string | Page;
|
value: string | Page;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
|
||||||
relationTo: 'users';
|
|
||||||
value: string | User;
|
|
||||||
} | null)
|
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: string | Post;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
@@ -231,12 +229,12 @@ export interface PayloadQueryPreset {
|
|||||||
read?: {
|
read?: {
|
||||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles' | 'noone') | null;
|
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles' | 'noone') | null;
|
||||||
users?: (string | User)[] | null;
|
users?: (string | User)[] | null;
|
||||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
roles?: ('admin' | 'editor' | 'user')[] | null;
|
||||||
};
|
};
|
||||||
update?: {
|
update?: {
|
||||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
|
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
|
||||||
users?: (string | User)[] | null;
|
users?: (string | User)[] | null;
|
||||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
roles?: ('admin' | 'editor' | 'user')[] | null;
|
||||||
};
|
};
|
||||||
delete?: {
|
delete?: {
|
||||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers') | null;
|
constraint?: ('everyone' | 'onlyMe' | 'specificUsers') | null;
|
||||||
@@ -262,6 +260,10 @@ export interface PayloadQueryPreset {
|
|||||||
| boolean
|
| boolean
|
||||||
| null;
|
| null;
|
||||||
relatedCollection: 'pages' | 'posts';
|
relatedCollection: 'pages' | 'posts';
|
||||||
|
/**
|
||||||
|
* This is a tempoary field used to determine if updating the preset would remove the user's access to it. When `true`, this record will be deleted after running the preset's `validate` function.
|
||||||
|
*/
|
||||||
|
isTemp?: boolean | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -273,7 +275,15 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
text?: T;
|
text?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
_status?: T;
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts_select".
|
||||||
|
*/
|
||||||
|
export interface PostsSelect<T extends boolean = true> {
|
||||||
|
text?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -292,16 +302,6 @@ export interface UsersSelect<T extends boolean = true> {
|
|||||||
loginAttempts?: T;
|
loginAttempts?: T;
|
||||||
lockUntil?: T;
|
lockUntil?: T;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "posts_select".
|
|
||||||
*/
|
|
||||||
export interface PostsSelect<T extends boolean = true> {
|
|
||||||
text?: T;
|
|
||||||
updatedAt?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
_status?: T;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents_select".
|
* via the `definition` "payload-locked-documents_select".
|
||||||
@@ -368,6 +368,7 @@ export interface PayloadQueryPresetsSelect<T extends boolean = true> {
|
|||||||
where?: T;
|
where?: T;
|
||||||
columns?: T;
|
columns?: T;
|
||||||
relatedCollection?: T;
|
relatedCollection?: T;
|
||||||
|
isTemp?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ type SeededQueryPreset = {
|
|||||||
} & Omit<QueryPreset, 'id' | 'relatedCollection'>
|
} & Omit<QueryPreset, 'id' | 'relatedCollection'>
|
||||||
|
|
||||||
export const seedData: {
|
export const seedData: {
|
||||||
everyone: SeededQueryPreset
|
everyone: () => SeededQueryPreset
|
||||||
onlyMe: SeededQueryPreset
|
onlyMe: () => SeededQueryPreset
|
||||||
specificUsers: (args: { userID: string }) => SeededQueryPreset
|
specificUsers: (args: { adminUserID: string }) => SeededQueryPreset
|
||||||
} = {
|
} = {
|
||||||
onlyMe: {
|
onlyMe: () => ({
|
||||||
relatedCollection: pagesSlug,
|
relatedCollection: pagesSlug,
|
||||||
isShared: false,
|
isShared: false,
|
||||||
title: 'Only Me',
|
title: 'Only Me',
|
||||||
@@ -40,8 +40,8 @@ export const seedData: {
|
|||||||
equals: 'example page',
|
equals: 'example page',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
everyone: {
|
everyone: () => ({
|
||||||
relatedCollection: pagesSlug,
|
relatedCollection: pagesSlug,
|
||||||
isShared: true,
|
isShared: true,
|
||||||
title: 'Everyone',
|
title: 'Everyone',
|
||||||
@@ -67,8 +67,8 @@ export const seedData: {
|
|||||||
equals: 'example page',
|
equals: 'example page',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
specificUsers: ({ userID }: { userID: string }) => ({
|
specificUsers: ({ adminUserID }: { adminUserID: string }) => ({
|
||||||
title: 'Specific Users',
|
title: 'Specific Users',
|
||||||
isShared: true,
|
isShared: true,
|
||||||
where: {
|
where: {
|
||||||
@@ -79,15 +79,15 @@ export const seedData: {
|
|||||||
access: {
|
access: {
|
||||||
read: {
|
read: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificUsers',
|
||||||
users: [userID],
|
users: [adminUserID],
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificUsers',
|
||||||
users: [userID],
|
users: [adminUserID],
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
constraint: 'specificUsers',
|
constraint: 'specificUsers',
|
||||||
users: [userID],
|
users: [adminUserID],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
@@ -101,7 +101,7 @@ export const seedData: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const seed = async (_payload: Payload) => {
|
export const seed = async (_payload: Payload) => {
|
||||||
const [devUser] = await executePromises(
|
const [adminUser] = await executePromises(
|
||||||
[
|
[
|
||||||
() =>
|
() =>
|
||||||
_payload.create({
|
_payload.create({
|
||||||
@@ -119,18 +119,18 @@ export const seed = async (_payload: Payload) => {
|
|||||||
data: {
|
data: {
|
||||||
email: regularCredentials.email,
|
email: regularCredentials.email,
|
||||||
password: regularCredentials.password,
|
password: regularCredentials.password,
|
||||||
name: 'User',
|
name: 'Editor',
|
||||||
roles: ['user'],
|
roles: ['editor'],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: usersSlug,
|
collection: usersSlug,
|
||||||
data: {
|
data: {
|
||||||
email: 'anonymous@email.com',
|
email: 'public@email.com',
|
||||||
password: regularCredentials.password,
|
password: regularCredentials.password,
|
||||||
name: 'User',
|
name: 'Public User',
|
||||||
roles: ['anonymous'],
|
roles: ['user'],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -149,29 +149,30 @@ export const seed = async (_payload: Payload) => {
|
|||||||
() =>
|
() =>
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
user: devUser,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: seedData.specificUsers({ userID: devUser?.id || '' }),
|
data: seedData.specificUsers({
|
||||||
|
adminUserID: adminUser?.id || '',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
user: devUser,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: seedData.everyone,
|
data: seedData.everyone(),
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
user: devUser,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
data: seedData.onlyMe,
|
data: seedData.onlyMe(),
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
_payload.create({
|
_payload.create({
|
||||||
collection: 'payload-query-presets',
|
collection: 'payload-query-presets',
|
||||||
user: devUser,
|
user: adminUser,
|
||||||
overrideAccess: false,
|
|
||||||
data: {
|
data: {
|
||||||
relatedCollection: 'pages',
|
relatedCollection: 'pages',
|
||||||
title: 'Noone',
|
title: 'Noone',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": ["./test/_community/config.ts"],
|
"@payload-config": ["./test/query-presets/config.ts"],
|
||||||
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
|
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
|
||||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user