From d0f7677d5ff2e0109fc348260d87e2606fdbd293 Mon Sep 17 00:00:00 2001 From: Patrik <35232443+PatrikKozak@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:56:07 -0500 Subject: [PATCH] fix: prioritizes `value` key when filtering / querying for relationships (#4727) * fix: object equality query by prioritizing value key in relationship queries * chore: adds e2e & int test * chore: updates test for REST querying on poly relationships --- .../src/queries/buildSearchParams.ts | 11 +++- .../src/queries/sanitizeQueryValue.ts | 25 +++++++- .../payload/src/database/getLocalizedPaths.ts | 6 +- test/fields/e2e.spec.ts | 58 +++++++++++++++++++ test/relationships/int.spec.ts | 44 +++++++++++++- 5 files changed, 135 insertions(+), 9 deletions(-) diff --git a/packages/db-mongodb/src/queries/buildSearchParams.ts b/packages/db-mongodb/src/queries/buildSearchParams.ts index 91ca73cf45..389e2cea38 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -16,7 +16,8 @@ import { sanitizeQueryValue } from './sanitizeQueryValue' type SearchParam = { path?: string - value: unknown + rawQuery?: unknown + value?: unknown } const subQueryOptions = { @@ -92,7 +93,11 @@ export async function buildSearchParam({ const [{ field, path }] = paths if (path) { - const { operator: formattedOperator, val: formattedValue } = sanitizeQueryValue({ + const { + operator: formattedOperator, + rawQuery, + val: formattedValue, + } = sanitizeQueryValue({ field, hasCustomID, operator, @@ -100,6 +105,8 @@ export async function buildSearchParam({ val, }) + if (rawQuery) return { value: rawQuery } + // If there are multiple collections to search through, // Recursively build up a list of query constraints if (paths.length > 1) { diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts index c36430723d..604df04351 100644 --- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts +++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts @@ -17,7 +17,11 @@ export const sanitizeQueryValue = ({ operator, path, val, -}: SanitizeQueryValueArgs): { operator: string; val: unknown } => { +}: SanitizeQueryValueArgs): { + operator?: string + rawQuery?: unknown + val?: unknown +} => { let formattedValue = val let formattedOperator = operator @@ -70,6 +74,23 @@ export const sanitizeQueryValue = ({ formattedValue = null } + // Object equality requires the value to be the first key in the object that is being queried. + if ( + operator === 'equals' && + typeof formattedValue === 'object' && + formattedValue.value && + formattedValue.relationTo + ) { + return { + rawQuery: { + $and: [ + { [`${path}.value`]: { $eq: formattedValue.value } }, + { [`${path}.relationTo`]: { $eq: formattedValue.relationTo } }, + ], + }, + } + } + if (operator === 'in' && Array.isArray(formattedValue)) { formattedValue = formattedValue.reduce((formattedValues, inVal) => { const newValues = [inVal] @@ -104,7 +125,7 @@ export const sanitizeQueryValue = ({ formattedValue = undefined } else { formattedValue = { - $geometry: { coordinates: [parseFloat(lng), parseFloat(lat)], type: 'Point' }, + $geometry: { type: 'Point', coordinates: [parseFloat(lng), parseFloat(lat)] }, } if (maxDistance) formattedValue.$maxDistance = parseFloat(maxDistance) diff --git a/packages/payload/src/database/getLocalizedPaths.ts b/packages/payload/src/database/getLocalizedPaths.ts index 049b554480..56d660a5bb 100644 --- a/packages/payload/src/database/getLocalizedPaths.ts +++ b/packages/payload/src/database/getLocalizedPaths.ts @@ -96,9 +96,9 @@ export async function getLocalizedPaths({ // If this is a polymorphic relation, // We only support querying directly (no nested querying) if (typeof matchedField.relationTo !== 'string') { - const lastSegmentIsValid = ['relationTo', 'value'].includes( - pathSegments[pathSegments.length - 1], - ) + const lastSegmentIsValid = + ['relationTo', 'value'].includes(pathSegments[pathSegments.length - 1]) || + pathSegments.length === 1 if (lastSegmentIsValid) { lastIncompletePath.complete = true diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index c6518ff517..11ba26f3d5 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -3,6 +3,8 @@ import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' import path from 'path' +import type { RelationshipField, TextField } from './payload-types' + import payload from '../../packages/payload/src' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' import wait from '../../packages/payload/src/utilities/wait' @@ -1377,6 +1379,7 @@ describe('fields', () => { describe('relationship', () => { let url: AdminUrlUtil + const tableRowLocator = 'table > tbody > tr' beforeAll(async () => { url = new AdminUrlUtil(serverURL, 'relationship-fields') @@ -1709,6 +1712,37 @@ describe('fields', () => { const firstOptionText = await firstOption.textContent() expect(firstOptionText.trim()).toBe('Seeded text document') }) + + test('should allow filtering by relationship field / equals', async () => { + const textDoc = await createTextFieldDoc() + await createRelationshipFieldDoc({ value: textDoc.id, relationTo: 'text-fields' }) + + await page.goto(url.list) + + await page.locator('.list-controls__toggle-columns').click() + await page.locator('.list-controls__toggle-where').click() + await page.waitForSelector('.list-controls__where.rah-static--height-auto') + await page.locator('.where-builder__add-first-filter').click() + + const conditionField = page.locator('.condition__field') + await conditionField.click() + + const dropdownFieldOptions = conditionField.locator('.rs__option') + await dropdownFieldOptions.locator('text=Relationship').nth(0).click() + + const operatorField = page.locator('.condition__operator') + await operatorField.click() + + const dropdownOperatorOptions = operatorField.locator('.rs__option') + await dropdownOperatorOptions.locator('text=equals').click() + + const valueField = page.locator('.condition__value') + await valueField.click() + const dropdownValueOptions = valueField.locator('.rs__option') + await dropdownValueOptions.locator('text=some text').click() + + await expect(page.locator(tableRowLocator)).toHaveCount(1) + }) }) describe('upload', () => { @@ -1950,3 +1984,27 @@ describe('fields', () => { }) }) }) + +async function createTextFieldDoc(overrides?: Partial): Promise { + return payload.create({ + collection: 'text-fields', + data: { + text: 'some text', + localizedText: 'some localized text', + ...overrides, + }, + }) as unknown as Promise +} + +async function createRelationshipFieldDoc( + relationship: RelationshipField['relationship'], + overrides?: Partial, +): Promise { + return payload.create({ + collection: 'relationship-fields', + data: { + relationship, + ...overrides, + }, + }) as unknown as Promise +} diff --git a/test/relationships/int.spec.ts b/test/relationships/int.spec.ts index 398547148a..81536269b3 100644 --- a/test/relationships/int.spec.ts +++ b/test/relationships/int.spec.ts @@ -12,6 +12,7 @@ import type { import payload from '../../packages/payload/src' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' +import { devUser } from '../credentials' import { initPayloadTest } from '../helpers/configHelpers' import { RESTClient } from '../helpers/rest' import config, { @@ -23,15 +24,35 @@ import config, { slug, } from './config' +let apiUrl +let jwt let client: RESTClient +const headers = { + 'Content-Type': 'application/json', +} +const { email, password } = devUser + type EasierChained = { id: string; relation: EasierChained } describe('Relationships', () => { beforeAll(async () => { const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }) + apiUrl = `${serverURL}/api` client = new RESTClient(config, { serverURL, defaultSlug: slug }) await client.login() + + const response = await fetch(`${apiUrl}/users/login`, { + body: JSON.stringify({ + email, + password, + }), + headers, + method: 'post', + }) + + const data = await response.json() + jwt = data.token }) afterAll(async () => { @@ -582,7 +603,7 @@ describe('Relationships', () => { }, }) - const query = await client.find({ + const queryOne = await client.find({ slug: 'polymorphic-relationships', query: { and: [ @@ -600,7 +621,26 @@ describe('Relationships', () => { }, }) - expect(query.result.docs).toHaveLength(1) + const queryTwo = await client.find({ + slug: 'polymorphic-relationships', + query: { + and: [ + { + 'polymorphic.relationTo': { + equals: 'movies', + }, + }, + { + 'polymorphic.value': { + equals: movie.id, + }, + }, + ], + }, + }) + + expect(queryOne.result.docs).toHaveLength(1) + expect(queryTwo.result.docs).toHaveLength(1) }) }) })