fix: findDistinct with polymorphic relationships (#13875)
Fixes `findDistinct` with polymorphic relationships and also fixes a bug from https://github.com/payloadcms/payload/pull/13840 when `findDistinct` didn't work properly for `hasMany` relationships in mongodb if `sort` is the same as `field` --------- Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
This commit is contained in:
@@ -56,22 +56,26 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
|
||||
const sortProperty = Object.keys(sort)[0]! // assert because buildSortParam always returns at least 1 key.
|
||||
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
|
||||
|
||||
let $unwind: string = ''
|
||||
let $group: any
|
||||
let $unwind: any = ''
|
||||
let $group: any = null
|
||||
if (
|
||||
isHasManyValue &&
|
||||
sortAggregation.length &&
|
||||
sortAggregation[0] &&
|
||||
'$lookup' in sortAggregation[0]
|
||||
) {
|
||||
$unwind = `$${sortAggregation[0].$lookup.as}`
|
||||
$unwind = { path: `$${sortAggregation[0].$lookup.as}`, preserveNullAndEmptyArrays: true }
|
||||
$group = {
|
||||
_id: {
|
||||
_field: `$${sortAggregation[0].$lookup.as}._id`,
|
||||
_sort: `$${sortProperty}`,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
} else if (isHasManyValue) {
|
||||
$unwind = { path: `$${args.field}`, preserveNullAndEmptyArrays: true }
|
||||
}
|
||||
|
||||
if (!$group) {
|
||||
$group = {
|
||||
_id: {
|
||||
_field: `$${fieldPath}`,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { FindDistinct, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { type FindDistinct, getFieldByPath, type SanitizedCollectionConfig } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter, GenericColumn } from './types.js'
|
||||
@@ -57,12 +56,32 @@ export const findDistinct: FindDistinct = async function (this: DrizzleAdapter,
|
||||
},
|
||||
selectFields: {
|
||||
_selected: selectFields['_selected'],
|
||||
...(orderBy[0].column === selectFields['_selected'] ? {} : { _order: orderBy[0].column }),
|
||||
...(orderBy.length &&
|
||||
(orderBy[0].column === selectFields['_selected'] ? {} : { _order: orderBy[0]?.column })),
|
||||
} as Record<string, GenericColumn>,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
const field = getFieldByPath({
|
||||
fields: collectionConfig.flattenedFields,
|
||||
path: args.field,
|
||||
})?.field
|
||||
|
||||
if (field && 'relationTo' in field && Array.isArray(field.relationTo)) {
|
||||
for (const row of selectDistinctResult as any) {
|
||||
const json = JSON.parse(row._selected)
|
||||
const relationTo = Object.keys(json).find((each) => Boolean(json[each]))
|
||||
const value = json[relationTo]
|
||||
|
||||
if (!value) {
|
||||
row._selected = null
|
||||
} else {
|
||||
row._selected = { relationTo, value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const values = selectDistinctResult.map((each) => ({
|
||||
[args.field]: (each as Record<string, any>)._selected,
|
||||
}))
|
||||
|
||||
@@ -19,6 +19,8 @@ import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
|
||||
import { jsonBuildObject } from '../utilities/json.js'
|
||||
import { DistinctSymbol } from '../utilities/rawConstraint.js'
|
||||
import { resolveBlockTableName } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { addJoinTable } from './addJoinTable.js'
|
||||
import { getTableAlias } from './getTableAlias.js'
|
||||
@@ -722,6 +724,28 @@ export const getTableColumnFromPath = ({
|
||||
rawColumn: sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`),
|
||||
table: aliasRelationshipTable,
|
||||
}
|
||||
} else if (value === DistinctSymbol) {
|
||||
const obj: Record<string, SQL> = {}
|
||||
|
||||
field.relationTo.forEach((relationTo) => {
|
||||
const relationTableName = adapter.tableNameMap.get(
|
||||
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
|
||||
)
|
||||
|
||||
obj[relationTo] = sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`)
|
||||
})
|
||||
|
||||
let rawColumn = jsonBuildObject(adapter, obj)
|
||||
if (adapter.name === 'postgres') {
|
||||
rawColumn = sql`${rawColumn}::text`
|
||||
}
|
||||
|
||||
return {
|
||||
constraints,
|
||||
field,
|
||||
rawColumn,
|
||||
table: aliasRelationshipTable,
|
||||
}
|
||||
} else {
|
||||
throw new APIError('Not supported')
|
||||
}
|
||||
|
||||
@@ -132,6 +132,17 @@ export const getConfig: () => Partial<Config> = () => ({
|
||||
hasMany: true,
|
||||
name: 'categories',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['categories'],
|
||||
name: 'categoryPoly',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['categories'],
|
||||
hasMany: true,
|
||||
name: 'categoryPolyMany',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'categories-custom-id',
|
||||
|
||||
@@ -923,6 +923,183 @@ describe('database', () => {
|
||||
expect(fromRes.categories.title).toBe(title)
|
||||
expect(fromRes.categories.id).toBe(id)
|
||||
}
|
||||
|
||||
// Non-consistent sorting by ID
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (process.env.PAYLOAD_DATABASE?.includes('uuid')) {
|
||||
return
|
||||
}
|
||||
|
||||
const resultDepth1NoSort = await payload.findDistinct({
|
||||
depth: 1,
|
||||
collection: 'posts',
|
||||
field: 'categories',
|
||||
})
|
||||
|
||||
for (let i = 0; i < resultDepth1NoSort.values.length; i++) {
|
||||
const fromRes = resultDepth1NoSort.values[i] as any
|
||||
const id = categoriesIDS[i].categories as any
|
||||
const title = categories[i]?.title
|
||||
expect(fromRes.categories.title).toBe(title)
|
||||
expect(fromRes.categories.id).toBe(id)
|
||||
}
|
||||
})
|
||||
|
||||
it('should populate distinct relationships of polymorphic when depth>0', async () => {
|
||||
await payload.delete({ collection: 'posts', where: {} })
|
||||
await payload.delete({ collection: 'categories', where: {} })
|
||||
|
||||
const category_1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category_1' },
|
||||
})
|
||||
const category_2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category_2' },
|
||||
})
|
||||
const category_3 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category_3' },
|
||||
})
|
||||
|
||||
const post_1 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'post_1', categoryPoly: { relationTo: 'categories', value: category_1.id } },
|
||||
})
|
||||
const post_2 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'post_2', categoryPoly: { relationTo: 'categories', value: category_1.id } },
|
||||
})
|
||||
const post_3 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'post_3', categoryPoly: { relationTo: 'categories', value: category_2.id } },
|
||||
})
|
||||
const post_4 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'post_4', categoryPoly: { relationTo: 'categories', value: category_3.id } },
|
||||
})
|
||||
const post_5 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'post_5', categoryPoly: { relationTo: 'categories', value: category_3.id } },
|
||||
})
|
||||
|
||||
const result = await payload.findDistinct({
|
||||
depth: 0,
|
||||
collection: 'posts',
|
||||
field: 'categoryPoly',
|
||||
})
|
||||
|
||||
expect(result.values).toHaveLength(3)
|
||||
expect(
|
||||
result.values.some(
|
||||
(v) =>
|
||||
v.categoryPoly?.relationTo === 'categories' && v.categoryPoly.value === category_1.id,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
result.values.some(
|
||||
(v) =>
|
||||
v.categoryPoly?.relationTo === 'categories' && v.categoryPoly.value === category_2.id,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
result.values.some(
|
||||
(v) =>
|
||||
v.categoryPoly?.relationTo === 'categories' && v.categoryPoly.value === category_3.id,
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should populate distinct relationships of hasMany polymorphic when depth>0', async () => {
|
||||
await payload.delete({ collection: 'posts', where: {} })
|
||||
await payload.delete({ collection: 'categories', where: {} })
|
||||
|
||||
const category_1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category_1' },
|
||||
})
|
||||
const category_2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category_2' },
|
||||
})
|
||||
const category_3 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category_3' },
|
||||
})
|
||||
|
||||
const post_1 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post_1',
|
||||
categoryPolyMany: [{ relationTo: 'categories', value: category_1.id }],
|
||||
},
|
||||
})
|
||||
const post_2 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post_2',
|
||||
categoryPolyMany: [{ relationTo: 'categories', value: category_1.id }],
|
||||
},
|
||||
})
|
||||
const post_3 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post_3',
|
||||
categoryPolyMany: [{ relationTo: 'categories', value: category_2.id }],
|
||||
},
|
||||
})
|
||||
const post_4 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post_4',
|
||||
categoryPolyMany: [{ relationTo: 'categories', value: category_3.id }],
|
||||
},
|
||||
})
|
||||
const post_5 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post_5',
|
||||
categoryPolyMany: [{ relationTo: 'categories', value: category_3.id }],
|
||||
},
|
||||
})
|
||||
|
||||
const post_6 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'post_6',
|
||||
categoryPolyMany: null,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await payload.findDistinct({
|
||||
depth: 0,
|
||||
collection: 'posts',
|
||||
field: 'categoryPolyMany',
|
||||
})
|
||||
|
||||
expect(result.values).toHaveLength(4)
|
||||
expect(
|
||||
result.values.some(
|
||||
(v) =>
|
||||
v.categoryPolyMany?.relationTo === 'categories' &&
|
||||
v.categoryPolyMany.value === category_1.id,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
result.values.some(
|
||||
(v) =>
|
||||
v.categoryPolyMany?.relationTo === 'categories' &&
|
||||
v.categoryPolyMany.value === category_2.id,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
result.values.some(
|
||||
(v) =>
|
||||
v.categoryPolyMany?.relationTo === 'categories' &&
|
||||
v.categoryPolyMany.value === category_3.id,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(result.values.some((v) => v.categoryPolyMany === null)).toBe(true)
|
||||
})
|
||||
|
||||
describe('Compound Indexes', () => {
|
||||
|
||||
@@ -198,6 +198,16 @@ export interface Post {
|
||||
title: string;
|
||||
category?: (string | null) | Category;
|
||||
categories?: (string | Category)[] | null;
|
||||
categoryPoly?: {
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | null;
|
||||
categoryPolyMany?:
|
||||
| {
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
}[]
|
||||
| null;
|
||||
categoryCustomID?: (number | null) | CategoriesCustomId;
|
||||
localized?: string | null;
|
||||
text?: string | null;
|
||||
@@ -827,6 +837,8 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
category?: T;
|
||||
categories?: T;
|
||||
categoryPoly?: T;
|
||||
categoryPolyMany?: T;
|
||||
categoryCustomID?: T;
|
||||
localized?: T;
|
||||
text?: T;
|
||||
|
||||
Reference in New Issue
Block a user