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