fix(payload, db-mongodb): querying relationships with in & not_in (#6773)

## Description

Fixes #6741 

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
This commit is contained in:
Patrik
2024-06-14 16:16:59 -04:00
committed by GitHub
parent 68ea693a88
commit f6ba3befae
4 changed files with 135 additions and 23 deletions

View File

@@ -11,6 +11,50 @@ type SanitizeQueryValueArgs = {
val: any
}
const handleHasManyValues = (formattedValue) => {
return formattedValue.reduce((formattedValues, inVal) => {
const newValues = [inVal]
if (mongoose.Types.ObjectId.isValid(inVal)) {
newValues.push(new mongoose.Types.ObjectId(inVal))
}
const parsedNumber = parseFloat(inVal)
if (!Number.isNaN(parsedNumber)) {
newValues.push(parsedNumber)
}
return [...formattedValues, ...newValues]
}, [])
}
const handleNonHasManyValues = (formattedValue, operator, path) => {
const formattedQueries = formattedValue
.map((inVal) => {
if (inVal && typeof inVal === 'object' && 'relationTo' in inVal && 'value' in inVal) {
if (operator === 'in') {
return {
[`${path}.relationTo`]: { $eq: inVal.relationTo },
[`${path}.value`]: { $eq: inVal.value },
}
} else if (operator === 'not_in') {
return {
$and: [
{ [`${path}.value`]: inVal.value },
{ [`${path}.relationTo`]: inVal.relationTo },
],
}
}
}
return null
})
.filter(Boolean)
if (formattedQueries.length > 0) {
return {
rawQuery: operator === 'in' ? { $or: formattedQueries } : { $nor: formattedQueries },
}
}
}
export const sanitizeQueryValue = ({
field,
hasCustomID,
@@ -92,17 +136,15 @@ export const sanitizeQueryValue = ({
}
}
if (operator === 'in' && Array.isArray(formattedValue)) {
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
const newValues = [inVal]
if (mongoose.Types.ObjectId.isValid(inVal))
newValues.push(new mongoose.Types.ObjectId(inVal))
const parsedNumber = parseFloat(inVal)
if (!Number.isNaN(parsedNumber)) newValues.push(parsedNumber)
return [...formattedValues, ...newValues]
}, [])
if (['in', 'not_in'].includes(operator) && Array.isArray(formattedValue)) {
if ('hasMany' in field && field.hasMany) {
formattedValue = handleHasManyValues(formattedValue)
} else {
const result = handleNonHasManyValues(formattedValue, operator, path)
if (result) {
return result
}
}
}
}

View File

@@ -26,6 +26,7 @@ const RelationshipField: React.FC<Props> = (props) => {
filterOptions,
hasMany,
onChange,
operator,
relationTo,
value,
} = props
@@ -47,6 +48,8 @@ const RelationshipField: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation('general')
const { user } = useAuth()
const isMulti = ['in', 'not_in'].includes(operator)
const addOptions = useCallback(
(data, relation) => {
const collection = collections.find((coll) => coll.slug === relation)
@@ -175,7 +178,7 @@ const RelationshipField: React.FC<Props> = (props) => {
const findOptionsByValue = useCallback((): Option | Option[] => {
if (value) {
if (hasMany) {
if (hasMany || isMulti) {
if (Array.isArray(value)) {
return value.map((val) => {
if (hasMultipleRelations) {
@@ -228,7 +231,7 @@ const RelationshipField: React.FC<Props> = (props) => {
}
return undefined
}, [hasMany, hasMultipleRelations, value, options])
}, [hasMany, hasMultipleRelations, isMulti, value, options])
const handleInputChange = useCallback(
(newSearch) => {
@@ -253,6 +256,7 @@ const RelationshipField: React.FC<Props> = (props) => {
const data = await response.json()
addOptions({ docs: [data] }, relation)
} else {
// eslint-disable-next-line no-console
console.error(t('error:loadingDocument', { id }))
}
}
@@ -266,15 +270,15 @@ const RelationshipField: React.FC<Props> = (props) => {
useEffect(() => {
dispatchOptions({
type: 'CLEAR',
i18n,
required: true,
type: 'CLEAR',
})
setHasLoadedFirstOptions(true)
setLastLoadedPage(1)
setLastFullyLoadedRelation(-1)
getResults({ search: debouncedSearch })
void getResults({ search: debouncedSearch })
}, [getResults, debouncedSearch, relationTo, i18n])
// ///////////////////////////
@@ -283,15 +287,15 @@ const RelationshipField: React.FC<Props> = (props) => {
useEffect(() => {
if (value && hasLoadedFirstOptions) {
if (hasMany) {
if (hasMany || isMulti) {
const matchedOptions = findOptionsByValue()
;((matchedOptions as Option[]) || []).forEach((option, i) => {
if (!option) {
if (hasMultipleRelations) {
addOptionByID(value[i].value, value[i].relationTo)
void addOptionByID(value[i].value, value[i].relationTo)
} else {
addOptionByID(value[i], relationTo)
void addOptionByID(value[i], relationTo)
}
}
})
@@ -301,9 +305,9 @@ const RelationshipField: React.FC<Props> = (props) => {
if (!matchedOption) {
if (hasMultipleRelations) {
const valueWithRelation = value as ValueWithRelation
addOptionByID(valueWithRelation.value, valueWithRelation.relationTo)
void addOptionByID(valueWithRelation.value, valueWithRelation.relationTo)
} else {
addOptionByID(value, relationTo)
void addOptionByID(value, relationTo)
}
}
}
@@ -313,6 +317,7 @@ const RelationshipField: React.FC<Props> = (props) => {
findOptionsByValue,
hasMany,
hasMultipleRelations,
isMulti,
relationTo,
value,
hasLoadedFirstOptions,
@@ -329,10 +334,10 @@ const RelationshipField: React.FC<Props> = (props) => {
{!errorLoading && (
<ReactSelect
disabled={disabled}
isMulti={hasMany}
isMulti={hasMany || isMulti}
isSortable={isSortable}
onChange={(selected) => {
if (hasMany) {
if (hasMany || isMulti) {
onChange(
selected
? selected.map((option) => {
@@ -358,7 +363,7 @@ const RelationshipField: React.FC<Props> = (props) => {
}}
onInputChange={handleInputChange}
onMenuScrollToBottom={() => {
getResults({ lastFullyLoadedRelation, lastLoadedPage: lastLoadedPage + 1 })
void getResults({ lastFullyLoadedRelation, lastLoadedPage: lastLoadedPage + 1 })
}}
options={options}
placeholder={t('selectValue')}

View File

@@ -2,11 +2,13 @@ import type i18n from 'i18next'
import type { SanitizedCollectionConfig } from '../../../../../../collections/config/types'
import type { PaginatedDocs } from '../../../../../../database/types'
import type { Operator } from '../../../../../../exports/types'
import type { RelationshipField } from '../../../../../../fields/config/types'
export type Props = {
disabled?: boolean
onChange: (val: unknown) => void
operator: Operator
value: unknown
} & RelationshipField