Skip to content

Commit d0543a4

Browse files
authored
fix: support hasMany: true relationships in findDistinct (#13840)
Previously, the `findDistinct` operation didn't work correctly for relationships with `hasMany: true`. This PR fixes it.
1 parent a26d8d9 commit d0543a4

File tree

6 files changed

+139
-21
lines changed

6 files changed

+139
-21
lines changed

packages/db-mongodb/src/findDistinct.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,56 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
4848
fieldPath = fieldPathResult.localizedPath.replace('<locale>', args.locale)
4949
}
5050

51+
const isHasManyValue =
52+
fieldPathResult && 'hasMany' in fieldPathResult.field && fieldPathResult.field.hasMany
53+
5154
const page = args.page || 1
5255

5356
const sortProperty = Object.keys(sort)[0]! // assert because buildSortParam always returns at least 1 key.
5457
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
5558

59+
let $unwind: string = ''
60+
let $group: any
61+
if (
62+
isHasManyValue &&
63+
sortAggregation.length &&
64+
sortAggregation[0] &&
65+
'$lookup' in sortAggregation[0]
66+
) {
67+
$unwind = `$${sortAggregation[0].$lookup.as}`
68+
$group = {
69+
_id: {
70+
_field: `$${sortAggregation[0].$lookup.as}._id`,
71+
_sort: `$${sortProperty}`,
72+
},
73+
}
74+
} else {
75+
$group = {
76+
_id: {
77+
_field: `$${fieldPath}`,
78+
...(sortProperty === fieldPath
79+
? {}
80+
: {
81+
_sort: `$${sortProperty}`,
82+
}),
83+
},
84+
}
85+
}
86+
5687
const pipeline: PipelineStage[] = [
5788
{
5889
$match: query,
5990
},
6091
...(sortAggregation.length > 0 ? sortAggregation : []),
61-
92+
...($unwind
93+
? [
94+
{
95+
$unwind,
96+
},
97+
]
98+
: []),
6299
{
63-
$group: {
64-
_id: {
65-
_field: `$${fieldPath}`,
66-
...(sortProperty === fieldPath
67-
? {}
68-
: {
69-
_sort: `$${sortProperty}`,
70-
}),
71-
},
72-
},
100+
$group,
73101
},
74102
{
75103
$sort: {

packages/drizzle/src/queries/getTableColumnFromPath.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SQL } from 'drizzle-orm'
1+
import type { SQL, Table } from 'drizzle-orm'
22
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
33
import type {
44
FlattenedBlock,
@@ -8,7 +8,7 @@ import type {
88
TextField,
99
} from 'payload'
1010

11-
import { and, eq, like, sql } from 'drizzle-orm'
11+
import { and, eq, getTableName, like, sql } from 'drizzle-orm'
1212
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
1313
import { APIError, getFieldByPath } from 'payload'
1414
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
@@ -537,13 +537,22 @@ export const getTableColumnFromPath = ({
537537
if (Array.isArray(field.relationTo) || field.hasMany) {
538538
let relationshipFields: FlattenedField[]
539539
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
540-
const {
541-
newAliasTable: aliasRelationshipTable,
542-
newAliasTableName: aliasRelationshipTableName,
543-
} = getTableAlias({
544-
adapter,
545-
tableName: relationTableName,
546-
})
540+
541+
const existingJoin = joins.find((e) => e.queryPath === `${constraintPath}.${field.name}`)
542+
543+
let aliasRelationshipTable: PgTableWithColumns<any> | SQLiteTableWithColumns<any>
544+
let aliasRelationshipTableName: string
545+
if (existingJoin) {
546+
aliasRelationshipTable = existingJoin.table
547+
aliasRelationshipTableName = getTableName(existingJoin.table)
548+
} else {
549+
const res = getTableAlias({
550+
adapter,
551+
tableName: relationTableName,
552+
})
553+
aliasRelationshipTable = res.newAliasTable
554+
aliasRelationshipTableName = res.newAliasTableName
555+
}
547556

548557
if (selectLocale && isFieldLocalized && adapter.payload.config.localization) {
549558
selectFields._locale = aliasRelationshipTable.locale

packages/payload/src/collections/operations/findDistinct.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,18 @@ export const findDistinctOperation = async (
155155
args.depth
156156
) {
157157
const populationPromises: Promise<void>[] = []
158+
const sanitizedField = { ...fieldResult.field }
159+
if (fieldResult.field.hasMany) {
160+
sanitizedField.hasMany = false
161+
}
158162
for (const doc of result.values) {
159163
populationPromises.push(
160164
relationshipPopulationPromise({
161165
currentDepth: 0,
162166
depth: args.depth,
163167
draft: false,
164168
fallbackLocale: req.fallbackLocale || null,
165-
field: fieldResult.field,
169+
field: sanitizedField,
166170
locale: req.locale || null,
167171
overrideAccess: args.overrideAccess ?? true,
168172
parentIsLocalized: false,

test/database/getConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ export const getConfig: () => Partial<Config> = () => ({
126126
relationTo: 'categories',
127127
name: 'category',
128128
},
129+
{
130+
type: 'relationship',
131+
relationTo: 'categories',
132+
hasMany: true,
133+
name: 'categories',
134+
},
129135
{
130136
type: 'relationship',
131137
relationTo: 'categories-custom-id',

test/database/int.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,75 @@ describe('database', () => {
856856
}
857857
})
858858

859+
it('should populate distinct relationships of hasMany: true when depth>0', async () => {
860+
await payload.delete({ collection: 'posts', where: {} })
861+
await payload.delete({ collection: 'categories', where: {} })
862+
863+
const categories = ['category-1', 'category-2', 'category-3', 'category-4'].map((title) => ({
864+
title,
865+
}))
866+
867+
const categoriesIDS: { categories: string }[] = []
868+
869+
for (const { title } of categories) {
870+
const doc = await payload.create({ collection: 'categories', data: { title } })
871+
categoriesIDS.push({ categories: doc.id })
872+
}
873+
874+
await payload.create({
875+
collection: 'posts',
876+
data: {
877+
title: '1',
878+
categories: [categoriesIDS[0]?.categories, categoriesIDS[1]?.categories],
879+
},
880+
})
881+
882+
await payload.create({
883+
collection: 'posts',
884+
data: {
885+
title: '2',
886+
categories: [
887+
categoriesIDS[0]?.categories,
888+
categoriesIDS[2]?.categories,
889+
categoriesIDS[3]?.categories,
890+
],
891+
},
892+
})
893+
894+
await payload.create({
895+
collection: 'posts',
896+
data: {
897+
title: '3',
898+
categories: [
899+
categoriesIDS[0]?.categories,
900+
categoriesIDS[3]?.categories,
901+
categoriesIDS[1]?.categories,
902+
],
903+
},
904+
})
905+
906+
const resultDepth0 = await payload.findDistinct({
907+
collection: 'posts',
908+
sort: 'categories.title',
909+
field: 'categories',
910+
})
911+
expect(resultDepth0.values).toStrictEqual(categoriesIDS)
912+
const resultDepth1 = await payload.findDistinct({
913+
depth: 1,
914+
collection: 'posts',
915+
field: 'categories',
916+
sort: 'categories.title',
917+
})
918+
919+
for (let i = 0; i < resultDepth1.values.length; i++) {
920+
const fromRes = resultDepth1.values[i] as any
921+
const id = categoriesIDS[i].categories as any
922+
const title = categories[i]?.title
923+
expect(fromRes.categories.title).toBe(title)
924+
expect(fromRes.categories.id).toBe(id)
925+
}
926+
})
927+
859928
describe('Compound Indexes', () => {
860929
beforeEach(async () => {
861930
await payload.delete({ collection: 'compound-indexes', where: {} })

test/database/payload-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export interface Post {
197197
id: string;
198198
title: string;
199199
category?: (string | null) | Category;
200+
categories?: (string | Category)[] | null;
200201
categoryCustomID?: (number | null) | CategoriesCustomId;
201202
localized?: string | null;
202203
text?: string | null;
@@ -822,6 +823,7 @@ export interface CategoriesCustomIdSelect<T extends boolean = true> {
822823
export interface PostsSelect<T extends boolean = true> {
823824
title?: T;
824825
category?: T;
826+
categories?: T;
825827
categoryCustomID?: T;
826828
localized?: T;
827829
text?: T;

0 commit comments

Comments
 (0)