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.
This commit is contained in:
@@ -48,28 +48,56 @@ export const findDistinct: FindDistinct = async function (this: MongooseAdapter,
|
|||||||
fieldPath = fieldPathResult.localizedPath.replace('<locale>', args.locale)
|
fieldPath = fieldPathResult.localizedPath.replace('<locale>', args.locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isHasManyValue =
|
||||||
|
fieldPathResult && 'hasMany' in fieldPathResult.field && fieldPathResult.field.hasMany
|
||||||
|
|
||||||
const page = args.page || 1
|
const page = args.page || 1
|
||||||
|
|
||||||
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 $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[] = [
|
const pipeline: PipelineStage[] = [
|
||||||
{
|
{
|
||||||
$match: query,
|
$match: query,
|
||||||
},
|
},
|
||||||
...(sortAggregation.length > 0 ? sortAggregation : []),
|
...(sortAggregation.length > 0 ? sortAggregation : []),
|
||||||
|
...($unwind
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
$unwind,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
$group: {
|
$group,
|
||||||
_id: {
|
|
||||||
_field: `$${fieldPath}`,
|
|
||||||
...(sortProperty === fieldPath
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
_sort: `$${sortProperty}`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$sort: {
|
$sort: {
|
||||||
|
|||||||
@@ -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 { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
|
||||||
import type {
|
import type {
|
||||||
FlattenedBlock,
|
FlattenedBlock,
|
||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
TextField,
|
TextField,
|
||||||
} from 'payload'
|
} 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 { type PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||||
import { APIError, getFieldByPath } from 'payload'
|
import { APIError, getFieldByPath } from 'payload'
|
||||||
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
|
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
|
||||||
@@ -537,13 +537,22 @@ export const getTableColumnFromPath = ({
|
|||||||
if (Array.isArray(field.relationTo) || field.hasMany) {
|
if (Array.isArray(field.relationTo) || field.hasMany) {
|
||||||
let relationshipFields: FlattenedField[]
|
let relationshipFields: FlattenedField[]
|
||||||
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
||||||
const {
|
|
||||||
newAliasTable: aliasRelationshipTable,
|
const existingJoin = joins.find((e) => e.queryPath === `${constraintPath}.${field.name}`)
|
||||||
newAliasTableName: aliasRelationshipTableName,
|
|
||||||
} = getTableAlias({
|
let aliasRelationshipTable: PgTableWithColumns<any> | SQLiteTableWithColumns<any>
|
||||||
adapter,
|
let aliasRelationshipTableName: string
|
||||||
tableName: relationTableName,
|
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) {
|
if (selectLocale && isFieldLocalized && adapter.payload.config.localization) {
|
||||||
selectFields._locale = aliasRelationshipTable.locale
|
selectFields._locale = aliasRelationshipTable.locale
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ export const findDistinctOperation = async (
|
|||||||
args.depth
|
args.depth
|
||||||
) {
|
) {
|
||||||
const populationPromises: Promise<void>[] = []
|
const populationPromises: Promise<void>[] = []
|
||||||
|
const sanitizedField = { ...fieldResult.field }
|
||||||
|
if (fieldResult.field.hasMany) {
|
||||||
|
sanitizedField.hasMany = false
|
||||||
|
}
|
||||||
for (const doc of result.values) {
|
for (const doc of result.values) {
|
||||||
populationPromises.push(
|
populationPromises.push(
|
||||||
relationshipPopulationPromise({
|
relationshipPopulationPromise({
|
||||||
@@ -162,7 +166,7 @@ export const findDistinctOperation = async (
|
|||||||
depth: args.depth,
|
depth: args.depth,
|
||||||
draft: false,
|
draft: false,
|
||||||
fallbackLocale: req.fallbackLocale || null,
|
fallbackLocale: req.fallbackLocale || null,
|
||||||
field: fieldResult.field,
|
field: sanitizedField,
|
||||||
locale: req.locale || null,
|
locale: req.locale || null,
|
||||||
overrideAccess: args.overrideAccess ?? true,
|
overrideAccess: args.overrideAccess ?? true,
|
||||||
parentIsLocalized: false,
|
parentIsLocalized: false,
|
||||||
|
|||||||
@@ -126,6 +126,12 @@ export const getConfig: () => Partial<Config> = () => ({
|
|||||||
relationTo: 'categories',
|
relationTo: 'categories',
|
||||||
name: 'category',
|
name: 'category',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'categories',
|
||||||
|
hasMany: true,
|
||||||
|
name: 'categories',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationTo: 'categories-custom-id',
|
relationTo: 'categories-custom-id',
|
||||||
|
|||||||
@@ -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', () => {
|
describe('Compound Indexes', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await payload.delete({ collection: 'compound-indexes', where: {} })
|
await payload.delete({ collection: 'compound-indexes', where: {} })
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export interface Post {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
category?: (string | null) | Category;
|
category?: (string | null) | Category;
|
||||||
|
categories?: (string | Category)[] | null;
|
||||||
categoryCustomID?: (number | null) | CategoriesCustomId;
|
categoryCustomID?: (number | null) | CategoriesCustomId;
|
||||||
localized?: string | null;
|
localized?: string | null;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
@@ -822,6 +823,7 @@ export interface CategoriesCustomIdSelect<T extends boolean = true> {
|
|||||||
export interface PostsSelect<T extends boolean = true> {
|
export interface PostsSelect<T extends boolean = true> {
|
||||||
title?: T;
|
title?: T;
|
||||||
category?: T;
|
category?: T;
|
||||||
|
categories?: T;
|
||||||
categoryCustomID?: T;
|
categoryCustomID?: T;
|
||||||
localized?: T;
|
localized?: T;
|
||||||
text?: T;
|
text?: T;
|
||||||
|
|||||||
Reference in New Issue
Block a user