Files
payload/packages/payload/src/database/queryValidation/validateSearchParams.ts
Sasha b9ffbc6994 fix: querying by polymorphic join field relationTo with overrideAccess: false (#11999)
Previously, querying by polymorphic joins `relationTo` with
`overrideAccess: false` caused an error:
```
QueryError: The following paths cannot be queried: relationTo
```

As this field actually doesn't exist in the schema. Now, under condition
that the query comes from a polymorphic join we skip checking
`relationTo` field access.
2025-04-07 20:19:43 +00:00

213 lines
6.1 KiB
TypeScript

// @ts-strict-ignore
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
import type { FlattenedField } from '../../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
import type { PayloadRequest } from '../../types/index.js'
import type { EntityPolicies, PathToQuery } from './types.js'
import { fieldAffectsData, fieldIsVirtual } from '../../fields/config/types.js'
import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
import isolateObjectProperty from '../../utilities/isolateObjectProperty.js'
import { getLocalizedPaths } from '../getLocalizedPaths.js'
import { validateQueryPaths } from './validateQueryPaths.js'
type Args = {
collectionConfig?: SanitizedCollectionConfig
errors: { path: string }[]
fields: FlattenedField[]
globalConfig?: SanitizedGlobalConfig
operator: string
overrideAccess: boolean
parentIsLocalized?: boolean
path: string
policies: EntityPolicies
polymorphicJoin?: boolean
req: PayloadRequest
val: unknown
versionFields?: FlattenedField[]
}
/**
* Validate the Payload key / value / operator
*/
export async function validateSearchParam({
collectionConfig,
errors,
fields,
globalConfig,
operator,
overrideAccess,
parentIsLocalized,
path: incomingPath,
policies,
polymorphicJoin,
req,
val,
versionFields,
}: Args): Promise<void> {
// Replace GraphQL nested field double underscore formatting
let sanitizedPath
if (incomingPath === '_id') {
sanitizedPath = 'id'
} else {
sanitizedPath = incomingPath.replace(/__/g, '.')
}
let paths: PathToQuery[] = []
const { slug } = collectionConfig || globalConfig
const blockPolicies = {}
if (globalConfig && !policies.globals[slug]) {
policies.globals[slug] = await getEntityPolicies({
type: 'global',
blockPolicies,
entity: globalConfig,
operations: ['read'],
req,
})
}
if (sanitizedPath !== 'id') {
paths = getLocalizedPaths({
collectionSlug: collectionConfig?.slug,
fields,
globalSlug: globalConfig?.slug,
incomingPath: sanitizedPath,
locale: req.locale,
overrideAccess,
parentIsLocalized,
payload: req.payload,
})
}
const promises = []
// Sanitize relation.otherRelation.id to relation.otherRelation
if (paths.at(-1)?.path === 'id') {
const previousField = paths.at(-2)?.field
if (
previousField &&
(previousField.type === 'relationship' || previousField.type === 'upload') &&
typeof previousField.relationTo === 'string'
) {
paths.pop()
}
}
promises.push(
...paths.map(async ({ collectionSlug, field, invalid, path }, i) => {
if (invalid) {
errors.push({ path })
return
}
if (fieldIsVirtual(field)) {
errors.push({ path })
}
if (polymorphicJoin && path === 'relationTo') {
return
}
if (!overrideAccess && fieldAffectsData(field)) {
if (collectionSlug) {
if (!policies.collections[collectionSlug]) {
policies.collections[collectionSlug] = await getEntityPolicies({
type: 'collection',
blockPolicies,
entity: req.payload.collections[collectionSlug].config,
operations: ['read'],
req: isolateObjectProperty(req, 'transactionID'),
})
}
if (
['hash', 'salt'].includes(incomingPath) &&
collectionConfig.auth &&
!collectionConfig.auth?.disableLocalStrategy
) {
errors.push({ path: incomingPath })
}
}
let fieldPath = path
// remove locale from end of path
if (path.endsWith(`.${req.locale}`)) {
fieldPath = path.slice(0, -(req.locale.length + 1))
}
// remove ".value" from ends of polymorphic relationship paths
if (
(field.type === 'relationship' || field.type === 'upload') &&
Array.isArray(field.relationTo)
) {
fieldPath = fieldPath.replace('.value', '')
}
const entityType: 'collections' | 'globals' = globalConfig ? 'globals' : 'collections'
const entitySlug = collectionSlug || globalConfig.slug
const segments = fieldPath.split('.')
let fieldAccess
if (versionFields) {
fieldAccess = policies[entityType][entitySlug]
if (segments[0] === 'parent' || segments[0] === 'version') {
segments.shift()
}
} else {
fieldAccess = policies[entityType][entitySlug].fields
}
segments.forEach((segment) => {
if (fieldAccess[segment]) {
if ('fields' in fieldAccess[segment]) {
fieldAccess = fieldAccess[segment].fields
} else if (
'blocks' in fieldAccess[segment] ||
'blockReferences' in fieldAccess[segment]
) {
fieldAccess = fieldAccess[segment]
} else {
fieldAccess = fieldAccess[segment]
}
}
})
if (!fieldAccess?.read?.permission) {
errors.push({ path: fieldPath })
}
}
if (i > 1) {
// Remove top collection and reverse array
// to work backwards from top
const pathsToQuery = paths.slice(1).reverse()
pathsToQuery.forEach(
({ collectionSlug: pathCollectionSlug, path: subPath }, pathToQueryIndex) => {
// On the "deepest" collection,
// validate query of the relationship
if (pathToQueryIndex === 0) {
promises.push(
validateQueryPaths({
collectionConfig: req.payload.collections[pathCollectionSlug].config,
errors,
globalConfig: undefined,
overrideAccess,
policies,
req,
where: {
[subPath]: {
[operator]: val,
},
},
}),
)
}
},
)
}
}),
)
await Promise.all(promises)
}