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:
@@ -37,24 +37,21 @@ const relationshipSort = ({
|
|||||||
fields,
|
fields,
|
||||||
locale,
|
locale,
|
||||||
path,
|
path,
|
||||||
sort,
|
previousField = '',
|
||||||
sortAggregation,
|
sortAggregation,
|
||||||
sortDirection,
|
|
||||||
versions,
|
|
||||||
}: {
|
}: {
|
||||||
adapter: MongooseAdapter
|
adapter: MongooseAdapter
|
||||||
fields: FlattenedField[]
|
fields: FlattenedField[]
|
||||||
locale?: string
|
locale?: string
|
||||||
path: string
|
path: string
|
||||||
sort: Record<string, string>
|
previousField?: string
|
||||||
sortAggregation: PipelineStage[]
|
sortAggregation: PipelineStage[]
|
||||||
sortDirection: SortDirection
|
|
||||||
versions?: boolean
|
versions?: boolean
|
||||||
}) => {
|
}): null | string => {
|
||||||
let currentFields = fields
|
let currentFields = fields
|
||||||
const segments = path.split('.')
|
const segments = path.split('.')
|
||||||
if (segments.length < 2) {
|
if (segments.length < 2) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < segments.length; i++) {
|
for (let i = 0; i < segments.length; i++) {
|
||||||
@@ -62,98 +59,87 @@ const relationshipSort = ({
|
|||||||
const field = currentFields.find((each) => each.name === segment)
|
const field = currentFields.find((each) => each.name === segment)
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('fields' in field) {
|
if ('fields' in field) {
|
||||||
currentFields = field.flattenedFields
|
currentFields = field.flattenedFields
|
||||||
if (field.name === 'version' && versions && i === 0) {
|
|
||||||
segments.shift()
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
} else if (
|
} else if (
|
||||||
(field.type === 'relationship' || field.type === 'upload') &&
|
(field.type === 'relationship' || field.type === 'upload') &&
|
||||||
i !== segments.length - 1
|
i !== segments.length - 1
|
||||||
) {
|
) {
|
||||||
const relationshipPath = segments.slice(0, i + 1).join('.')
|
const relationshipPath = segments.slice(0, i + 1).join('.')
|
||||||
let sortFieldPath = segments.slice(i + 1, segments.length).join('.')
|
const nextPath = segments.slice(i + 1, segments.length)
|
||||||
if (sortFieldPath.endsWith('.id')) {
|
const relationshipFieldResult = getFieldByPath({ fields, path: relationshipPath })
|
||||||
sortFieldPath = sortFieldPath.split('.').slice(0, -1).join('.')
|
|
||||||
}
|
if (
|
||||||
if (Array.isArray(field.relationTo)) {
|
!relationshipFieldResult ||
|
||||||
throw new APIError('Not supported')
|
!('relationTo' in relationshipFieldResult.field) ||
|
||||||
|
typeof relationshipFieldResult.field.relationTo !== 'string'
|
||||||
|
) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const foreignCollection = getCollection({ adapter, collectionSlug: field.relationTo })
|
const { collectionConfig, Model } = getCollection({
|
||||||
|
adapter,
|
||||||
const foreignFieldPath = getFieldByPath({
|
collectionSlug: relationshipFieldResult.field.relationTo,
|
||||||
fields: foreignCollection.collectionConfig.flattenedFields,
|
|
||||||
path: sortFieldPath,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!foreignFieldPath) {
|
let localizedRelationshipPath: string = relationshipFieldResult.localizedPath
|
||||||
return false
|
|
||||||
|
if (locale && relationshipFieldResult.pathHasLocalized) {
|
||||||
|
localizedRelationshipPath = relationshipFieldResult.localizedPath.replace(
|
||||||
|
'<locale>',
|
||||||
|
locale,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foreignFieldPath.pathHasLocalized && locale) {
|
if (nextPath.join('.') === 'id') {
|
||||||
sortFieldPath = foreignFieldPath.localizedPath.replace('<locale>', locale)
|
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
|
sortAggregation.push({
|
||||||
if (!sortAggregation.some((each) => '$lookup' in each && each.$lookup.as === as)) {
|
$lookup: {
|
||||||
let localField = versions ? `version.${relationshipPath}` : relationshipPath
|
as: `__${previousField}${localizedRelationshipPath}`,
|
||||||
|
foreignField: '_id',
|
||||||
|
from: Model.collection.name,
|
||||||
|
localField: `${previousField}${localizedRelationshipPath}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (adapter.usePipelineInSortLookup) {
|
if (nextPath.length > 1) {
|
||||||
const flattenedField = `__${localField.replace(/\./g, '__')}_lookup`
|
const nextRes = relationshipSort({
|
||||||
sortAggregation.push({
|
adapter,
|
||||||
$addFields: {
|
fields: collectionConfig.flattenedFields,
|
||||||
[flattenedField]: `$${localField}`,
|
locale,
|
||||||
},
|
path: nextPath.join('.'),
|
||||||
})
|
previousField: `${as}.`,
|
||||||
localField = flattenedField
|
sortAggregation,
|
||||||
}
|
|
||||||
|
|
||||||
sortAggregation.push({
|
|
||||||
$lookup: {
|
|
||||||
as,
|
|
||||||
foreignField: '_id',
|
|
||||||
from: foreignCollection.Model.collection.name,
|
|
||||||
localField,
|
|
||||||
...(!adapter.usePipelineInSortLookup && {
|
|
||||||
pipeline: [
|
|
||||||
{
|
|
||||||
$project: {
|
|
||||||
[sortFieldPath]: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (adapter.usePipelineInSortLookup) {
|
if (nextRes) {
|
||||||
sortAggregation.push({
|
return nextRes
|
||||||
$unset: localField,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return `${as}.${nextPath.join('.')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!adapter.usePipelineInSortLookup) {
|
const nextField = getFieldByPath({
|
||||||
const lookup = sortAggregation.find(
|
fields: collectionConfig.flattenedFields,
|
||||||
(each) => '$lookup' in each && each.$lookup.as === as,
|
path: nextPath[0]!,
|
||||||
) as PipelineStage.Lookup
|
})
|
||||||
const pipeline = lookup.$lookup.pipeline![0] as PipelineStage.Project
|
|
||||||
pipeline.$project[sortFieldPath] = true
|
if (nextField && nextField.pathHasLocalized && locale) {
|
||||||
|
return `${as}.${nextField.localizedPath.replace('<locale>', locale)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
sort[`${as}.${sortFieldPath}`] = sortDirection
|
return `${as}.${nextPath[0]}`
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildSortParam = ({
|
export const buildSortParam = ({
|
||||||
@@ -217,20 +203,20 @@ export const buildSortParam = ({
|
|||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (sortAggregation) {
|
||||||
sortAggregation &&
|
const sortRelProperty = relationshipSort({
|
||||||
relationshipSort({
|
|
||||||
adapter,
|
adapter,
|
||||||
fields,
|
fields,
|
||||||
locale,
|
locale,
|
||||||
path: sortProperty,
|
path: sortProperty,
|
||||||
sort: acc,
|
|
||||||
sortAggregation,
|
sortAggregation,
|
||||||
sortDirection,
|
|
||||||
versions,
|
versions,
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return acc
|
if (sortRelProperty) {
|
||||||
|
acc[sortRelProperty] = sortDirection
|
||||||
|
return acc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const localizedProperty = getLocalizedSortProperty({
|
const localizedProperty = getLocalizedSortProperty({
|
||||||
|
|||||||
@@ -2903,7 +2903,7 @@ describe('Localization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const req = await createLocalReq({ user }, payload)
|
const req = await createLocalReq({ user }, payload)
|
||||||
global.d = true
|
|
||||||
const res = (await copyDataFromLocaleHandler({
|
const res = (await copyDataFromLocaleHandler({
|
||||||
fromLocale: 'en',
|
fromLocale: 'en',
|
||||||
req,
|
req,
|
||||||
@@ -2933,7 +2933,6 @@ describe('Localization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const req = await createLocalReq({ user }, payload)
|
const req = await createLocalReq({ user }, payload)
|
||||||
global.d = true
|
|
||||||
const res = (await copyDataFromLocaleHandler({
|
const res = (await copyDataFromLocaleHandler({
|
||||||
fromLocale: 'en',
|
fromLocale: 'en',
|
||||||
req,
|
req,
|
||||||
|
|||||||
@@ -791,6 +791,45 @@ describe('Relationships', () => {
|
|||||||
expect(localized_res_2.docs).toStrictEqual([movie_1, movie_2])
|
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 () => {
|
it('should sort by multiple properties of a relationship', async () => {
|
||||||
await payload.delete({ collection: 'directors', where: {} })
|
await payload.delete({ collection: 'directors', where: {} })
|
||||||
await payload.delete({ collection: 'movies', where: {} })
|
await payload.delete({ collection: 'movies', where: {} })
|
||||||
|
|||||||
@@ -368,7 +368,6 @@ describe('Versions', () => {
|
|||||||
|
|
||||||
it('should allow to create with a localized relationships inside a localized array and a block', async () => {
|
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: {} })
|
const post = await payload.create({ collection: 'posts', data: {} })
|
||||||
global.d = true
|
|
||||||
const res = await payload.create({
|
const res = await payload.create({
|
||||||
collection: 'localized-posts',
|
collection: 'localized-posts',
|
||||||
draft: true,
|
draft: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user