diff --git a/packages/db-mongodb/src/findDistinct.ts b/packages/db-mongodb/src/findDistinct.ts index bc77a8cab..8ec994932 100644 --- a/packages/db-mongodb/src/findDistinct.ts +++ b/packages/db-mongodb/src/findDistinct.ts @@ -48,28 +48,56 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter, fieldPath = fieldPathResult.localizedPath.replace('', args.locale) } + const isHasManyValue = + fieldPathResult && 'hasMany' in fieldPathResult.field && fieldPathResult.field.hasMany + const page = args.page || 1 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 + if ( + isHasManyValue && + sortAggregation.length && + sortAggregation[0] && + '$lookup' in sortAggregation[0] + ) { + $unwind = `$${sortAggregation[0].$lookup.as}` + $group = { + _id: { + _field: `$${sortAggregation[0].$lookup.as}._id`, + _sort: `$${sortProperty}`, + }, + } + } else { + $group = { + _id: { + _field: `$${fieldPath}`, + ...(sortProperty === fieldPath + ? {} + : { + _sort: `$${sortProperty}`, + }), + }, + } + } + const pipeline: PipelineStage[] = [ { $match: query, }, ...(sortAggregation.length > 0 ? sortAggregation : []), - + ...($unwind + ? [ + { + $unwind, + }, + ] + : []), { - $group: { - _id: { - _field: `$${fieldPath}`, - ...(sortProperty === fieldPath - ? {} - : { - _sort: `$${sortProperty}`, - }), - }, - }, + $group, }, { $sort: { diff --git a/packages/drizzle/src/queries/getTableColumnFromPath.ts b/packages/drizzle/src/queries/getTableColumnFromPath.ts index a03e44c6b..ae5979541 100644 --- a/packages/drizzle/src/queries/getTableColumnFromPath.ts +++ b/packages/drizzle/src/queries/getTableColumnFromPath.ts @@ -1,4 +1,4 @@ -import type { SQL } from 'drizzle-orm' +import type { SQL, Table } from 'drizzle-orm' import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core' import type { FlattenedBlock, @@ -8,7 +8,7 @@ import type { TextField, } from 'payload' -import { and, eq, like, sql } from 'drizzle-orm' +import { and, eq, getTableName, like, sql } from 'drizzle-orm' import { type PgTableWithColumns } from 'drizzle-orm/pg-core' import { APIError, getFieldByPath } from 'payload' import { fieldShouldBeLocalized, tabHasName } from 'payload/shared' @@ -537,13 +537,22 @@ export const getTableColumnFromPath = ({ if (Array.isArray(field.relationTo) || field.hasMany) { let relationshipFields: FlattenedField[] const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}` - const { - newAliasTable: aliasRelationshipTable, - newAliasTableName: aliasRelationshipTableName, - } = getTableAlias({ - adapter, - tableName: relationTableName, - }) + + const existingJoin = joins.find((e) => e.queryPath === `${constraintPath}.${field.name}`) + + let aliasRelationshipTable: PgTableWithColumns | SQLiteTableWithColumns + let aliasRelationshipTableName: string + if (existingJoin) { + aliasRelationshipTable = existingJoin.table + aliasRelationshipTableName = getTableName(existingJoin.table) + } else { + const res = getTableAlias({ + adapter, + tableName: relationTableName, + }) + aliasRelationshipTable = res.newAliasTable + aliasRelationshipTableName = res.newAliasTableName + } if (selectLocale && isFieldLocalized && adapter.payload.config.localization) { selectFields._locale = aliasRelationshipTable.locale diff --git a/packages/payload/src/collections/operations/findDistinct.ts b/packages/payload/src/collections/operations/findDistinct.ts index fd7574866..5df99f845 100644 --- a/packages/payload/src/collections/operations/findDistinct.ts +++ b/packages/payload/src/collections/operations/findDistinct.ts @@ -155,6 +155,10 @@ export const findDistinctOperation = async ( args.depth ) { const populationPromises: Promise[] = [] + const sanitizedField = { ...fieldResult.field } + if (fieldResult.field.hasMany) { + sanitizedField.hasMany = false + } for (const doc of result.values) { populationPromises.push( relationshipPopulationPromise({ @@ -162,7 +166,7 @@ export const findDistinctOperation = async ( depth: args.depth, draft: false, fallbackLocale: req.fallbackLocale || null, - field: fieldResult.field, + field: sanitizedField, locale: req.locale || null, overrideAccess: args.overrideAccess ?? true, parentIsLocalized: false, diff --git a/test/database/getConfig.ts b/test/database/getConfig.ts index 02206721a..ea3cbde13 100644 --- a/test/database/getConfig.ts +++ b/test/database/getConfig.ts @@ -126,6 +126,12 @@ export const getConfig: () => Partial = () => ({ relationTo: 'categories', name: 'category', }, + { + type: 'relationship', + relationTo: 'categories', + hasMany: true, + name: 'categories', + }, { type: 'relationship', relationTo: 'categories-custom-id', diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index 87343b14c..cd3f313ac 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -856,6 +856,75 @@ describe('database', () => { } }) + it('should populate distinct relationships of hasMany: true when depth>0', async () => { + await payload.delete({ collection: 'posts', where: {} }) + await payload.delete({ collection: 'categories', where: {} }) + + const categories = ['category-1', 'category-2', 'category-3', 'category-4'].map((title) => ({ + title, + })) + + const categoriesIDS: { categories: string }[] = [] + + for (const { title } of categories) { + const doc = await payload.create({ collection: 'categories', data: { title } }) + categoriesIDS.push({ categories: doc.id }) + } + + await payload.create({ + collection: 'posts', + data: { + title: '1', + categories: [categoriesIDS[0]?.categories, categoriesIDS[1]?.categories], + }, + }) + + await payload.create({ + collection: 'posts', + data: { + title: '2', + categories: [ + categoriesIDS[0]?.categories, + categoriesIDS[2]?.categories, + categoriesIDS[3]?.categories, + ], + }, + }) + + await payload.create({ + collection: 'posts', + data: { + title: '3', + categories: [ + categoriesIDS[0]?.categories, + categoriesIDS[3]?.categories, + categoriesIDS[1]?.categories, + ], + }, + }) + + const resultDepth0 = await payload.findDistinct({ + collection: 'posts', + sort: 'categories.title', + field: 'categories', + }) + expect(resultDepth0.values).toStrictEqual(categoriesIDS) + const resultDepth1 = await payload.findDistinct({ + depth: 1, + collection: 'posts', + field: 'categories', + sort: 'categories.title', + }) + + for (let i = 0; i < resultDepth1.values.length; i++) { + const fromRes = resultDepth1.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) + } + }) + describe('Compound Indexes', () => { beforeEach(async () => { await payload.delete({ collection: 'compound-indexes', where: {} }) diff --git a/test/database/payload-types.ts b/test/database/payload-types.ts index 46ce4559e..4aa780ddf 100644 --- a/test/database/payload-types.ts +++ b/test/database/payload-types.ts @@ -197,6 +197,7 @@ export interface Post { id: string; title: string; category?: (string | null) | Category; + categories?: (string | Category)[] | null; categoryCustomID?: (number | null) | CategoriesCustomId; localized?: string | null; text?: string | null; @@ -822,6 +823,7 @@ export interface CategoriesCustomIdSelect { export interface PostsSelect { title?: T; category?: T; + categories?: T; categoryCustomID?: T; localized?: T; text?: T;