Skip to content

Commit e4f8478

Browse files
authored
feat: support any depth for relationships in findDistinct (#14090)
Follow up to #14026 ```ts // Supported before const relationResult = await payload.findDistinct({ collection: 'posts', field: 'relation1.title' }) // Supported now const relationResult = await payload.findDistinct({ collection: 'posts', field: 'relation1.relation2.title' }) const relationResult = await payload.findDistinct({ collection: 'posts', field: 'relation1.relation2.relation3.title' }) ```
1 parent ef84b20 commit e4f8478

File tree

7 files changed

+210
-51
lines changed

7 files changed

+210
-51
lines changed

packages/db-mongodb/src/findDistinct.ts

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { PipelineStage } from 'mongoose'
22
import type { FindDistinct, FlattenedField } from 'payload'
33

4-
import { APIError, getFieldByPath } from 'payload'
4+
import { getFieldByPath } from 'payload'
55

66
import type { MongooseAdapter } from './index.js'
77

@@ -20,7 +20,7 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
2020

2121
const { where = {} } = args
2222

23-
const sortAggregation: PipelineStage[] = []
23+
let sortAggregation: PipelineStage[] = []
2424

2525
const sort = buildSortParam({
2626
adapter: this,
@@ -41,7 +41,9 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
4141
})
4242

4343
const fieldPathResult = getFieldByPath({
44+
config: this.payload.config,
4445
fields: collectionConfig.flattenedFields,
46+
includeRelationships: true,
4547
path: args.field,
4648
})
4749
let fieldPath = args.field
@@ -55,76 +57,86 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
5557
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
5658

5759
let currentFields = collectionConfig.flattenedFields
58-
let relationTo: null | string = null
5960
let foundField: FlattenedField | null = null
60-
let foundFieldPath = ''
61-
let relationFieldPath = ''
61+
62+
let rels: {
63+
fieldPath: string
64+
relationTo: string
65+
}[] = []
66+
67+
let tempPath = ''
68+
let insideRelation = false
6269

6370
for (const segment of args.field.split('.')) {
6471
const field = currentFields.find((e) => e.name === segment)
72+
if (rels.length) {
73+
insideRelation = true
74+
}
6575

6676
if (!field) {
6777
break
6878
}
6979

70-
if (relationTo) {
71-
foundFieldPath = `${foundFieldPath}${field?.name}`
80+
if (tempPath) {
81+
tempPath = `${tempPath}.${field.name}`
7282
} else {
73-
relationFieldPath = `${relationFieldPath}${field.name}`
83+
tempPath = field.name
7484
}
7585

7686
if ('flattenedFields' in field) {
7787
currentFields = field.flattenedFields
78-
79-
if (relationTo) {
80-
foundFieldPath = `${foundFieldPath}.`
81-
} else {
82-
relationFieldPath = `${relationFieldPath}.`
83-
}
8488
continue
8589
}
8690

8791
if (
8892
(field.type === 'relationship' || field.type === 'upload') &&
8993
typeof field.relationTo === 'string'
9094
) {
91-
if (relationTo) {
92-
throw new APIError(
93-
`findDistinct for fields nested to relationships supported 1 level only, errored field: ${args.field}`,
94-
)
95-
}
96-
relationTo = field.relationTo
95+
rels.push({ fieldPath: tempPath, relationTo: field.relationTo })
9796
currentFields = this.payload.collections[field.relationTo]?.config
9897
.flattenedFields as FlattenedField[]
9998
continue
10099
}
101100
foundField = field
102-
103-
if (
104-
sortAggregation.some(
105-
(stage) => '$lookup' in stage && stage.$lookup.localField === relationFieldPath,
106-
)
107-
) {
108-
sortProperty = sortProperty.replace('__', '')
109-
sortAggregation.pop()
110-
}
111101
}
112102

113103
const resolvedField = foundField || fieldPathResult?.field
114104
const isHasManyValue = resolvedField && 'hasMany' in resolvedField && resolvedField
115105

116-
let relationLookup: null | PipelineStage = null
117-
if (relationTo && foundFieldPath && relationFieldPath) {
118-
const { Model: foreignModel } = getCollection({ adapter: this, collectionSlug: relationTo })
106+
let relationLookup: null | PipelineStage[] = null
119107

120-
relationLookup = {
121-
$lookup: {
122-
as: relationFieldPath,
123-
foreignField: '_id',
124-
from: foreignModel.collection.name,
125-
localField: relationFieldPath,
126-
},
108+
if (!insideRelation) {
109+
rels = []
110+
}
111+
112+
if (rels.length) {
113+
if (sortProperty.startsWith('_')) {
114+
const sortWithoutRelationPrefix = sortProperty.replace(/^_+/, '')
115+
const lastFieldPath = rels.at(-1)?.fieldPath as string
116+
if (sortWithoutRelationPrefix.startsWith(lastFieldPath)) {
117+
sortProperty = sortWithoutRelationPrefix
118+
}
127119
}
120+
relationLookup = rels.reduce<PipelineStage[]>((acc, { fieldPath, relationTo }) => {
121+
sortAggregation = sortAggregation.filter((each) => {
122+
if ('$lookup' in each && each.$lookup.as.replace(/^_+/, '') === fieldPath) {
123+
return false
124+
}
125+
126+
return true
127+
})
128+
const { Model: foreignModel } = getCollection({ adapter: this, collectionSlug: relationTo })
129+
acc.push({
130+
$lookup: {
131+
as: fieldPath,
132+
foreignField: '_id',
133+
from: foreignModel.collection.name,
134+
localField: fieldPath,
135+
},
136+
})
137+
acc.push({ $unwind: `$${fieldPath}` })
138+
return acc
139+
}, [])
128140
}
129141

130142
let $unwind: any = ''
@@ -164,7 +176,7 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
164176
$match: query,
165177
},
166178
...(sortAggregation.length > 0 ? sortAggregation : []),
167-
...(relationLookup ? [relationLookup, { $unwind: `$${relationFieldPath}` }] : []),
179+
...(relationLookup?.length ? relationLookup : []),
168180
...($unwind
169181
? [
170182
{

packages/drizzle/src/findDistinct.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export const findDistinct: FindDistinct = async function (this: DrizzleAdapter,
6464
})
6565

6666
const field = getFieldByPath({
67+
config: this.payload.config,
6768
fields: collectionConfig.flattenedFields,
69+
includeRelationships: true,
6870
path: args.field,
6971
})?.field
7072

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ export const findDistinctOperation = async (
118118
})
119119

120120
const fieldResult = getFieldByPath({
121+
config: payload.config,
121122
fields: collectionConfig.flattenedFields,
123+
includeRelationships: true,
122124
path: args.field,
123125
})
124126

packages/payload/src/utilities/getFieldByPath.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SanitizedConfig } from '../config/types.js'
12
import type { FlattenedField } from '../fields/config/types.js'
23

34
/**
@@ -7,11 +8,15 @@ import type { FlattenedField } from '../fields/config/types.js'
78
* group.<locale>.title // group is localized here
89
*/
910
export const getFieldByPath = ({
11+
config,
1012
fields,
13+
includeRelationships = false,
1114
localizedPath = '',
1215
path,
1316
}: {
17+
config?: SanitizedConfig
1418
fields: FlattenedField[]
19+
includeRelationships?: boolean
1520
localizedPath?: string
1621
path: string
1722
}): {
@@ -45,10 +50,26 @@ export const getFieldByPath = ({
4550
currentFields = field.flattenedFields
4651
}
4752

53+
if (
54+
config &&
55+
includeRelationships &&
56+
(field.type === 'relationship' || field.type === 'upload') &&
57+
!Array.isArray(field.relationTo)
58+
) {
59+
const flattenedFields = config.collections.find(
60+
(e) => e.slug === field.relationTo,
61+
)?.flattenedFields
62+
if (flattenedFields) {
63+
currentFields = flattenedFields
64+
}
65+
}
66+
4867
if ('blocks' in field) {
4968
for (const block of field.blocks) {
5069
const maybeField = getFieldByPath({
70+
config,
5171
fields: block.flattenedFields,
72+
includeRelationships,
5273
localizedPath,
5374
path: [...segments].join('.'),
5475
})

test/database/getConfig.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export const getConfig: () => Partial<Config> = () => ({
5454
type: 'text',
5555
name: 'title',
5656
},
57+
{
58+
name: 'simple',
59+
type: 'relationship',
60+
relationTo: 'simple',
61+
},
5762
{
5863
type: 'tabs',
5964
tabs: [
@@ -131,6 +136,11 @@ export const getConfig: () => Partial<Config> = () => ({
131136
name: 'categoryTitle',
132137
virtual: 'category.title',
133138
},
139+
{
140+
type: 'text',
141+
name: 'categorySimpleText',
142+
virtual: 'category.simple.text',
143+
},
134144
{
135145
type: 'relationship',
136146
relationTo: 'categories',

test/database/int.spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,114 @@ describe('database', () => {
11901190
])
11911191
})
11921192

1193+
it('should find distinct values with field nested to a 2x relationship', async () => {
1194+
await payload.delete({ collection: 'posts', where: {} })
1195+
await payload.delete({ collection: 'categories', where: {} })
1196+
await payload.delete({ collection: 'simple', where: {} })
1197+
1198+
const simple_1 = await payload.create({ collection: 'simple', data: { text: 'simple_1' } })
1199+
const simple_2 = await payload.create({ collection: 'simple', data: { text: 'simple_2' } })
1200+
const simple_3 = await payload.create({ collection: 'simple', data: { text: 'simple_3' } })
1201+
1202+
const category_1 = await payload.create({
1203+
collection: 'categories',
1204+
data: { title: 'category_1', simple: simple_1 },
1205+
})
1206+
const category_2 = await payload.create({
1207+
collection: 'categories',
1208+
data: { title: 'category_2', simple: simple_2 },
1209+
})
1210+
const category_3 = await payload.create({
1211+
collection: 'categories',
1212+
data: { title: 'category_3', simple: simple_3 },
1213+
})
1214+
const category_4 = await payload.create({
1215+
collection: 'categories',
1216+
data: { title: 'category_4', simple: simple_3 },
1217+
})
1218+
1219+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_1 } })
1220+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_2 } })
1221+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_2 } })
1222+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_2 } })
1223+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1224+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1225+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1226+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1227+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_4 } })
1228+
1229+
const res = await payload.findDistinct({
1230+
collection: 'posts',
1231+
field: 'category.simple.text',
1232+
})
1233+
1234+
expect(res.values).toEqual([
1235+
{
1236+
'category.simple.text': 'simple_1',
1237+
},
1238+
{
1239+
'category.simple.text': 'simple_2',
1240+
},
1241+
{
1242+
'category.simple.text': 'simple_3',
1243+
},
1244+
])
1245+
})
1246+
1247+
it('should find distinct values with virtual field linked to a 2x relationship', async () => {
1248+
await payload.delete({ collection: 'posts', where: {} })
1249+
await payload.delete({ collection: 'categories', where: {} })
1250+
await payload.delete({ collection: 'simple', where: {} })
1251+
1252+
const simple_1 = await payload.create({ collection: 'simple', data: { text: 'simple_1' } })
1253+
const simple_2 = await payload.create({ collection: 'simple', data: { text: 'simple_2' } })
1254+
const simple_3 = await payload.create({ collection: 'simple', data: { text: 'simple_3' } })
1255+
1256+
const category_1 = await payload.create({
1257+
collection: 'categories',
1258+
data: { title: 'category_1', simple: simple_1 },
1259+
})
1260+
const category_2 = await payload.create({
1261+
collection: 'categories',
1262+
data: { title: 'category_2', simple: simple_2 },
1263+
})
1264+
const category_3 = await payload.create({
1265+
collection: 'categories',
1266+
data: { title: 'category_3', simple: simple_3 },
1267+
})
1268+
const category_4 = await payload.create({
1269+
collection: 'categories',
1270+
data: { title: 'category_4', simple: simple_3 },
1271+
})
1272+
1273+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_1 } })
1274+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_2 } })
1275+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_2 } })
1276+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_2 } })
1277+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1278+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1279+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1280+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_3 } })
1281+
await payload.create({ collection: 'posts', data: { title: 'post', category: category_4 } })
1282+
1283+
const res = await payload.findDistinct({
1284+
collection: 'posts',
1285+
field: 'categorySimpleText',
1286+
})
1287+
1288+
expect(res.values).toEqual([
1289+
{
1290+
categorySimpleText: 'simple_1',
1291+
},
1292+
{
1293+
categorySimpleText: 'simple_2',
1294+
},
1295+
{
1296+
categorySimpleText: 'simple_3',
1297+
},
1298+
])
1299+
})
1300+
11931301
describe('Compound Indexes', () => {
11941302
beforeEach(async () => {
11951303
await payload.delete({ collection: 'compound-indexes', where: {} })

0 commit comments

Comments
 (0)