Skip to content

Commit e99e054

Browse files
r1tsuuPatrikKozak
andauthored
fix: findDistinct with polymorphic relationships (#13875)
Fixes `findDistinct` with polymorphic relationships and also fixes a bug from #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>
1 parent 82d98ab commit e99e054

File tree

6 files changed

+254
-7
lines changed

6 files changed

+254
-7
lines changed

packages/db-mongodb/src/findDistinct.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,26 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
5656
const sortProperty = Object.keys(sort)[0]! // assert because buildSortParam always returns at least 1 key.
5757
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
5858

59-
let $unwind: string = ''
60-
let $group: any
59+
let $unwind: any = ''
60+
let $group: any = null
6161
if (
6262
isHasManyValue &&
6363
sortAggregation.length &&
6464
sortAggregation[0] &&
6565
'$lookup' in sortAggregation[0]
6666
) {
67-
$unwind = `$${sortAggregation[0].$lookup.as}`
67+
$unwind = { path: `$${sortAggregation[0].$lookup.as}`, preserveNullAndEmptyArrays: true }
6868
$group = {
6969
_id: {
7070
_field: `$${sortAggregation[0].$lookup.as}._id`,
7171
_sort: `$${sortProperty}`,
7272
},
7373
}
74-
} else {
74+
} else if (isHasManyValue) {
75+
$unwind = { path: `$${args.field}`, preserveNullAndEmptyArrays: true }
76+
}
77+
78+
if (!$group) {
7579
$group = {
7680
_id: {
7781
_field: `$${fieldPath}`,

packages/drizzle/src/findDistinct.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { FindDistinct, SanitizedCollectionConfig } from 'payload'
2-
1+
import { type FindDistinct, getFieldByPath, type SanitizedCollectionConfig } from 'payload'
32
import toSnakeCase from 'to-snake-case'
43

54
import type { DrizzleAdapter, GenericColumn } from './types.js'
@@ -57,12 +56,32 @@ export const findDistinct: FindDistinct = async function (this: DrizzleAdapter,
5756
},
5857
selectFields: {
5958
_selected: selectFields['_selected'],
60-
...(orderBy[0].column === selectFields['_selected'] ? {} : { _order: orderBy[0].column }),
59+
...(orderBy.length &&
60+
(orderBy[0].column === selectFields['_selected'] ? {} : { _order: orderBy[0]?.column })),
6161
} as Record<string, GenericColumn>,
6262
tableName,
6363
where,
6464
})
6565

66+
const field = getFieldByPath({
67+
fields: collectionConfig.flattenedFields,
68+
path: args.field,
69+
})?.field
70+
71+
if (field && 'relationTo' in field && Array.isArray(field.relationTo)) {
72+
for (const row of selectDistinctResult as any) {
73+
const json = JSON.parse(row._selected)
74+
const relationTo = Object.keys(json).find((each) => Boolean(json[each]))
75+
const value = json[relationTo]
76+
77+
if (!value) {
78+
row._selected = null
79+
} else {
80+
row._selected = { relationTo, value }
81+
}
82+
}
83+
}
84+
6685
const values = selectDistinctResult.map((each) => ({
6786
[args.field]: (each as Record<string, any>)._selected,
6887
}))

packages/drizzle/src/queries/getTableColumnFromPath.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import type { DrizzleAdapter, GenericColumn } from '../types.js'
1919
import type { BuildQueryJoinAliases } from './buildQuery.js'
2020

2121
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
22+
import { jsonBuildObject } from '../utilities/json.js'
23+
import { DistinctSymbol } from '../utilities/rawConstraint.js'
2224
import { resolveBlockTableName } from '../utilities/validateExistingBlockIsIdentical.js'
2325
import { addJoinTable } from './addJoinTable.js'
2426
import { getTableAlias } from './getTableAlias.js'
@@ -722,6 +724,28 @@ export const getTableColumnFromPath = ({
722724
rawColumn: sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`),
723725
table: aliasRelationshipTable,
724726
}
727+
} else if (value === DistinctSymbol) {
728+
const obj: Record<string, SQL> = {}
729+
730+
field.relationTo.forEach((relationTo) => {
731+
const relationTableName = adapter.tableNameMap.get(
732+
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
733+
)
734+
735+
obj[relationTo] = sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`)
736+
})
737+
738+
let rawColumn = jsonBuildObject(adapter, obj)
739+
if (adapter.name === 'postgres') {
740+
rawColumn = sql`${rawColumn}::text`
741+
}
742+
743+
return {
744+
constraints,
745+
field,
746+
rawColumn,
747+
table: aliasRelationshipTable,
748+
}
725749
} else {
726750
throw new APIError('Not supported')
727751
}

