perf: do not populate globals when calculating permissions, cleanup getEntityPolicies (#11237)

Previously, we forgot to add `depth: 0` to our `findGlobal` call in `getEntityPolicies`. This PR adds `depth: 0` which will be faster.

It also cleans up the `getEntityPolicies` function in general by adding missing types, JSDocs and improving code readability.

This was part of https://github.com/payloadcms/payload/pull/11236 and has been extracted into this separate PR, to make it easier to review
This commit is contained in:
Alessio Gravili
2025-02-17 15:31:27 -07:00
committed by GitHub
parent daaaa5f1be
commit d49de7bdf8
2 changed files with 199 additions and 184 deletions

View File

@@ -1,23 +1,22 @@
// @ts-strict-ignore
import type { Where } from '../types/index.js'
import { hasWhereAccessResult } from '../auth/index.js'
/**
* Combines two queries into a single query, using an AND operator
*/
export const combineQueries = (where: Where, access: boolean | Where): Where => {
if (!where && !access) {
return {}
}
const result: Where = {
and: [],
}
const and: Where[] = where ? [where] : []
if (where) {
result.and.push(where)
}
if (hasWhereAccessResult(access)) {
result.and.push(access)
and.push(access)
}
return result
return {
and,
}
}

View File

@@ -1,10 +1,10 @@
// @ts-strict-ignore
import type { CollectionPermission, GlobalPermission } from '../auth/types.js'
import type { CollectionPermission, FieldsPermissions, GlobalPermission } from '../auth/types.js'
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { Access } from '../config/types.js'
import type { Field, FieldAccess } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { AllOperations, Document, PayloadRequest, Where } from '../types/index.js'
import type { AllOperations, JsonObject, Payload, PayloadRequest, Where } from '../types/index.js'
import { combineQueries } from '../database/combineQueries.js'
import { tabHasName } from '../fields/config/types.js'
@@ -26,11 +26,14 @@ type CreateAccessPromise = (args: {
accessLevel: 'entity' | 'field'
disableWhere?: boolean
operation: AllOperations
policiesObj: {
[key: string]: any
}
policiesObj: CollectionPermission | GlobalPermission
}) => Promise<void>
type EntityDoc = JsonObject | TypeWithID
/**
* Build up permissions object for an entity (collection or global)
*/
export async function getEntityPolicies<T extends Args>(args: T): Promise<ReturnType<T>> {
const { id, type, entity, operations, req } = args
const { data, locale, payload, user } = req
@@ -40,50 +43,51 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
fields: {},
} as ReturnType<T>
let docBeingAccessed
let docBeingAccessed: EntityDoc | Promise<EntityDoc | undefined> | undefined
async function getEntityDoc({ where }: { where?: Where } = {}): Promise<Document & TypeWithID> {
if (entity.slug) {
if (type === 'global') {
return payload.findGlobal({
slug: entity.slug,
fallbackLocale: null,
locale,
overrideAccess: true,
req,
})
}
async function getEntityDoc({ where }: { where?: Where } = {}): Promise<EntityDoc | undefined> {
if (!entity.slug) {
return undefined
}
if (type === 'collection' && id) {
if (typeof where === 'object') {
const paginatedRes = await payload.find({
collection: entity.slug,
depth: 0,
fallbackLocale: null,
limit: 1,
locale,
overrideAccess: true,
pagination: false,
req,
where: combineQueries(where, { id: { equals: id } }),
})
if (type === 'global') {
return payload.findGlobal({
slug: entity.slug,
depth: 0,
fallbackLocale: null,
locale,
overrideAccess: true,
req,
})
}
return paginatedRes?.docs?.[0] || undefined
}
return payload.findByID({
id,
if (type === 'collection' && id) {
if (typeof where === 'object') {
const paginatedRes = await payload.find({
collection: entity.slug,
depth: 0,
fallbackLocale: null,
limit: 1,
locale,
overrideAccess: true,
pagination: false,
req,
where: combineQueries(where, { id: { equals: id } }),
})
}
}
return undefined
return paginatedRes?.docs?.[0] || undefined
}
return payload.findByID({
id,
collection: entity.slug,
depth: 0,
fallbackLocale: null,
locale,
overrideAccess: true,
req,
})
}
}
const createAccessPromise: CreateAccessPromise = async ({
@@ -91,10 +95,8 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
accessLevel,
disableWhere = false,
operation,
policiesObj,
policiesObj: mutablePolicies,
}) => {
const mutablePolicies = policiesObj
if (accessLevel === 'field' && docBeingAccessed === undefined) {
// assign docBeingAccessed first as the promise to avoid multiple calls to getEntityDoc
docBeingAccessed = getEntityDoc().then((doc) => {
@@ -107,6 +109,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
// https://payloadcms.slack.com/archives/C048Z9C2BEX/p1702054928343769
const accessResult = await access({ id, data, doc: docBeingAccessed, req })
// Where query was returned from access function => check if document is returned when querying with where
if (typeof accessResult === 'object' && !disableWhere) {
mutablePolicies[operation] = {
permission:
@@ -120,137 +123,9 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
}
}
const executeFieldPolicies = async ({
entityPermission,
fields,
operation,
policiesObj,
}: {
entityPermission
fields: Field[]
operation: AllOperations
policiesObj
}) => {
const mutablePolicies = policiesObj.fields
// Fields don't have all operations of a collection
if (operation === 'delete' || operation === 'readVersions' || operation === 'unlock') {
return
}
await Promise.all(
fields.map(async (field) => {
if ('name' in field && field.name) {
if (!mutablePolicies[field.name]) {
mutablePolicies[field.name] = {}
}
if ('access' in field && field.access && typeof field.access[operation] === 'function') {
await createAccessPromise({
access: field.access[operation],
accessLevel: 'field',
disableWhere: true,
operation,
policiesObj: mutablePolicies[field.name],
})
} else {
mutablePolicies[field.name][operation] = {
permission: policiesObj[operation]?.permission,
}
}
if ('fields' in field && field.fields) {
if (!mutablePolicies[field.name].fields) {
mutablePolicies[field.name].fields = {}
}
await executeFieldPolicies({
entityPermission,
fields: field.fields,
operation,
policiesObj: mutablePolicies[field.name],
})
}
if (
('blocks' in field && field.blocks) ||
('blockReferences' in field && field.blockReferences)
) {
if (!mutablePolicies[field.name]?.blocks) {
mutablePolicies[field.name].blocks = {}
}
await Promise.all(
(field.blockReferences ?? field.blocks).map(async (_block) => {
const block = typeof _block === 'string' ? payload.blocks[_block] : _block // TODO: Skip over string blocks
if (!mutablePolicies[field.name].blocks?.[block.slug]) {
mutablePolicies[field.name].blocks[block.slug] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[field.name].blocks[block.slug][operation]) {
mutablePolicies[field.name].blocks[block.slug][operation] = {
permission: entityPermission,
}
}
await executeFieldPolicies({
entityPermission,
fields: block.fields,
operation,
policiesObj: mutablePolicies[field.name].blocks[block.slug],
})
}),
)
}
} else if ('fields' in field && field.fields) {
await executeFieldPolicies({
entityPermission,
fields: field.fields,
operation,
policiesObj,
})
} else if (field.type === 'tabs') {
await Promise.all(
field.tabs.map(async (tab) => {
if (tabHasName(tab)) {
if (!mutablePolicies[tab.name]) {
mutablePolicies[tab.name] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[tab.name][operation]) {
mutablePolicies[tab.name][operation] = { permission: entityPermission }
}
await executeFieldPolicies({
entityPermission,
fields: tab.fields,
operation,
policiesObj: mutablePolicies[tab.name],
})
} else {
await executeFieldPolicies({
entityPermission,
fields: tab.fields,
operation,
policiesObj,
})
}
}),
)
}
}),
)
}
await operations.reduce(async (priorOperation, operation) => {
await priorOperation
let entityAccessPromise: Promise<void>
for (const operation of operations) {
if (typeof entity.access[operation] === 'function') {
entityAccessPromise = createAccessPromise({
await createAccessPromise({
access: entity.access[operation],
accessLevel: 'entity',
operation,
@@ -262,15 +137,156 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
}
}
await entityAccessPromise
await executeFieldPolicies({
entityPermission: policies[operation].permission,
createAccessPromise,
entityPermission: policies[operation].permission as boolean,
fields: entity.fields,
operation,
payload,
policiesObj: policies,
})
}, Promise.resolve())
}
return policies
}
/**
* Build up permissions object and run access functions for each field of an entity
*/
const executeFieldPolicies = async ({
createAccessPromise,
entityPermission,
fields,
operation,
payload,
policiesObj,
}: {
createAccessPromise: CreateAccessPromise
entityPermission: boolean
fields: Field[]
operation: AllOperations
payload: Payload
policiesObj: CollectionPermission | FieldsPermissions | GlobalPermission
}) => {
const mutablePolicies = policiesObj.fields
// Fields don't have all operations of a collection
if (operation === 'delete' || operation === 'readVersions' || operation === 'unlock') {
return
}
await Promise.all(
fields.map(async (field) => {
if ('name' in field && field.name) {
if (!mutablePolicies[field.name]) {
mutablePolicies[field.name] = {}
}
if ('access' in field && field.access && typeof field.access[operation] === 'function') {
await createAccessPromise({
access: field.access[operation],
accessLevel: 'field',
disableWhere: true,
operation,
policiesObj: mutablePolicies[field.name],
})
} else {
mutablePolicies[field.name][operation] = {
permission: policiesObj[operation]?.permission,
}
}
if ('fields' in field && field.fields) {
if (!mutablePolicies[field.name].fields) {
mutablePolicies[field.name].fields = {}
}
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: field.fields,
operation,
payload,
policiesObj: mutablePolicies[field.name],
})
}
if (
('blocks' in field && field.blocks?.length) ||
('blockReferences' in field && field.blockReferences?.length)
) {
if (!mutablePolicies[field.name]?.blocks) {
mutablePolicies[field.name].blocks = {}
}
await Promise.all(
(field.blockReferences ?? field.blocks).map(async (_block) => {
const block = typeof _block === 'string' ? payload.blocks[_block] : _block // TODO: Skip over string blocks
if (!mutablePolicies[field.name].blocks?.[block.slug]) {
mutablePolicies[field.name].blocks[block.slug] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[field.name].blocks[block.slug][operation]) {
mutablePolicies[field.name].blocks[block.slug][operation] = {
permission: entityPermission,
}
}
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: block.fields,
operation,
payload,
policiesObj: mutablePolicies[field.name].blocks[block.slug],
})
}),
)
}
} else if ('fields' in field && field.fields) {
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: field.fields,
operation,
payload,
policiesObj,
})
} else if (field.type === 'tabs') {
await Promise.all(
field.tabs.map(async (tab) => {
if (tabHasName(tab)) {
if (!mutablePolicies[tab.name]) {
mutablePolicies[tab.name] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[tab.name][operation]) {
mutablePolicies[tab.name][operation] = { permission: entityPermission }
}
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: tab.fields,
operation,
payload,
policiesObj: mutablePolicies[tab.name],
})
} else {
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: tab.fields,
operation,
payload,
policiesObj,
})
}
}),
)
}
}),
)
}