fix(db-mongodb): support 2x and more relationship sorting (#13819)

Previously, sorting like:
`sort: 'relationship.anotherRelationship.title'` (and more nested)
didn't work with the MongoDB adapter, this PR fixes this.
This commit is contained in:
Sasha
2025-09-16 21:03:48 +03:00
committed by GitHub
parent 5c02d17726
commit 24ace70b58
4 changed files with 103 additions and 80 deletions

View File

@@ -37,24 +37,21 @@ const relationshipSort = ({
fields,
locale,
path,
sort,
previousField = '',
sortAggregation,
sortDirection,
versions,
}: {
adapter: MongooseAdapter
fields: FlattenedField[]
locale?: string
path: string
sort: Record<string, string>
previousField?: string
sortAggregation: PipelineStage[]
sortDirection: SortDirection
versions?: boolean
}) => {
}): null | string => {
let currentFields = fields
const segments = path.split('.')
if (segments.length < 2) {
return false
return null
}
for (let i = 0; i < segments.length; i++) {
@@ -62,98 +59,87 @@ const relationshipSort = ({
const field = currentFields.find((each) => each.name === segment)
if (!field) {
return false
return null
}
if ('fields' in field) {
currentFields = field.flattenedFields
if (field.name === 'version' && versions && i === 0) {
segments.shift()
i--
}
} else if (
(field.type === 'relationship' || field.type === 'upload') &&
i !== segments.length - 1
) {
const relationshipPath = segments.slice(0, i + 1).join('.')
let sortFieldPath = segments.slice(i + 1, segments.length).join('.')
if (sortFieldPath.endsWith('.id')) {
sortFieldPath = sortFieldPath.split('.').slice(0, -1).join('.')
}
if (Array.isArray(field.relationTo)) {
throw new APIError('Not supported')
const nextPath = segments.slice(i + 1, segments.length)
const relationshipFieldResult = getFieldByPath({ fields, path: relationshipPath })
if (
!relationshipFieldResult ||
!('relationTo' in relationshipFieldResult.field) ||
typeof relationshipFieldResult.field.relationTo !== 'string'
) {
return null
}
const foreignCollection = getCollection({ adapter, collectionSlug: field.relationTo })
const foreignFieldPath = getFieldByPath({
fields: foreignCollection.collectionConfig.flattenedFields,
path: sortFieldPath,
const { collectionConfig, Model } = getCollection({
adapter,
collectionSlug: relationshipFieldResult.field.relationTo,
})
if (!foreignFieldPath) {
return false
let localizedRelationshipPath: string = relationshipFieldResult.localizedPath
if (locale && relationshipFieldResult.pathHasLocalized) {
localizedRelationshipPath = relationshipFieldResult.localizedPath.replace(
'<locale>',
locale,
)
}
if (foreignFieldPath.pathHasLocalized && locale) {
sortFieldPath = foreignFieldPath.localizedPath.replace('<locale>', locale)
if (nextPath.join('.') === 'id') {
return `${previousField}${localizedRelationshipPath}`
}
const as = `__${relationshipPath.replace(/\./g, '__')}`
const as = `__${previousField}${localizedRelationshipPath}`
// If we have not already sorted on this relationship yet, we need to add a lookup stage
if (!sortAggregation.some((each) => '$lookup' in each && each.$lookup.as === as)) {
let localField = versions ? `version.${relationshipPath}` : relationshipPath
sortAggregation.push({
$lookup: {
as: `__${previousField}${localizedRelationshipPath}`,
foreignField: '_id',
from: Model.collection.name,
localField: `${previousField}${localizedRelationshipPath}`,
},
})
if (adapter.usePipelineInSortLookup) {
const flattenedField = `__${localField.replace(/\./g, '__')}_lookup`
sortAggregation.push({
$addFields: {
[flattenedField]: `$${localField}`,
},
})
localField = flattenedField
}
sortAggregation.push({
$lookup: {
as,
foreignField: '_id',
from: foreignCollection.Model.collection.name,
localField,
...(!adapter.usePipelineInSortLookup && {
pipeline: [
{
$project: {
[sortFieldPath]: true,
},
},
],
}),
},
if (nextPath.length > 1) {
const nextRes = relationshipSort({
adapter,
fields: collectionConfig.flattenedFields,
locale,
path: nextPath.join('.'),
previousField: `${as}.`,
sortAggregation,
})
if (adapter.usePipelineInSortLookup) {
sortAggregation.push({
$unset: localField,
})
if (nextRes) {
return nextRes
}
return `${as}.${nextPath.join('.')}`
}
if (!adapter.usePipelineInSortLookup) {
const lookup = sortAggregation.find(
(each) => '$lookup' in each && each.$lookup.as === as,
) as PipelineStage.Lookup
const pipeline = lookup.$lookup.pipeline![0] as PipelineStage.Project
pipeline.$project[sortFieldPath] = true
const nextField = getFieldByPath({
fields: collectionConfig.flattenedFields,
path: nextPath[0]!,
})
if (nextField && nextField.pathHasLocalized && locale) {
return `${as}.${nextField.localizedPath.replace('<locale>', locale)}`
}
sort[`${as}.${sortFieldPath}`] = sortDirection
return true
return `${as}.${nextPath[0]}`
}
}
return false
return null
}
export const buildSortParam = ({
@@ -217,20 +203,20 @@ export const buildSortParam = ({
return acc
}
if (
sortAggregation &&
relationshipSort({
if (sortAggregation) {
const sortRelProperty = relationshipSort({
adapter,
fields,
locale,
path: sortProperty,
sort: acc,
sortAggregation,
sortDirection,
versions,
})
) {
return acc
if (sortRelProperty) {
acc[sortRelProperty] = sortDirection
return acc
}
}
const localizedProperty = getLocalizedSortProperty({

View File

@@ -2903,7 +2903,7 @@ describe('Localization', () => {
})
const req = await createLocalReq({ user }, payload)
global.d = true
const res = (await copyDataFromLocaleHandler({
fromLocale: 'en',
req,
@@ -2933,7 +2933,6 @@ describe('Localization', () => {
})
const req = await createLocalReq({ user }, payload)
global.d = true
const res = (await copyDataFromLocaleHandler({
fromLocale: 'en',
req,

View File

@@ -791,6 +791,45 @@ describe('Relationships', () => {
expect(localized_res_2.docs).toStrictEqual([movie_1, movie_2])
})
it('should sort by a property of a nested relationship', async () => {
await payload.delete({ collection: 'directors', where: {} })
await payload.delete({ collection: 'movies', where: {} })
const director = await payload.create({ collection: 'directors', data: {} })
const movie = await payload.create({
collection: 'movies',
data: { director: director.id, name: 'movie 1' },
})
await payload.update({
collection: 'directors',
id: director.id,
data: { movie: movie.id },
})
const director_2 = await payload.create({ collection: 'directors', data: {} })
const movie_2 = await payload.create({
collection: 'movies',
data: { director: director_2.id, name: 'movie 2' },
})
await payload.update({
collection: 'directors',
id: director_2.id,
data: { movie: movie_2.id },
})
const res = await payload.find({ collection: 'movies', sort: 'director.movie.name' })
expect(res.docs[0].id).toBe(movie.id)
expect(res.docs[1].id).toBe(movie_2.id)
const res_2 = await payload.find({ collection: 'movies', sort: '-director.movie.name' })
expect(res_2.docs[0].id).toBe(movie_2.id)
expect(res_2.docs[1].id).toBe(movie.id)
})
it('should sort by multiple properties of a relationship', async () => {
await payload.delete({ collection: 'directors', where: {} })
await payload.delete({ collection: 'movies', where: {} })

View File

@@ -368,7 +368,6 @@ describe('Versions', () => {
it('should allow to create with a localized relationships inside a localized array and a block', async () => {
const post = await payload.create({ collection: 'posts', data: {} })
global.d = true
const res = await payload.create({
collection: 'localized-posts',
draft: true,