test/database/getConfig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ export const getConfig: () => Partial<Config> = () => ({
132132
hasMany: true,
133133
name: 'categories',
134134
},
135+
{
136+
type: 'relationship',
137+
relationTo: ['categories'],
138+
name: 'categoryPoly',
139+
},
140+
{
141+
type: 'relationship',
142+
relationTo: ['categories'],
143+
hasMany: true,
144+
name: 'categoryPolyMany',
145+
},
135146
{
136147
type: 'relationship',
137148
relationTo: 'categories-custom-id',

test/database/int.spec.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,183 @@ describe('database', () => {
923923
expect(fromRes.categories.title).toBe(title)
924924
expect(fromRes.categories.id).toBe(id)
925925
}
926+
927+
// Non-consistent sorting by ID
928+
// eslint-disable-next-line jest/no-conditional-in-test
929+
if (process.env.PAYLOAD_DATABASE?.includes('uuid')) {
930+
return
931+
}
932+
933+
const resultDepth1NoSort = await payload.findDistinct({
934+
depth: 1,
935+
collection: 'posts',
936+
field: 'categories',
937+
})
938+
939+
for (let i = 0; i < resultDepth1NoSort.values.length; i++) {
940+
const fromRes = resultDepth1NoSort.values[i] as any
941+
const id = categoriesIDS[i].categories as any
942+
const title = categories[i]?.title
943+
expect(fromRes.categories.title).toBe(title)
944+
expect(fromRes.categories.id).toBe(id)
945+
}
946+
})
947+
948+
it('should populate distinct relationships of polymorphic when depth>0', async () => {
949+
await payload.delete({ collection: 'posts', where: {} })
950+
await payload.delete({ collection: 'categories', where: {} })
951+
952+
const category_1 = await payload.create({
953+
collection: 'categories',
954+
data: { title: 'category_1' },
955+
})
956+
const category_2 = await payload.create({
957+
collection: 'categories',
958+
data: { title: 'category_2' },
959+
})
960+
const category_3 = await payload.create({
961+
collection: 'categories',
962+
data: { title: 'category_3' },
963+
})
964+
965+
const post_1 = await payload.create({
966+
collection: 'posts',
967+
data: { title: 'post_1', categoryPoly: { relationTo: 'categories', value: category_1.id } },
968+
})
969+
const post_2 = await payload.create({
970+
collection: 'posts',
971+
data: { title: 'post_2', categoryPoly: { relationTo: 'categories', value: category_1.id } },
972+
})
973+
const post_3 = await payload.create({
974+
collection: 'posts',
975+
data: { title: 'post_3', categoryPoly: { relationTo: 'categories', value: category_2.id } },
976+
})
977+
const post_4 = await payload.create({
978+
collection: 'posts',
979+
data: { title: 'post_4', categoryPoly: { relationTo: 'categories', value: category_3.id } },
980+
})
981+
const post_5 = await payload.create({
982+
collection: 'posts',
983+
data: { title: 'post_5', categoryPoly: { relationTo: 'categories', value: category_3.id } },
984+
})
985+
986+
const result = await payload.findDistinct({
987+
depth: 0,
988+
collection: 'posts',
989+
field: 'categoryPoly',
990+
})
991+
992+
expect(result.values).toHaveLength(3)
993+
expect(
994+
result.values.some(
995+
(v) =>
996+
v.categoryPoly?.relationTo === 'categories' && v.categoryPoly.value === category_1.id,
997+
),
998+
).toBe(true)
999+
expect(
1000+
result.values.some(
1001+
(v) =>
1002+
v.categoryPoly?.relationTo === 'categories' && v.categoryPoly.value === category_2.id,
1003+
),
1004+
).toBe(true)
1005+
expect(
1006+
result.values.some(
1007+
(v) =>
1008+
v.categoryPoly?.relationTo === 'categories' && v.categoryPoly.value === category_3.id,
1009+
),
1010+
).toBe(true)
1011+
})
1012+
1013+
it('should populate distinct relationships of hasMany polymorphic when depth>0', async () => {
1014+
await payload.delete({ collection: 'posts', where: {} })
1015+
await payload.delete({ collection: 'categories', where: {} })
1016+
1017+
const category_1 = await payload.create({
1018+
collection: 'categories',
1019+
data: { title: 'category_1' },
1020+
})
1021+
const category_2 = await payload.create({
1022+
collection: 'categories',
1023+
data: { title: 'category_2' },
1024+
})
1025+
const category_3 = await payload.create({
1026+
collection: 'categories',
1027+
data: { title: 'category_3' },
1028+
})
1029+
1030+
const post_1 = await payload.create({
1031+
collection: 'posts',
1032+
data: {
1033+
title: 'post_1',
1034+
categoryPolyMany: [{ relationTo: 'categories', value: category_1.id }],
1035+
},
1036+
})
1037+
const post_2 = await payload.create({
1038+
collection: 'posts',
1039+
data: {
1040+
title: 'post_2',
1041+
categoryPolyMany: [{ relationTo: 'categories', value: category_1.id }],
1042+
},
1043+
})
1044+
const post_3 = await payload.create({
1045+
collection: 'posts',
1046+
data: {
1047+
title: 'post_3',
1048+
categoryPolyMany: [{ relationTo: 'categories', value: category_2.id }],
1049+
},
1050+
})
1051+
const post_4 = await payload.create({
1052+
collection: 'posts',
1053+
data: {
1054+
title: 'post_4',
1055+
categoryPolyMany: [{ relationTo: 'categories', value: category_3.id }],
1056+
},
1057+
})
1058+
const post_5 = await payload.create({
1059+
collection: 'posts',
1060+
data: {
1061+
title: 'post_5',
1062+
categoryPolyMany: [{ relationTo: 'categories', value: category_3.id }],
1063+
},
1064+
})
1065+
1066+
const post_6 = await payload.create({
1067+
collection: 'posts',
1068+
data: {
1069+
title: 'post_6',
1070+
categoryPolyMany: null,
1071+
},
1072+
})
1073+
1074+
const result = await payload.findDistinct({
1075+
depth: 0,
1076+
collection: 'posts',
1077+
field: 'categoryPolyMany',
1078+
})
1079+
1080+
expect(result.values).toHaveLength(4)
1081+
expect(
1082+
result.values.some(
1083+
(v) =>
1084+
v.categoryPolyMany?.relationTo === 'categories' &&
1085+
v.categoryPolyMany.value === category_1.id,
1086+
),
1087+
).toBe(true)
1088+
expect(
1089+
result.values.some(
1090+
(v) =>
1091+
v.categoryPolyMany?.relationTo === 'categories' &&
1092+
v.categoryPolyMany.value === category_2.id,
1093+
),
1094+
).toBe(true)
1095+
expect(
1096+
result.values.some(
1097+
(v) =>
1098+
v.categoryPolyMany?.relationTo === 'categories' &&
1099+
v.categoryPolyMany.value === category_3.id,
1100+
),
1101+
).toBe(true)
1102+
expect(result.values.some((v) => v.categoryPolyMany === null)).toBe(true)
9261103
})
9271104

9281105
describe('Compound Indexes', () => {

test/database/payload-types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ export interface Post {
198198
title: string;
199199
category?: (string | null) | Category;
200200
categories?: (string | Category)[] | null;
201+
categoryPoly?: {
202+
relationTo: 'categories';
203+
value: string | Category;
204+
} | null;
205+
categoryPolyMany?:
206+
| {
207+
relationTo: 'categories';
208+
value: string | Category;
209+
}[]
210+
| null;
201211
categoryCustomID?: (number | null) | CategoriesCustomId;
202212
localized?: string | null;
203213
text?: string | null;
@@ -827,6 +837,8 @@ export interface PostsSelect<T extends boolean = true> {
827837
title?: T;
828838
category?: T;
829839
categories?: T;
840+
categoryPoly?: T;
841+
categoryPolyMany?: T;
830842
categoryCustomID?: T;
831843
localized?: T;
832844
text?: T;

0 commit comments

Comments
 (0)