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 sortProperty = Object.keys(sort)[0]! // assert because buildSortParam always returns at least 1 key.
|
||||||
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
|
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
|
||||||
|
|
||||||
let $unwind: string = ''
|
let $unwind: any = ''
|
||||||
let $group: any
|
let $group: any = null
|
||||||
if (
|
if (
|
||||||
isHasManyValue &&
|
isHasManyValue &&
|
||||||
sortAggregation.length &&
|
sortAggregation.length &&
|
||||||
sortAggregation[0] &&
|
sortAggregation[0] &&
|
||||||
'$lookup' in sortAggregation[0]
|
'$lookup' in sortAggregation[0]
|
||||||
) {
|
) {
|
||||||
$unwind = `$${sortAggregation[0].$lookup.as}`
|
$unwind = { path: `$${sortAggregation[0].$lookup.as}`, preserveNullAndEmptyArrays: true }
|
||||||
$group = {
|
$group = {
|
||||||
_id: {
|
_id: {
|
||||||
_field: `$${sortAggregation[0].$lookup.as}._id`,
|
_field: `$${sortAggregation[0].$lookup.as}._id`,
|
||||||
_sort: `$${sortProperty}`,
|
_sort: `$${sortProperty}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else if (isHasManyValue) {
|
||||||
|
$unwind = { path: `$${args.field}`, preserveNullAndEmptyArrays: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$group) {
|
||||||
$group = {
|
$group = {
|
||||||
_id: {
|
_id: {
|
||||||
_field: `$${fieldPath}`,
|
_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 toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
import type { DrizzleAdapter, GenericColumn } from './types.js'
|
import type { DrizzleAdapter, GenericColumn } from './types.js'
|
||||||
@@ -57,12 +56,32 @@ export const findDistinct: FindDistinct = async function (this: DrizzleAdapter,
|
|||||||
},
|
},
|
||||||
selectFields: {
|
selectFields: {
|
||||||
_selected: selectFields['_selected'],
|
_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>,
|
} as Record<string, GenericColumn>,
|
||||||
tableName,
|
tableName,
|
||||||
where,
|
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) => ({
|
const values = selectDistinctResult.map((each) => ({
|
||||||
[args.field]: (each as Record<string, any>)._selected,
|
[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 type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||||
|
|
||||||
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.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 { resolveBlockTableName } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||||
import { addJoinTable } from './addJoinTable.js'
|
import { addJoinTable } from './addJoinTable.js'
|
||||||
import { getTableAlias } from './getTableAlias.js'
|
import { getTableAlias } from './getTableAlias.js'
|
||||||
@@ -722,6 +724,28 @@ export const getTableColumnFromPath = ({
|
|||||||
rawColumn: sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`),
|
rawColumn: sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`),
|
||||||
table: aliasRelationshipTable,
|
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 {
|
} else {
|
||||||
throw new APIError('Not supported')
|
throw new APIError('Not supported')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,17 @@ export const getConfig: () => Partial<Config> = () => ({
|
|||||||
hasMany: true,
|
hasMany: true,
|
||||||
name: 'categories',
|
name: 'categories',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['categories'],
|
||||||
|
name: 'categoryPoly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['categories'],
|
||||||
|
hasMany: true,
|
||||||
|
name: 'categoryPolyMany',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationTo: 'categories-custom-id',
|
relationTo: 'categories-custom-id',
|
||||||
|
|||||||
@@ -923,6 +923,183 @@ describe('database', () => {
|
|||||||
expect(fromRes.categories.title).toBe(title)
|
expect(fromRes.categories.title).toBe(title)
|
||||||
expect(fromRes.categories.id).toBe(id)
|
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', () => {
|
describe('Compound Indexes', () => {
|
||||||
|
|||||||
@@ -198,6 +198,16 @@ export interface Post {
|
|||||||
title: string;
|
title: string;
|
||||||
category?: (string | null) | Category;
|
category?: (string | null) | Category;
|
||||||
categories?: (string | Category)[] | null;
|
categories?: (string | Category)[] | null;
|
||||||
|
categoryPoly?: {
|
||||||
|
relationTo: 'categories';
|
||||||
|
value: string | Category;
|
||||||
|
} | null;
|
||||||
|
categoryPolyMany?:
|
||||||
|
| {
|
||||||
|
relationTo: 'categories';
|
||||||
|
value: string | Category;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
categoryCustomID?: (number | null) | CategoriesCustomId;
|
categoryCustomID?: (number | null) | CategoriesCustomId;
|
||||||
localized?: string | null;
|
localized?: string | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
@@ -827,6 +837,8 @@ export interface PostsSelect<T extends boolean = true> {
|
|||||||
title?: T;
|
title?: T;
|
||||||
category?: T;
|
category?: T;
|
||||||
categories?: T;
|
categories?: T;
|
||||||
|
categoryPoly?: T;
|
||||||
|
categoryPolyMany?: T;
|
||||||
categoryCustomID?: T;
|
categoryCustomID?: T;
|
||||||
localized?: T;
|
localized?: T;
|
||||||
text?: T;
|
text?: T;
|
||||||
|
|||||||
Reference in New Issue
Block a user