From 3175541c8023f0f169aded277679bd1c202ef17e Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:06:05 +0200 Subject: [PATCH] fix: `select` with unnamed tabs (#8966) ### What? Fixes `select` handling for properties inside of unnamed tabs using the mongodb adapter. Additionally, refactors `traverseFields` in drizzle to reuse logic from groups / collapsible or rows if unnamed. ### Why? `select` must work for any fields. ### How? Fixes the `'tab'` case in `buildProjectionFromSelect` to handle when the field is an unnamed tab. Adds extra tests for named tabs / unnamed. --- .../utilities/buildProjectionFromSelect.ts | 17 +++- packages/drizzle/src/find/traverseFields.ts | 78 ++++++++----------- test/select/collections/Posts/index.ts | 31 ++++++++ test/select/int.spec.ts | 57 +++++++++++++- test/select/payload-types.ts | 20 ++++- 5 files changed, 151 insertions(+), 52 deletions(-) diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts index 1bb2bb7fea..3df792b066 100644 --- a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts +++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts @@ -6,7 +6,7 @@ import { type SelectType, type TabAsField, } from 'payload' -import { fieldAffectsData, getSelectMode } from 'payload/shared' +import { fieldAffectsData, getSelectMode, tabHasName } from 'payload/shared' import type { MongooseAdapter } from '../index.js' @@ -131,9 +131,17 @@ const traverseFields = ({ case 'group': case 'tab': - case 'array': + case 'array': { + let fieldSelect: SelectType + + if (field.type === 'tab' && !tabHasName(field)) { + fieldSelect = select + } else { + fieldSelect = select[field.name] as SelectType + } + if (field.type === 'array' && selectMode === 'include') { - select[field.name]['id'] = true + fieldSelect['id'] = true } traverseFields({ @@ -141,12 +149,13 @@ const traverseFields = ({ databaseSchemaPath: fieldDatabaseSchemaPath, fields: field.fields, projection, - select: select[field.name] as SelectType, + select: fieldSelect, selectMode, withinLocalizedField: fieldWithinLocalizedField, }) break + } case 'blocks': { const blocksSelect = select[field.name] as SelectType diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index 7badc56871..1b3ea8b47c 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -1,5 +1,5 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql' -import type { Field, JoinQuery, SelectMode, SelectType } from 'payload' +import type { Field, JoinQuery, SelectMode, SelectType, TabAsField } from 'payload' import { and, eq, sql } from 'drizzle-orm' import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared' @@ -17,7 +17,7 @@ type TraverseFieldArgs = { currentArgs: Result currentTableName: string depth?: number - fields: Field[] + fields: (Field | TabAsField)[] joinQuery: JoinQuery joins?: BuildQueryJoinAliases locale?: string @@ -77,7 +77,11 @@ export const traverseFields = ({ } } - if (field.type === 'collapsible' || field.type === 'row') { + if ( + field.type === 'collapsible' || + field.type === 'row' || + (field.type === 'tab' && !tabHasName(field)) + ) { traverseFields({ _locales, adapter, @@ -100,43 +104,24 @@ export const traverseFields = ({ } if (field.type === 'tabs') { - field.tabs.forEach((tab) => { - const tabPath = tabHasName(tab) ? `${path}${tab.name}_` : path - const tabTablePath = tabHasName(tab) ? `${tablePath}${toSnakeCase(tab.name)}_` : tablePath - - const tabSelect = tabHasName(tab) ? select?.[tab.name] : select - - if (tabSelect === false) { - return - } - - let tabSelectAllOnCurrentLevel = selectAllOnCurrentLevel - - if (tabHasName(tab) && select && !tabSelectAllOnCurrentLevel) { - tabSelectAllOnCurrentLevel = - select[tab.name] === true || - (selectMode === 'exclude' && typeof select[tab.name] === 'undefined') - } - - traverseFields({ - _locales, - adapter, - currentArgs, - currentTableName, - depth, - fields: tab.fields, - joinQuery, - joins, - path: tabPath, - select: typeof tabSelect === 'object' ? tabSelect : undefined, - selectAllOnCurrentLevel: tabSelectAllOnCurrentLevel, - selectMode, - tablePath: tabTablePath, - topLevelArgs, - topLevelTableName, - versions, - withTabledFields, - }) + traverseFields({ + _locales, + adapter, + currentArgs, + currentTableName, + depth, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + joinQuery, + joins, + path, + select, + selectAllOnCurrentLevel, + selectMode, + tablePath, + topLevelArgs, + topLevelTableName, + versions, + withTabledFields, }) return @@ -369,10 +354,11 @@ export const traverseFields = ({ break } - case 'group': { - const groupSelect = select?.[field.name] + case 'group': + case 'tab': { + const fieldSelect = select?.[field.name] - if (groupSelect === false) { + if (fieldSelect === false) { break } @@ -386,11 +372,11 @@ export const traverseFields = ({ joinQuery, joins, path: `${path}${field.name}_`, - select: typeof groupSelect === 'object' ? groupSelect : undefined, + select: typeof fieldSelect === 'object' ? fieldSelect : undefined, selectAllOnCurrentLevel: selectAllOnCurrentLevel || - groupSelect === true || - (selectMode === 'exclude' && typeof groupSelect === 'undefined'), + fieldSelect === true || + (selectMode === 'exclude' && typeof fieldSelect === 'undefined'), selectMode, tablePath: `${tablePath}${toSnakeCase(field.name)}_`, topLevelArgs, diff --git a/test/select/collections/Posts/index.ts b/test/select/collections/Posts/index.ts index d0e0c3f3e7..41288d52dd 100644 --- a/test/select/collections/Posts/index.ts +++ b/test/select/collections/Posts/index.ts @@ -74,5 +74,36 @@ export const PostsCollection: CollectionConfig = { }, ], }, + { + type: 'tabs', + tabs: [ + { + name: 'tab', + fields: [ + { + type: 'text', + name: 'text', + }, + { + type: 'number', + name: 'number', + }, + ], + }, + { + label: 'Tab Unnamed', + fields: [ + { + type: 'text', + name: 'unnamedTabText', + }, + { + type: 'number', + name: 'unnamedTabNumber', + }, + ], + }, + ], + }, ], } diff --git a/test/select/int.spec.ts b/test/select/int.spec.ts index d9ef79e50c..2406ae4396 100644 --- a/test/select/int.spec.ts +++ b/test/select/int.spec.ts @@ -129,6 +129,55 @@ describe('Select', () => { }) }) + it('should select all the fields inside of named tab', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + tab: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + tab: post.tab, + }) + }) + + it('should select text field inside of named tab', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + tab: { + text: true, + }, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + tab: { + text: post.tab.text, + }, + }) + }) + + it('should select text field inside of unnamed tab', async () => { + const res = await payload.findByID({ + collection: 'posts', + id: postId, + select: { + unnamedTabText: true, + }, + }) + + expect(res).toStrictEqual({ + id: postId, + unnamedTabText: post.unnamedTabText, + }) + }) + it('should select id as default from array', async () => { const res = await payload.findByID({ collection: 'posts', @@ -1662,7 +1711,7 @@ describe('Select', () => { expect(richTextSlateRel.value).toStrictEqual(expectedHomePage) }) - it('REST API - should populate with the defaultPopulate select shape', async () => { + it('rEST API - should populate with the defaultPopulate select shape', async () => { const restResult = await ( await restClient.GET(`/pages/${aboutPage.id}`, { query: { depth: 1 } }) ).json() @@ -1728,6 +1777,12 @@ function createPost() { number: 1, }, ], + tab: { + text: 'text', + number: 1, + }, + unnamedTabNumber: 2, + unnamedTabText: 'text2', }, }) } diff --git a/test/select/payload-types.ts b/test/select/payload-types.ts index 9a3bee9d14..3a049c4519 100644 --- a/test/select/payload-types.ts +++ b/test/select/payload-types.ts @@ -45,6 +45,10 @@ export interface Config { user: User & { collection: 'users'; }; +jobs?: { + tasks: unknown; + workflows?: unknown; + }; } export interface UserAuthOperations { forgotPassword: { @@ -101,6 +105,12 @@ export interface Post { } )[] | null; + tab?: { + text?: string | null; + number?: number | null; + }; + unnamedTabText?: string | null; + unnamedTabNumber?: number | null; updatedAt: string; createdAt: string; } @@ -424,6 +434,14 @@ export interface PostsSelect { blockName?: T; }; }; + tab?: + | T + | { + text?: T; + number?: T; + }; + unnamedTabText?: T; + unnamedTabNumber?: T; updatedAt?: T; createdAt?: T; } @@ -691,4 +709,4 @@ export interface Auth { declare module 'payload' { // @ts-ignore export interface GeneratedTypes extends Config {} -} \ No newline at end of file +}