From 462c077d7fd1f59ec7e8f33e11430522a1ca4eb7 Mon Sep 17 00:00:00 2001 From: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:34:54 -0500 Subject: [PATCH 1/3] fix(drizzle): convert null to undefined for JSON fields in SQL databases --- .../src/transform/read/traverseFields.ts | 9 +++++ test/query-presets/int.spec.ts | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/drizzle/src/transform/read/traverseFields.ts b/packages/drizzle/src/transform/read/traverseFields.ts index 5f6093d72b1..81656042304 100644 --- a/packages/drizzle/src/transform/read/traverseFields.ts +++ b/packages/drizzle/src/transform/read/traverseFields.ts @@ -669,6 +669,15 @@ export const traverseFields = >({ return } + case 'json': + case 'richText': { + if (fieldData === null) { + val = undefined + } + + break + } + case 'number': { if (typeof fieldData === 'string') { val = Number.parseFloat(fieldData) diff --git a/test/query-presets/int.spec.ts b/test/query-presets/int.spec.ts index c157f70f513..7cac690894e 100644 --- a/test/query-presets/int.spec.ts +++ b/test/query-presets/int.spec.ts @@ -803,5 +803,43 @@ describe('Query Presets', () => { ], }) }) + + it('should handle empty where and columns fields', async () => { + const result = await payload.create({ + collection: queryPresetsCollectionSlug, + user: adminUser, + overrideAccess: false, + data: { + title: 'Empty Where and Columns', + // Not including where or columns at all + access: { + read: { + constraint: 'everyone', + }, + update: { + constraint: 'everyone', + }, + delete: { + constraint: 'everyone', + }, + }, + relatedCollection: 'pages', + }, + }) + + expect(result.where).toBeUndefined() + expect(result.columns).toBeUndefined() + + const fetched = await payload.findByID({ + collection: queryPresetsCollectionSlug, + depth: 0, + user: adminUser, + overrideAccess: false, + id: result.id, + }) + + expect(fetched.where).toBeUndefined() + expect(fetched.columns).toBeUndefined() + }) }) }) From aa2c4cf90e25438d0f2f68592f3b6145957ff2ad Mon Sep 17 00:00:00 2001 From: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:38:21 -0500 Subject: [PATCH 2/3] fix(ui): adds null checks before Object.keys() calls --- .../src/transform/read/traverseFields.ts | 9 --- .../QueryPresets/cells/WhereCell/index.tsx | 4 +- .../QueryPresets/fields/WhereField/index.tsx | 6 +- .../src/providers/ListQuery/sanitizeQuery.ts | 7 ++- test/query-presets/e2e.spec.ts | 59 +++++++++++++++++++ test/query-presets/int.spec.ts | 8 +-- test/query-presets/payload-types.ts | 45 ++++++++++---- 7 files changed, 108 insertions(+), 30 deletions(-) diff --git a/packages/drizzle/src/transform/read/traverseFields.ts b/packages/drizzle/src/transform/read/traverseFields.ts index 81656042304..5f6093d72b1 100644 --- a/packages/drizzle/src/transform/read/traverseFields.ts +++ b/packages/drizzle/src/transform/read/traverseFields.ts @@ -669,15 +669,6 @@ export const traverseFields = >({ return } - case 'json': - case 'richText': { - if (fieldData === null) { - val = undefined - } - - break - } - case 'number': { if (typeof fieldData === 'string') { val = Number.parseFloat(fieldData) diff --git a/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx b/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx index d06f81d9c95..ce179a8e2e9 100644 --- a/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx +++ b/packages/ui/src/elements/QueryPresets/cells/WhereCell/index.tsx @@ -9,13 +9,13 @@ const transformWhereToNaturalLanguage = (where: Where): string => { const orQuery = where.or[0] const andQuery = orQuery?.and?.[0] - if (!andQuery) { + if (!andQuery || typeof andQuery !== 'object') { return 'No where query' } const key = Object.keys(andQuery)[0] - if (!andQuery[key]) { + if (!key || !andQuery[key] || typeof andQuery[key] !== 'object') { return 'No where query' } diff --git a/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx b/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx index 0c3bb2a873e..a8b1cc7cf23 100644 --- a/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx +++ b/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx @@ -23,9 +23,13 @@ const transformWhereToNaturalLanguage = ( } const renderCondition = (condition: any): React.ReactNode => { + if (!condition || typeof condition !== 'object') { + return 'No where query' + } + const key = Object.keys(condition)[0] - if (!condition[key]) { + if (!key || !condition[key] || typeof condition[key] !== 'object') { return 'No where query' } diff --git a/packages/ui/src/providers/ListQuery/sanitizeQuery.ts b/packages/ui/src/providers/ListQuery/sanitizeQuery.ts index 551ddf459e4..fcaa4fc1d3d 100644 --- a/packages/ui/src/providers/ListQuery/sanitizeQuery.ts +++ b/packages/ui/src/providers/ListQuery/sanitizeQuery.ts @@ -16,7 +16,12 @@ export const sanitizeQuery = (toSanitize: ListQuery): ListQuery => { delete sanitized[key] } - if (key === 'where' && typeof value === 'object' && !Object.keys(value as Where).length) { + if ( + key === 'where' && + typeof value === 'object' && + value !== null && + !Object.keys(value as Where).length + ) { delete sanitized[key] } diff --git a/test/query-presets/e2e.spec.ts b/test/query-presets/e2e.spec.ts index 082650906b0..77535c3e412 100644 --- a/test/query-presets/e2e.spec.ts +++ b/test/query-presets/e2e.spec.ts @@ -394,6 +394,65 @@ describe('Query Presets', () => { ).toBeVisible() }) + test('can create and view preset with no filters or columns', async () => { + await page.goto(pagesUrl.list) + + const presetTitle = 'Empty Preset' + + // Create a new preset without setting any filters or columns + await page.locator('#create-new-preset').click() + const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') + await expect(modal).toBeVisible() + await modal.locator('input[name="title"]').fill(presetTitle) + + const currentURL = page.url() + await saveDocAndAssert(page) + await expect(modal).toBeHidden() + + await page.waitForURL(() => page.url() !== currentURL) + + await expect( + page.locator('button#select-preset', { + hasText: exactText(presetTitle), + }), + ).toBeVisible() + + // Open the edit modal to verify where/columns fields handle null values + await page.locator('#edit-preset').click() + const editModal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') + await expect(editModal).toBeVisible() + + // Verify the Where field displays "No where query" instead of crashing + const whereFieldContent = editModal.locator('.query-preset-where-field .value-wrapper') + await expect(whereFieldContent).toBeVisible() + await expect(whereFieldContent).toContainText('No where query') + + // Verify the Columns field displays "No columns selected" instead of crashing + const columnsFieldContent = editModal.locator('.query-preset-columns-field .value-wrapper') + await expect(columnsFieldContent).toBeVisible() + await expect(columnsFieldContent).toContainText('No columns selected') + + await editModal.locator('button.doc-drawer__header-close').click() + await expect(editModal).toBeHidden() + + await openQueryPresetDrawer({ page }) + const drawer = page.locator('[id^=list-drawer_0_]') + await expect(drawer).toBeVisible() + + const presetRow = drawer.locator('tbody tr', { + has: page.locator(`button:has-text("${presetTitle}")`), + }) + + await expect(presetRow).toBeVisible() + + // Column order: title (0), isShared (1), access (2), where (3), columns (4) + const whereCell = presetRow.locator('td').nth(3) + await expect(whereCell).toContainText('No where query') + + const columnsCell = presetRow.locator('td').nth(4) + await expect(columnsCell).toContainText('No columns selected') + }) + test('only shows query presets related to the underlying collection', async () => { // no results on `posts` collection const postsURL = new AdminUrlUtil(serverURL, 'posts') diff --git a/test/query-presets/int.spec.ts b/test/query-presets/int.spec.ts index 7cac690894e..a62d08febbc 100644 --- a/test/query-presets/int.spec.ts +++ b/test/query-presets/int.spec.ts @@ -827,8 +827,8 @@ describe('Query Presets', () => { }, }) - expect(result.where).toBeUndefined() - expect(result.columns).toBeUndefined() + expect(result.where == null).toBe(true) + expect(result.columns == null).toBe(true) const fetched = await payload.findByID({ collection: queryPresetsCollectionSlug, @@ -838,8 +838,8 @@ describe('Query Presets', () => { id: result.id, }) - expect(fetched.where).toBeUndefined() - expect(fetched.columns).toBeUndefined() + expect(fetched.where == null).toBe(true) + expect(fetched.columns == null).toBe(true) }) }) }) diff --git a/test/query-presets/payload-types.ts b/test/query-presets/payload-types.ts index 53bc703e785..bf4fc79de79 100644 --- a/test/query-presets/payload-types.ts +++ b/test/query-presets/payload-types.ts @@ -70,6 +70,7 @@ export interface Config { pages: Page; posts: Post; users: User; + 'payload-kv': PayloadKv; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; @@ -80,6 +81,7 @@ export interface Config { pages: PagesSelect | PagesSelect; posts: PostsSelect | PostsSelect; users: UsersSelect | UsersSelect; + 'payload-kv': PayloadKvSelect | PayloadKvSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; @@ -88,6 +90,7 @@ export interface Config { db: { defaultIDType: string; }; + fallbackLocale: null; globals: {}; globalsSelect: {}; locale: null; @@ -164,25 +167,33 @@ export interface User { | null; password?: string | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-kv". + */ +export interface PayloadKv { + id: string; + key: string; + data: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { id: string; - document?: - | ({ - relationTo: 'pages'; - value: string | Page; - } | null) - | ({ - relationTo: 'posts'; - value: string | Post; - } | null) - | ({ - relationTo: 'users'; - value: string | User; - } | null); + document?: { + relationTo: 'users'; + value: string | User; + } | null; globalSlug?: string | null; user: { relationTo: 'users'; @@ -318,6 +329,14 @@ export interface UsersSelect { expiresAt?: T; }; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-kv_select". + */ +export interface PayloadKvSelect { + key?: T; + data?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents_select". From 205b9b4218afac00d4e80d8e1d72dc7662224489 Mon Sep 17 00:00:00 2001 From: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:17:02 -0500 Subject: [PATCH 3/3] test: update flaky test --- test/query-presets/e2e.spec.ts | 118 ++++++++++++++++----------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/test/query-presets/e2e.spec.ts b/test/query-presets/e2e.spec.ts index 77535c3e412..f4f44b923bc 100644 --- a/test/query-presets/e2e.spec.ts +++ b/test/query-presets/e2e.spec.ts @@ -149,6 +149,65 @@ describe('Query Presets', () => { } }) + test('can create and view preset with no filters or columns', async () => { + await page.goto(pagesUrl.list) + + const presetTitle = 'Empty Preset' + + // Create a new preset without setting any filters or columns + await page.locator('#create-new-preset').click() + const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') + await expect(modal).toBeVisible() + await modal.locator('input[name="title"]').fill(presetTitle) + + const currentURL = page.url() + await saveDocAndAssert(page) + await expect(modal).toBeHidden() + + await page.waitForURL(() => page.url() !== currentURL) + + await expect( + page.locator('button#select-preset', { + hasText: exactText(presetTitle), + }), + ).toBeVisible() + + // Open the edit modal to verify where/columns fields handle null values + await page.locator('#edit-preset').click() + const editModal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') + await expect(editModal).toBeVisible() + + // Verify the Where field displays "No where query" instead of crashing + const whereFieldContent = editModal.locator('.query-preset-where-field .value-wrapper') + await expect(whereFieldContent).toBeVisible() + await expect(whereFieldContent).toContainText('No where query') + + // Verify the Columns field displays "No columns selected" instead of crashing + const columnsFieldContent = editModal.locator('.query-preset-columns-field .value-wrapper') + await expect(columnsFieldContent).toBeVisible() + await expect(columnsFieldContent).toContainText('No columns selected') + + await editModal.locator('button.doc-drawer__header-close').click() + await expect(editModal).toBeHidden() + + await openQueryPresetDrawer({ page }) + const drawer = page.locator('[id^=list-drawer_0_]') + await expect(drawer).toBeVisible() + + const presetRow = drawer.locator('tbody tr', { + has: page.locator(`button:has-text("${presetTitle}")`), + }) + + await expect(presetRow).toBeVisible() + + // Column order: title (0), isShared (1), access (2), where (3), columns (4) + const whereCell = presetRow.locator('td').nth(3) + await expect(whereCell).toContainText('No where query') + + const columnsCell = presetRow.locator('td').nth(4) + await expect(columnsCell).toContainText('No columns selected') + }) + test('should select preset and apply filters', async () => { await page.goto(pagesUrl.list) @@ -394,65 +453,6 @@ describe('Query Presets', () => { ).toBeVisible() }) - test('can create and view preset with no filters or columns', async () => { - await page.goto(pagesUrl.list) - - const presetTitle = 'Empty Preset' - - // Create a new preset without setting any filters or columns - await page.locator('#create-new-preset').click() - const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') - await expect(modal).toBeVisible() - await modal.locator('input[name="title"]').fill(presetTitle) - - const currentURL = page.url() - await saveDocAndAssert(page) - await expect(modal).toBeHidden() - - await page.waitForURL(() => page.url() !== currentURL) - - await expect( - page.locator('button#select-preset', { - hasText: exactText(presetTitle), - }), - ).toBeVisible() - - // Open the edit modal to verify where/columns fields handle null values - await page.locator('#edit-preset').click() - const editModal = page.locator('[id^=doc-drawer_payload-query-presets_0_]') - await expect(editModal).toBeVisible() - - // Verify the Where field displays "No where query" instead of crashing - const whereFieldContent = editModal.locator('.query-preset-where-field .value-wrapper') - await expect(whereFieldContent).toBeVisible() - await expect(whereFieldContent).toContainText('No where query') - - // Verify the Columns field displays "No columns selected" instead of crashing - const columnsFieldContent = editModal.locator('.query-preset-columns-field .value-wrapper') - await expect(columnsFieldContent).toBeVisible() - await expect(columnsFieldContent).toContainText('No columns selected') - - await editModal.locator('button.doc-drawer__header-close').click() - await expect(editModal).toBeHidden() - - await openQueryPresetDrawer({ page }) - const drawer = page.locator('[id^=list-drawer_0_]') - await expect(drawer).toBeVisible() - - const presetRow = drawer.locator('tbody tr', { - has: page.locator(`button:has-text("${presetTitle}")`), - }) - - await expect(presetRow).toBeVisible() - - // Column order: title (0), isShared (1), access (2), where (3), columns (4) - const whereCell = presetRow.locator('td').nth(3) - await expect(whereCell).toContainText('No where query') - - const columnsCell = presetRow.locator('td').nth(4) - await expect(columnsCell).toContainText('No columns selected') - }) - test('only shows query presets related to the underlying collection', async () => { // no results on `posts` collection const postsURL = new AdminUrlUtil(serverURL, 'posts')