From 1bf580fac379547f203e0686960081a01fbc6a09 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Tue, 8 Oct 2024 11:40:34 -0400 Subject: [PATCH] feat: join field works with hasMany relationships (#8493) Join field works on relationships and uploads having `hasMany: true` --------- Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com> --- packages/db-sqlite/src/init.ts | 1 + packages/db-sqlite/src/schema/build.ts | 5 +- .../db-sqlite/src/schema/traverseFields.ts | 21 +++- .../drizzle/src/find/buildFindManyArgs.ts | 2 + packages/drizzle/src/find/traverseFields.ts | 119 ++++++++++++++---- packages/drizzle/src/postgres/init.ts | 1 + packages/drizzle/src/postgres/schema/build.ts | 5 +- .../src/postgres/schema/traverseFields.ts | 21 +++- packages/drizzle/src/queries/buildQuery.ts | 1 + .../src/transform/read/traverseFields.ts | 11 +- .../payload/src/collections/config/types.ts | 3 +- .../src/fields/config/sanitizeJoinField.ts | 12 +- packages/payload/src/index.ts | 1 + test/joins/collections/Categories.ts | 6 + test/joins/collections/Posts.ts | 6 + test/joins/int.spec.ts | 48 +++++++ test/joins/payload-types.ts | 5 + test/relationships/int.spec.ts | 47 +++++++ 18 files changed, 275 insertions(+), 40 deletions(-) diff --git a/packages/db-sqlite/src/init.ts b/packages/db-sqlite/src/init.ts index 4a2abfcb33..1a3b40a994 100644 --- a/packages/db-sqlite/src/init.ts +++ b/packages/db-sqlite/src/init.ts @@ -70,6 +70,7 @@ export const init: Init = async function init(this: SQLiteAdapter) { disableNotNull: !!collection?.versions?.drafts, disableUnique: false, fields: collection.fields, + joins: collection.joins, locales, tableName, timestamps: collection.timestamps, diff --git a/packages/db-sqlite/src/schema/build.ts b/packages/db-sqlite/src/schema/build.ts index 6b6a34d400..0883984f0b 100644 --- a/packages/db-sqlite/src/schema/build.ts +++ b/packages/db-sqlite/src/schema/build.ts @@ -7,7 +7,7 @@ import type { SQLiteTableWithColumns, UniqueConstraintBuilder, } from 'drizzle-orm/sqlite-core' -import type { Field } from 'payload' +import type { Field, SanitizedJoins } from 'payload' import { createTableName } from '@payloadcms/drizzle' import { relations, sql } from 'drizzle-orm' @@ -58,6 +58,7 @@ type Args = { disableNotNull: boolean disableUnique: boolean fields: Field[] + joins?: SanitizedJoins locales?: [string, ...string[]] rootRelationships?: Set rootRelationsToBuild?: RelationMap @@ -89,6 +90,7 @@ export const buildTable = ({ disableNotNull, disableUnique = false, fields, + joins, locales, rootRelationships, rootRelationsToBuild, @@ -134,6 +136,7 @@ export const buildTable = ({ disableUnique, fields, indexes, + joins, locales, localesColumns, localesIndexes, diff --git a/packages/db-sqlite/src/schema/traverseFields.ts b/packages/db-sqlite/src/schema/traverseFields.ts index 727abdabbf..1241ea6260 100644 --- a/packages/db-sqlite/src/schema/traverseFields.ts +++ b/packages/db-sqlite/src/schema/traverseFields.ts @@ -1,6 +1,6 @@ import type { Relation } from 'drizzle-orm' import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' -import type { Field, TabAsField } from 'payload' +import type { Field, SanitizedJoins, TabAsField } from 'payload' import { createTableName, @@ -41,6 +41,7 @@ type Args = { fields: (Field | TabAsField)[] forceLocalized?: boolean indexes: Record IndexBuilder> + joins?: SanitizedJoins locales: [string, ...string[]] localesColumns: Record localesIndexes: Record IndexBuilder> @@ -78,6 +79,7 @@ export const traverseFields = ({ fields, forceLocalized, indexes, + joins, locales, localesColumns, localesIndexes, @@ -651,6 +653,7 @@ export const traverseFields = ({ fields: field.fields, forceLocalized, indexes, + joins, locales, localesColumns, localesIndexes, @@ -705,6 +708,7 @@ export const traverseFields = ({ fields: field.fields, forceLocalized: field.localized, indexes, + joins, locales, localesColumns, localesIndexes, @@ -760,6 +764,7 @@ export const traverseFields = ({ fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), forceLocalized, indexes, + joins, locales, localesColumns, localesIndexes, @@ -815,6 +820,7 @@ export const traverseFields = ({ fields: field.fields, forceLocalized, indexes, + joins, locales, localesColumns, localesIndexes, @@ -905,9 +911,18 @@ export const traverseFields = ({ case 'join': { // fieldName could be 'posts' or 'group_posts' - // using on as the key for the relation + // using `on` as the key for the relation const localized = adapter.payload.config.localization && field.localized - const target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}` + const fieldSchemaPath = `${fieldPrefix || ''}${field.name}` + let target: string + const joinConfig = joins[field.collection].find( + ({ schemaPath }) => fieldSchemaPath === schemaPath, + ) + if (joinConfig.targetField.hasMany) { + target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}` + } else { + target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}` + } relationsToBuild.set(fieldName, { type: 'many', // joins are not localized on the parent table diff --git a/packages/drizzle/src/find/buildFindManyArgs.ts b/packages/drizzle/src/find/buildFindManyArgs.ts index 278bfcd780..63239975ca 100644 --- a/packages/drizzle/src/find/buildFindManyArgs.ts +++ b/packages/drizzle/src/find/buildFindManyArgs.ts @@ -36,6 +36,7 @@ export const buildFindManyArgs = ({ tableName, }: BuildFindQueryArgs): Record => { const result: Result = { + extras: {}, with: {}, } @@ -44,6 +45,7 @@ export const buildFindManyArgs = ({ id: false, _parentID: false, }, + extras: {}, with: {}, } diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index 148bf6f87c..e4b4f9506f 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -1,14 +1,15 @@ -import type { DBQueryConfig } from 'drizzle-orm' +import type { LibSQLDatabase } from 'drizzle-orm/libsql' import type { Field, JoinQuery } from 'payload' +import { and, type DBQueryConfig, eq, sql } from 'drizzle-orm' import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared' import toSnakeCase from 'to-snake-case' -import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js' +import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js' import type { Result } from './buildFindManyArgs.js' -import { buildOrderBy } from '../queries/buildOrderBy.js' import buildQuery from '../queries/buildQuery.js' +import { chainMethods } from './chainMethods.js' type TraverseFieldArgs = { _locales: Result @@ -241,24 +242,93 @@ export const traverseFields = ({ // get an additional document and slice it later to determine if there is a next page limit += 1 } + const fields = adapter.payload.collections[field.collection].config.fields + const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection)) const joinTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${ field.localized && adapter.payload.config.localization ? adapter.localesSuffix : '' }` + + if (!adapter.tables[joinTableName][field.on]) { + const db = adapter.drizzle as LibSQLDatabase + const joinTable = `${joinTableName}${adapter.relationshipsSuffix}` + + const joins: BuildQueryJoinAliases = [ + { + type: 'innerJoin', + condition: and( + eq(adapter.tables[joinTable].parent, adapter.tables[joinTableName].id), + eq( + sql.raw(`"${joinTable}"."${topLevelTableName}_id"`), + adapter.tables[currentTableName].id, + ), + ), + table: adapter.tables[joinTable], + }, + ] + + const { orderBy, where: subQueryWhere } = buildQuery({ + adapter, + fields, + joins, + locale, + sort, + tableName: joinCollectionTableName, + where: {}, + }) + + const chainedMethods: ChainedMethods = [] + + joins.forEach(({ type, condition, table }) => { + chainedMethods.push({ + args: [table, condition], + method: type ?? 'leftJoin', + }) + }) + + const subQuery = chainMethods({ + methods: chainedMethods, + query: db + .select({ + id: adapter.tables[joinTableName].id, + }) + .from(adapter.tables[joinTableName]) + .where(subQueryWhere) + .orderBy(orderBy.order(orderBy.column)) + .limit(11), + }) + + const columnName = `${path.replaceAll('.', '_')}${field.name}` + + const extras = field.localized ? _locales.extras : currentArgs.extras + + if (adapter.name === 'sqlite') { + extras[columnName] = sql` + COALESCE(( + SELECT json_group_array("id") + FROM ( + ${subQuery} + ) AS ${sql.raw(`${columnName}_sub`)} + ), '[]') + `.as(columnName) + } else { + extras[columnName] = sql` + COALESCE(( + SELECT json_agg("id") + FROM ( + ${subQuery} + ) AS ${sql.raw(`${columnName}_sub`)} + ), '[]'::json) + `.as(columnName) + } + + break + } + const selectFields = {} - const orderBy = buildOrderBy({ - adapter, - fields, - joins: [], - locale, - selectFields, - sort, - tableName: joinTableName, - }) const withJoin: DBQueryConfig<'many', true, any, any> = { columns: selectFields, - orderBy: () => [orderBy.order(orderBy.column)], } if (limit) { withJoin.limit = limit @@ -269,20 +339,21 @@ export const traverseFields = ({ withJoin.columns._parentID = true } else { withJoin.columns.id = true + withJoin.columns.parent = true } - - if (where) { - const { where: joinWhere } = buildQuery({ - adapter, - fields, - joins, - locale, - sort, - tableName: joinTableName, - where, - }) + const { orderBy, where: joinWhere } = buildQuery({ + adapter, + fields, + joins, + locale, + sort, + tableName: joinTableName, + where, + }) + if (joinWhere) { withJoin.where = () => joinWhere } + withJoin.orderBy = orderBy.order(orderBy.column) currentArgs.with[`${path.replaceAll('.', '_')}${field.name}`] = withJoin break } diff --git a/packages/drizzle/src/postgres/init.ts b/packages/drizzle/src/postgres/init.ts index c76f4d9457..c9ed8996d6 100644 --- a/packages/drizzle/src/postgres/init.ts +++ b/packages/drizzle/src/postgres/init.ts @@ -57,6 +57,7 @@ export const init: Init = async function init(this: BasePostgresAdapter) { disableNotNull: !!collection?.versions?.drafts, disableUnique: false, fields: collection.fields, + joins: collection.joins, tableName, timestamps: collection.timestamps, versions: false, diff --git a/packages/drizzle/src/postgres/schema/build.ts b/packages/drizzle/src/postgres/schema/build.ts index 8aab6c058f..91132443a4 100644 --- a/packages/drizzle/src/postgres/schema/build.ts +++ b/packages/drizzle/src/postgres/schema/build.ts @@ -5,7 +5,7 @@ import type { PgColumnBuilder, PgTableWithColumns, } from 'drizzle-orm/pg-core' -import type { Field } from 'payload' +import type { Field, SanitizedJoins } from 'payload' import { relations } from 'drizzle-orm' import { @@ -47,6 +47,7 @@ type Args = { disableNotNull: boolean disableUnique: boolean fields: Field[] + joins?: SanitizedJoins rootRelationships?: Set rootRelationsToBuild?: RelationMap rootTableIDColType?: string @@ -77,6 +78,7 @@ export const buildTable = ({ disableNotNull, disableUnique = false, fields, + joins, rootRelationships, rootRelationsToBuild, rootTableIDColType, @@ -121,6 +123,7 @@ export const buildTable = ({ disableUnique, fields, indexes, + joins, localesColumns, localesIndexes, newTableName: tableName, diff --git a/packages/drizzle/src/postgres/schema/traverseFields.ts b/packages/drizzle/src/postgres/schema/traverseFields.ts index 40c942260a..777e428899 100644 --- a/packages/drizzle/src/postgres/schema/traverseFields.ts +++ b/packages/drizzle/src/postgres/schema/traverseFields.ts @@ -1,6 +1,6 @@ import type { Relation } from 'drizzle-orm' import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core' -import type { Field, TabAsField } from 'payload' +import type { Field, SanitizedJoins, TabAsField } from 'payload' import { relations } from 'drizzle-orm' import { @@ -48,6 +48,7 @@ type Args = { fields: (Field | TabAsField)[] forceLocalized?: boolean indexes: Record IndexBuilder> + joins?: SanitizedJoins localesColumns: Record localesIndexes: Record IndexBuilder> newTableName: string @@ -84,6 +85,7 @@ export const traverseFields = ({ fields, forceLocalized, indexes, + joins, localesColumns, localesIndexes, newTableName, @@ -658,6 +660,7 @@ export const traverseFields = ({ fields: field.fields, forceLocalized, indexes, + joins, localesColumns, localesIndexes, newTableName, @@ -711,6 +714,7 @@ export const traverseFields = ({ fields: field.fields, forceLocalized: field.localized, indexes, + joins, localesColumns, localesIndexes, newTableName: `${parentTableName}_${columnName}`, @@ -765,6 +769,7 @@ export const traverseFields = ({ fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), forceLocalized, indexes, + joins, localesColumns, localesIndexes, newTableName, @@ -819,6 +824,7 @@ export const traverseFields = ({ fields: field.fields, forceLocalized, indexes, + joins, localesColumns, localesIndexes, newTableName, @@ -908,9 +914,18 @@ export const traverseFields = ({ case 'join': { // fieldName could be 'posts' or 'group_posts' - // using on as the key for the relation + // using `on` as the key for the relation const localized = adapter.payload.config.localization && field.localized - const target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}` + const fieldSchemaPath = `${fieldPrefix || ''}${field.name}` + let target: string + const joinConfig = joins[field.collection].find( + ({ schemaPath }) => fieldSchemaPath === schemaPath, + ) + if (joinConfig.targetField.hasMany) { + target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}` + } else { + target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}` + } relationsToBuild.set(fieldName, { type: 'many', // joins are not localized on the parent table diff --git a/packages/drizzle/src/queries/buildQuery.ts b/packages/drizzle/src/queries/buildQuery.ts index dff3f9fc43..9873b399a3 100644 --- a/packages/drizzle/src/queries/buildQuery.ts +++ b/packages/drizzle/src/queries/buildQuery.ts @@ -10,6 +10,7 @@ import { parseParams } from './parseParams.js' export type BuildQueryJoinAliases = { condition: SQL table: GenericTable | PgTableWithColumns + type?: 'innerJoin' | 'leftJoin' | 'rightJoin' }[] type BuildQueryArgs = { diff --git a/packages/drizzle/src/transform/read/traverseFields.ts b/packages/drizzle/src/transform/read/traverseFields.ts index 42cc7889d7..b680e6e73b 100644 --- a/packages/drizzle/src/transform/read/traverseFields.ts +++ b/packages/drizzle/src/transform/read/traverseFields.ts @@ -137,7 +137,7 @@ export const traverseFields = >({ } const fieldName = `${fieldPrefix || ''}${field.name}` - const fieldData = table[fieldName] + let fieldData = table[fieldName] const localizedFieldData = {} const valuesToTransform: { ref: Record @@ -422,6 +422,11 @@ export const traverseFields = >({ if (field.type === 'join') { const { limit = 10 } = joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {} + // raw hasMany results from SQLite + if (typeof fieldData === 'string') { + fieldData = JSON.parse(fieldData) + } + let fieldResult: | { docs: unknown[]; hasNextPage: boolean } | Record @@ -447,7 +452,9 @@ export const traverseFields = >({ } else { const hasNextPage = limit !== 0 && fieldData.length > limit fieldResult = { - docs: hasNextPage ? fieldData.slice(0, limit) : fieldData, + docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map((objOrID) => ({ + id: typeof objOrID === 'object' ? objOrID.id : objOrID, + })), hasNextPage, } } diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 14e5765707..5fe12a4f32 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -31,7 +31,7 @@ import type { StaticLabel, } from '../../config/types.js' import type { DBIdentifierName } from '../../database/types.js' -import type { Field, JoinField } from '../../fields/config/types.js' +import type { Field, JoinField, RelationshipField, UploadField } from '../../fields/config/types.js' import type { CollectionSlug, JsonObject, @@ -485,6 +485,7 @@ export type SanitizedJoin = { * The schemaPath of the join field in dot notation */ schemaPath: string + targetField: RelationshipField | UploadField } export type SanitizedJoins = { diff --git a/packages/payload/src/fields/config/sanitizeJoinField.ts b/packages/payload/src/fields/config/sanitizeJoinField.ts index 632fbd09a5..9da7bc372b 100644 --- a/packages/payload/src/fields/config/sanitizeJoinField.ts +++ b/packages/payload/src/fields/config/sanitizeJoinField.ts @@ -1,4 +1,4 @@ -import type { SanitizedJoins } from '../../collections/config/types.js' +import type { SanitizedJoin, SanitizedJoins } from '../../collections/config/types.js' import type { Config } from '../../config/types.js' import type { JoinField, RelationshipField, UploadField } from './types.js' @@ -23,9 +23,10 @@ export const sanitizeJoinField = ({ if (!field.maxDepth) { field.maxDepth = 1 } - const join = { + const join: SanitizedJoin = { field, schemaPath: `${schemaPath || ''}${schemaPath ? '.' : ''}${field.name}`, + targetField: undefined, } const joinCollection = config.collections.find( (collection) => collection.slug === field.collection, @@ -73,11 +74,12 @@ export const sanitizeJoinField = ({ if (!joinRelationship) { throw new InvalidFieldJoin(join.field) } - - if (joinRelationship.hasMany) { - throw new APIError('Join fields cannot be used with hasMany relationships.') + if (Array.isArray(joinRelationship.relationTo)) { + throw new APIError('Join fields cannot be used with polymorphic relationships.') } + join.targetField = joinRelationship + // override the join field localized property to use whatever the relationship field has field.localized = joinRelationship.localized diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 8907386b50..ece0e1de7e 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -725,6 +725,7 @@ export type { RequiredDataFromCollection, RequiredDataFromCollectionSlug, SanitizedCollectionConfig, + SanitizedJoins, TypeWithID, TypeWithTimestamps, } from './collections/config/types.js' diff --git a/test/joins/collections/Categories.ts b/test/joins/collections/Categories.ts index cb353f943f..5780405ef2 100644 --- a/test/joins/collections/Categories.ts +++ b/test/joins/collections/Categories.ts @@ -49,6 +49,12 @@ export const Categories: CollectionConfig = { collection: postsSlug, on: 'category', }, + { + name: 'hasManyPosts', + type: 'join', + collection: postsSlug, + on: 'categories', + }, { name: 'group', type: 'group', diff --git a/test/joins/collections/Posts.ts b/test/joins/collections/Posts.ts index a78ab3ea9c..edfaf546e0 100644 --- a/test/joins/collections/Posts.ts +++ b/test/joins/collections/Posts.ts @@ -23,6 +23,12 @@ export const Posts: CollectionConfig = { type: 'relationship', relationTo: categoriesSlug, }, + { + name: 'categories', + type: 'relationship', + relationTo: categoriesSlug, + hasMany: true, + }, { name: 'group', type: 'group', diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index e6da3572c8..ade2ec6a06 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -23,6 +23,7 @@ const { email, password } = devUser describe('Joins Field', () => { let category: Category + let otherCategory: Category let categoryID // --__--__--__--__--__--__--__--__--__ // Boilerplate test setup/teardown @@ -49,6 +50,14 @@ describe('Joins Field', () => { }, }) + otherCategory = await payload.create({ + collection: categoriesSlug, + data: { + name: 'otherCategory', + group: {}, + }, + }) + // create an upload const imageFilePath = path.resolve(dirname, './image.png') const imageFile = await getFileByPath(imageFilePath) @@ -62,10 +71,15 @@ describe('Joins Field', () => { categoryID = idToString(category.id, payload) for (let i = 0; i < 15; i++) { + let categories = [category.id] + if (i % 2 === 0) { + categories = [category.id, otherCategory.id] + } await createPost({ title: `test ${i}`, category: category.id, upload: uploadedImage, + categories, group: { category: category.id, camelCaseCategory: category.id, @@ -90,6 +104,15 @@ describe('Joins Field', () => { }, collection: 'categories', }) + // const sortCategoryWithPosts = await payload.findByID({ + // id: category.id, + // joins: { + // 'group.relatedPosts': { + // sort: 'title', + // }, + // }, + // collection: 'categories', + // }) expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10) expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id') @@ -164,6 +187,31 @@ describe('Joins Field', () => { expect(categoryWithPosts.group.relatedPosts.docs[0].title).toBe('test 14') }) + it('should populate joins using find with hasMany relationships', async () => { + const result = await payload.find({ + collection: 'categories', + where: { + id: { equals: category.id }, + }, + }) + const otherResult = await payload.find({ + collection: 'categories', + where: { + id: { equals: otherCategory.id }, + }, + }) + + const [categoryWithPosts] = result.docs + const [otherCategoryWithPosts] = otherResult.docs + + expect(categoryWithPosts.hasManyPosts.docs).toHaveLength(10) + expect(categoryWithPosts.hasManyPosts.docs[0]).toHaveProperty('title') + expect(categoryWithPosts.hasManyPosts.docs[0].title).toBe('test 14') + expect(otherCategoryWithPosts.hasManyPosts.docs).toHaveLength(8) + expect(otherCategoryWithPosts.hasManyPosts.docs[0]).toHaveProperty('title') + expect(otherCategoryWithPosts.hasManyPosts.docs[0].title).toBe('test 14') + }) + it('should not error when deleting documents with joins', async () => { const category = await payload.create({ collection: 'categories', diff --git a/test/joins/payload-types.ts b/test/joins/payload-types.ts index a9f83397cd..ae89d82ef8 100644 --- a/test/joins/payload-types.ts +++ b/test/joins/payload-types.ts @@ -57,6 +57,7 @@ export interface Post { title?: string | null; upload?: (string | null) | Upload; category?: (string | null) | Category; + categories?: (string | Category)[] | null; group?: { category?: (string | null) | Category; camelCaseCategory?: (string | null) | Category; @@ -97,6 +98,10 @@ export interface Category { docs?: (string | Post)[] | null; hasNextPage?: boolean | null; } | null; + hasManyPosts?: { + docs?: (string | Post)[] | null; + hasNextPage?: boolean | null; + } | null; group?: { relatedPosts?: { docs?: (string | Post)[] | null; diff --git a/test/relationships/int.spec.ts b/test/relationships/int.spec.ts index 3386ed978c..7b889ca190 100644 --- a/test/relationships/int.spec.ts +++ b/test/relationships/int.spec.ts @@ -17,6 +17,7 @@ import type { } from './payload-types.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' +import { isMongoose } from '../helpers/isMongoose.js' import { chainedRelSlug, customIdNumberSlug, @@ -397,6 +398,52 @@ describe('Relationships', () => { expect(query2.totalDocs).toStrictEqual(2) }) + it('should sort by a property of a hasMany relationship', async () => { + // no support for sort by relation in mongodb + if (isMongoose(payload)) { + return + } + + const movie1 = await payload.create({ + collection: 'movies', + data: { + name: 'Pulp Fiction', + }, + }) + + const movie2 = await payload.create({ + collection: 'movies', + data: { + name: 'Inception', + }, + }) + + await payload.delete({ collection: 'directors', where: {} }) + + const director1 = await payload.create({ + collection: 'directors', + data: { + name: 'Quentin Tarantino', + movies: [movie1.id], + }, + }) + const director2 = await payload.create({ + collection: 'directors', + data: { + name: 'Christopher Nolan', + movies: [movie2.id], + }, + }) + + const result = await payload.find({ + collection: 'directors', + depth: 0, + sort: '-movies.name', + }) + + expect(result.docs[0].id).toStrictEqual(director1.id) + }) + it('should query using "in" by hasMany relationship field', async () => { const tree1 = await payload.create({ collection: treeSlug,