From 46a24a98228fab31d2349642406a4e5ae22c7d87 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Thu, 12 Oct 2023 17:11:12 -0400 Subject: [PATCH] fix(db-postgres): in and not_in query operator (#3608) --- .../db-postgres/src/queries/parseParams.ts | 14 +++- .../src/queries/sanitizeQueryValue.ts | 18 +++++ test/collections-rest/int.spec.ts | 81 ++++++++++++++++++- 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/packages/db-postgres/src/queries/parseParams.ts b/packages/db-postgres/src/queries/parseParams.ts index 715461f1c..ba95dafde 100644 --- a/packages/db-postgres/src/queries/parseParams.ts +++ b/packages/db-postgres/src/queries/parseParams.ts @@ -2,7 +2,7 @@ import type { SQL } from 'drizzle-orm' import type { Field, Operator, Where } from 'payload/types' -import { and, ilike, isNotNull, isNull, ne, or, sql } from 'drizzle-orm' +import { and, ilike, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm' import { QueryError } from 'payload/errors' import { validOperators } from 'payload/types' @@ -147,6 +147,7 @@ export async function parseParams({ const { operator: queryOperator, value: queryValue } = sanitizeQueryValue({ field, operator, + relationOrPath, val, }) @@ -158,6 +159,17 @@ export async function parseParams({ ne(rawColumn || table[columnName], queryValue), ), ) + } else if ( + (field.type === 'relationship' || field.type === 'upload') && + Array.isArray(queryValue) && + operator === 'not_in' + ) { + constraints.push( + sql`${notInArray(table[columnName], queryValue)} OR + ${table[columnName]} + IS + NULL`, + ) } else { constraints.push( operatorMap[queryOperator](rawColumn || table[columnName], queryValue), diff --git a/packages/db-postgres/src/queries/sanitizeQueryValue.ts b/packages/db-postgres/src/queries/sanitizeQueryValue.ts index 148318e75..f041f3d0b 100644 --- a/packages/db-postgres/src/queries/sanitizeQueryValue.ts +++ b/packages/db-postgres/src/queries/sanitizeQueryValue.ts @@ -5,12 +5,14 @@ import { createArrayFromCommaDelineated } from 'payload/utilities' type SanitizeQueryValueArgs = { field: Field | TabAsField operator: string + relationOrPath: string val: any } export const sanitizeQueryValue = ({ field, operator: operatorArg, + relationOrPath, val, }: SanitizeQueryValueArgs): { operator: string; value: unknown } => { let operator = operatorArg @@ -18,6 +20,22 @@ export const sanitizeQueryValue = ({ if (!fieldAffectsData(field)) return { operator, value: formattedValue } + if ( + (field.type === 'relationship' || field.type === 'upload') && + !relationOrPath.endsWith('relationTo') && + Array.isArray(formattedValue) + ) { + const allPossibleIDTypes: (number | string)[] = [] + formattedValue.forEach((val) => { + if (typeof val === 'string') { + allPossibleIDTypes.push(val, parseInt(val)) + } else { + allPossibleIDTypes.push(val, String(val)) + } + }) + formattedValue = allPossibleIDTypes + } + // Cast incoming values as proper searchable types if (field.type === 'checkbox' && typeof val === 'string') { if (val.toLowerCase() === 'true') formattedValue = true diff --git a/test/collections-rest/int.spec.ts b/test/collections-rest/int.spec.ts index 59c129128..c39b68c75 100644 --- a/test/collections-rest/int.spec.ts +++ b/test/collections-rest/int.spec.ts @@ -7,7 +7,14 @@ import payload from '../../packages/payload/src' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' import { initPayloadTest } from '../helpers/configHelpers' import { RESTClient } from '../helpers/rest' -import config, { customIdNumberSlug, customIdSlug, errorOnHookSlug, pointSlug, relationSlug, slug, } from './config' +import config, { + customIdNumberSlug, + customIdSlug, + errorOnHookSlug, + pointSlug, + relationSlug, + slug, +} from './config' let client: RESTClient @@ -676,6 +683,72 @@ describe('collections-rest', () => { expect(result.totalDocs).toEqual(1) }) + it('not_in (relationships)', async () => { + const relationship = await payload.create({ + collection: relationSlug, + data: {}, + }) + + await createPost({ relationField: relationship.id, title: 'not-me' }) + // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) + const post2 = await createPost({ title: 'me' }) + const { result, status } = await client.find({ + query: { + relationField: { + not_in: [relationship.id], + }, + }, + }) + + // do not want to error for empty arrays + const { status: emptyNotInStatus } = await client.find({ + query: { + relationField: { + not_in: [], + }, + }, + }) + + expect(emptyNotInStatus).toEqual(200) + + expect(status).toEqual(200) + expect(result.docs).toEqual([post2]) + expect(result.totalDocs).toEqual(1) + }) + + it('in (relationships)', async () => { + const relationship = await payload.create({ + collection: relationSlug, + data: {}, + }) + + const post1 = await createPost({ relationField: relationship.id, title: 'me' }) + // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) + await createPost({ title: 'not-me' }) + const { result, status } = await client.find({ + query: { + relationField: { + in: [relationship.id], + }, + }, + }) + + // do not want to error for empty arrays + const { status: emptyNotInStatus } = await client.find({ + query: { + relationField: { + in: [], + }, + }, + }) + + expect(emptyNotInStatus).toEqual(200) + + expect(status).toEqual(200) + expect(result.docs).toEqual([post1]) + expect(result.totalDocs).toEqual(1) + }) + it('like', async () => { const post1 = await createPost({ title: 'prefix-value' }) @@ -1175,18 +1248,18 @@ describe('collections-rest', () => { }) }) -async function createPost (overrides?: Partial) { +async function createPost(overrides?: Partial) { const { doc } = await client.create({ data: { title: 'title', ...overrides } }) return doc } -async function createPosts (count: number) { +async function createPosts(count: number) { await mapAsync([...Array(count)], async () => { await createPost() }) } -async function clearDocs (): Promise { +async function clearDocs(): Promise { const allDocs = await payload.find({ collection: slug, limit: 100 }) const ids = allDocs.docs.map((doc) => doc.id) await mapAsync(ids, async (id) => {