diff --git a/packages/payload/src/fields/config/sanitizeJoinField.ts b/packages/payload/src/fields/config/sanitizeJoinField.ts index 566280f2d..faffaa0d9 100644 --- a/packages/payload/src/fields/config/sanitizeJoinField.ts +++ b/packages/payload/src/fields/config/sanitizeJoinField.ts @@ -20,7 +20,7 @@ export const sanitizeJoinField = ({ if (typeof joins === 'undefined') { throw new APIError('Join fields cannot be added to arrays, blocks or globals.') } - if (!field.maxDepth) { + if (typeof field.maxDepth === 'undefined') { field.maxDepth = 1 } const join: SanitizedJoin = { diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 0fb84e76d..61355f8a7 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -1416,6 +1416,13 @@ export type JoinField = { * This does not need to be set and will be overridden by the relationship field's localized property. */ localized?: boolean + /** + * The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. + * + * @see https://payloadcms.com/docs/getting-started/concepts#depth + * + * @default 1 + */ maxDepth?: number /** * A string for the field in the collection being joined to. diff --git a/test/joins/config.ts b/test/joins/config.ts index fd407ee4a..8eafb5b38 100644 --- a/test/joins/config.ts +++ b/test/joins/config.ts @@ -168,6 +168,45 @@ export default buildConfigWithDefaults({ }, ], }, + { + slug: 'depth-joins-1', + fields: [ + { + name: 'rel', + type: 'relationship', + relationTo: 'depth-joins-2', + }, + { + name: 'joins', + type: 'join', + collection: 'depth-joins-3', + on: 'rel', + maxDepth: 2, + }, + ], + }, + { + slug: 'depth-joins-2', + fields: [ + { + name: 'joins', + type: 'join', + collection: 'depth-joins-1', + on: 'rel', + maxDepth: 2, + }, + ], + }, + { + slug: 'depth-joins-3', + fields: [ + { + name: 'rel', + type: 'relationship', + relationTo: 'depth-joins-1', + }, + ], + }, ], localization: { locales: ['en', 'es'], diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index e25b541f4..34a71f374 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -5,7 +5,7 @@ import { getFileByPath } from 'payload' import { fileURLToPath } from 'url' import type { NextRESTClient } from '../helpers/NextRESTClient.js' -import type { Category, Config, Post, Singular } from './payload-types.js' +import type { Category, Config, DepthJoins1, DepthJoins3, Post, Singular } from './payload-types.js' import { devUser } from '../credentials.js' import { idToString } from '../helpers/idToString.js' @@ -984,6 +984,35 @@ describe('Joins Field', () => { expect((data.joins.docs[0] as TypeWithID).id).toBe(doc_2.id) }) + + it('should populate joins on depth 2', async () => { + const depthJoin_2 = await payload.create({ collection: 'depth-joins-2', data: {}, depth: 0 }) + const depthJoin_1 = await payload.create({ + collection: 'depth-joins-1', + data: { rel: depthJoin_2 }, + depth: 0, + }) + + const depthJoin_3 = await payload.create({ + collection: 'depth-joins-3', + data: { rel: depthJoin_1 }, + depth: 0, + }) + + const data = await payload.findByID({ + collection: 'depth-joins-2', + id: depthJoin_2.id, + depth: 2, + }) + + const joinedDoc = data.joins.docs[0] as DepthJoins1 + + expect(joinedDoc.id).toBe(depthJoin_1.id) + + const joinedDoc2 = joinedDoc.joins.docs[0] as DepthJoins3 + + expect(joinedDoc2.id).toBe(depthJoin_3.id) + }) }) async function createPost(overrides?: Partial, locale?: Config['locale']) { diff --git a/test/joins/payload-types.ts b/test/joins/payload-types.ts index 241b99ec4..df65271f2 100644 --- a/test/joins/payload-types.ts +++ b/test/joins/payload-types.ts @@ -25,6 +25,9 @@ export interface Config { 'categories-join-restricted': CategoriesJoinRestricted; 'restricted-posts': RestrictedPost; 'collection-restricted': CollectionRestricted; + 'depth-joins-1': DepthJoins1; + 'depth-joins-2': DepthJoins2; + 'depth-joins-3': DepthJoins3; users: User; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; @@ -66,6 +69,12 @@ export interface Config { 'categories-join-restricted': { collectionRestrictedJoin: 'collection-restricted'; }; + 'depth-joins-1': { + joins: 'depth-joins-3'; + }; + 'depth-joins-2': { + joins: 'depth-joins-1'; + }; }; collectionsSelect: { posts: PostsSelect | PostsSelect; @@ -82,6 +91,9 @@ export interface Config { 'categories-join-restricted': CategoriesJoinRestrictedSelect | CategoriesJoinRestrictedSelect; 'restricted-posts': RestrictedPostsSelect | RestrictedPostsSelect; 'collection-restricted': CollectionRestrictedSelect | CollectionRestrictedSelect; + 'depth-joins-1': DepthJoins1Select | DepthJoins1Select; + 'depth-joins-2': DepthJoins2Select | DepthJoins2Select; + 'depth-joins-3': DepthJoins3Select | DepthJoins3Select; users: UsersSelect | UsersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; @@ -451,6 +463,43 @@ export interface RestrictedPost { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "depth-joins-1". + */ +export interface DepthJoins1 { + id: string; + rel?: (string | null) | DepthJoins2; + joins?: { + docs?: (string | DepthJoins3)[] | null; + hasNextPage?: boolean | null; + } | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "depth-joins-2". + */ +export interface DepthJoins2 { + id: string; + joins?: { + docs?: (string | DepthJoins1)[] | null; + hasNextPage?: boolean | null; + } | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "depth-joins-3". + */ +export interface DepthJoins3 { + id: string; + rel?: (string | null) | DepthJoins1; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". @@ -514,6 +563,18 @@ export interface PayloadLockedDocument { relationTo: 'collection-restricted'; value: string | CollectionRestricted; } | null) + | ({ + relationTo: 'depth-joins-1'; + value: string | DepthJoins1; + } | null) + | ({ + relationTo: 'depth-joins-2'; + value: string | DepthJoins2; + } | null) + | ({ + relationTo: 'depth-joins-3'; + value: string | DepthJoins3; + } | null) | ({ relationTo: 'users'; value: string | User; @@ -761,6 +822,34 @@ export interface CollectionRestrictedSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "depth-joins-1_select". + */ +export interface DepthJoins1Select { + rel?: T; + joins?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "depth-joins-2_select". + */ +export interface DepthJoins2Select { + joins?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "depth-joins-3_select". + */ +export interface DepthJoins3Select { + rel?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users_select".