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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user