From 12c812d379be60c9b42db04e6bfc60209bd57e93 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Fri, 17 May 2024 14:09:44 -0400 Subject: [PATCH] fix(db-postgres): query with like on id columns (#6414) When typing into the search input on the list view of a collection, the `like` operator is used for id which causes an error for postgres. To fix this we are sanitizing the `like` for number or uuid fields to instead be an `equals` operator. An alternate solution would have been to cast the ids to text `id::text` but this would have performence implications on larger data sets. --------- Co-authored-by: James --- .../db-postgres/src/queries/parseParams.ts | 9 +++- pnpm-lock.yaml | 44 ++++++++--------- test/collections-rest/int.spec.ts | 49 ++++++++++++++----- 3 files changed, 65 insertions(+), 37 deletions(-) diff --git a/packages/db-postgres/src/queries/parseParams.ts b/packages/db-postgres/src/queries/parseParams.ts index 477a9497f4..11341cb671 100644 --- a/packages/db-postgres/src/queries/parseParams.ts +++ b/packages/db-postgres/src/queries/parseParams.ts @@ -71,7 +71,7 @@ export async function parseParams({ // So we need to loop on keys again here to handle each operator independently const pathOperators = where[relationOrPath] if (typeof pathOperators === 'object') { - for (const operator of Object.keys(pathOperators)) { + for (let operator of Object.keys(pathOperators)) { if (validOperators.includes(operator as Operator)) { const val = where[relationOrPath][operator] const { @@ -157,6 +157,13 @@ export async function parseParams({ break } + if ( + operator === 'like' && + (field.type === 'number' || table[columnName].columnType === 'PgUUID') + ) { + operator = 'equals' + } + if (operator === 'like') { constraints.push( and(...val.split(' ').map((word) => ilike(table[columnName], `%${word}%`))), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e7accaf26..e6d50b3a05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1443,7 +1443,7 @@ importers: version: link:../plugin-cloud-storage uploadthing: specifier: ^6.10.1 - version: 6.10.1(next@14.3.0-canary.68) + version: 6.10.4(next@14.3.0-canary.68) devDependencies: payload: specifier: workspace:* @@ -1746,7 +1746,7 @@ importers: version: 5.4.5 uploadthing: specifier: ^6.10.1 - version: 6.10.1(next@14.3.0-canary.68) + version: 6.10.4(next@14.3.0-canary.68) packages: @@ -3366,13 +3366,13 @@ packages: dependencies: superjson: 2.2.1 - /@effect/schema@0.66.14(effect@3.1.2)(fast-check@3.18.0): - resolution: {integrity: sha512-2Yc6gnXpcMmwQnbU2JUwDl0ckeOJmFZzteXn2jjVWuNi9PGv+jp2yK7jxv0pALcieuYwdR5tKkCRI7STuhEwfg==} + /@effect/schema@0.66.16(effect@3.1.5)(fast-check@3.18.0): + resolution: {integrity: sha512-sT/k5NOgKslGPzs3DUaCFuM6g2JQoIIT8jpwEorAZooplPIMK2xIspr7ECz6pp6Dc7Wz/ppXGk7HVyGZQsIYEQ==} peerDependencies: - effect: ^3.1.2 + effect: ^3.1.3 fast-check: ^3.13.2 dependencies: - effect: 3.1.2 + effect: 3.1.5 fast-check: 3.18.0 /@emnapi/runtime@1.1.1: @@ -6696,21 +6696,19 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - /@uploadthing/mime-types@0.2.9: - resolution: {integrity: sha512-7Ap2evP+niXNSXoOck4DUZUAKYrMRRpJ85n+ZxCuRsnr3iziOgv/Rt6es5SnMVgE/aOObxddOxbrxyOhISQWHQ==} + /@uploadthing/mime-types@0.2.10: + resolution: {integrity: sha512-kz3F0oEgAyts25NAGXlUBCWh3mXonbSOQJFGFMawHuIgbUbnzXbe4w5WI+0XdneCbjNmikfWrdWrs8m/7HATfQ==} - /@uploadthing/shared@6.7.1(@uploadthing/mime-types@0.2.9): - resolution: {integrity: sha512-4Imk46n+rwFaobfDuDSclYcH/OcpJA048Ww0il6nqT7EoXQ7Pa9NJfGkoNkS8+K8rT71IVljdaiaLEapgRxj0Q==} + /@uploadthing/shared@6.7.4(@uploadthing/mime-types@0.2.10): + resolution: {integrity: sha512-7e35U7/84qQQx+bhldSdze9ODiAo4NCCZiYEV3NomD44O9fE5bfQXcCPtBeXh3y3udf7S28PMZbT6ybJeuKfgw==} peerDependencies: - '@uploadthing/mime-types': 0.2.9 + '@uploadthing/mime-types': 0.2.10 peerDependenciesMeta: '@uploadthing/mime-types': optional: true dependencies: - '@effect/schema': 0.66.14(effect@3.1.2)(fast-check@3.18.0) - '@uploadthing/mime-types': 0.2.9 - effect: 3.1.2 - fast-check: 3.18.0 + '@uploadthing/mime-types': 0.2.10 + effect: 3.1.5 std-env: 3.7.0 /@vercel/blob@0.22.3: @@ -9020,8 +9018,8 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false - /effect@3.1.2: - resolution: {integrity: sha512-XakSWck6w6ROqKyEys0tKE9K6Gx2p8W/09u2ZTEZZrneO5Z3QEdPhXzWTyC73kD5zUvfJinZLVIas8I1xoHaTg==} + /effect@3.1.5: + resolution: {integrity: sha512-nTkW/ViRF8cXVMKjusYMVp7L1eqLwMZV7K6GHoqq3MD7NjPT/tJRVhY8d6z5Vam9kxgvRAzqwJKkaRpRVz1jTw==} /electron-to-chromium@1.4.730: resolution: {integrity: sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==} @@ -16269,8 +16267,8 @@ packages: escalade: 3.1.2 picocolors: 1.0.0 - /uploadthing@6.10.1(next@14.3.0-canary.68): - resolution: {integrity: sha512-xqgEauaDYLlQ6NEsQWF+fpsUOuniWazd8ytSK2bCxcKFGakCkXO32GkFolW4iZZgkl7ATnHwN+JXG95FPdo74w==} + /uploadthing@6.10.4(next@14.3.0-canary.68): + resolution: {integrity: sha512-0hGO0Q7R7MnxzVkUbYHE6PkwFieYH+UUa905uo7JtA0h3Gpc89bNDFaOfK8634Z66088VRLNVuWxY2FTIqw4sg==} engines: {node: '>=18.13.0'} peerDependencies: express: '*' @@ -16290,11 +16288,11 @@ packages: tailwindcss: optional: true dependencies: - '@effect/schema': 0.66.14(effect@3.1.2)(fast-check@3.18.0) - '@uploadthing/mime-types': 0.2.9 - '@uploadthing/shared': 6.7.1(@uploadthing/mime-types@0.2.9) + '@effect/schema': 0.66.16(effect@3.1.5)(fast-check@3.18.0) + '@uploadthing/mime-types': 0.2.10 + '@uploadthing/shared': 6.7.4(@uploadthing/mime-types@0.2.10) consola: 3.2.3 - effect: 3.1.2 + effect: 3.1.5 fast-check: 3.18.0 next: 14.3.0-canary.68(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.3.1)(react@18.3.1)(sass@1.74.1) std-env: 3.7.0 diff --git a/test/collections-rest/int.spec.ts b/test/collections-rest/int.spec.ts index 12cdb65b09..736a6b4484 100644 --- a/test/collections-rest/int.spec.ts +++ b/test/collections-rest/int.spec.ts @@ -133,10 +133,10 @@ describe('collections-rest', () => { const description = 'updated' const response = await restClient.PATCH(`/${slug}`, { - query: { where: { title: { equals: 'title' } } }, body: JSON.stringify({ description, }), + query: { where: { title: { equals: 'title' } } }, }) const { docs, errors } = await response.json() @@ -155,10 +155,10 @@ describe('collections-rest', () => { const description = 'updated' const response = await restClient.PATCH(`/${slug}`, { - query: { where: { missing: { equals: 'title' } } }, body: JSON.stringify({ description, }), + query: { where: { missing: { equals: 'title' } } }, }) const { docs: noDocs, errors } = await response.json() @@ -181,18 +181,18 @@ describe('collections-rest', () => { const description = 'updated' const relationFieldResponse = await restClient.PATCH(`/${slug}`, { - query: { where: { 'relationField.missing': { equals: 'title' } } }, body: JSON.stringify({ description, }), + query: { where: { 'relationField.missing': { equals: 'title' } } }, }) expect(relationFieldResponse.status).toEqual(400) const relationMultiRelationToResponse = await restClient.PATCH(`/${slug}`, { - query: { where: { 'relationMultiRelationTo.missing': { equals: 'title' } } }, body: JSON.stringify({ description, }), + query: { where: { 'relationMultiRelationTo.missing': { equals: 'title' } } }, }) expect(relationMultiRelationToResponse.status).toEqual(400) @@ -214,10 +214,10 @@ describe('collections-rest', () => { const description = 'description' const response = await restClient.PATCH(`/${slug}`, { - query: { where: { restrictedField: { equals: 'restricted' } } }, body: JSON.stringify({ description, }), + query: { where: { restrictedField: { equals: 'restricted' } } }, }) const result = await response.json() @@ -253,10 +253,10 @@ describe('collections-rest', () => { const update = 'update' const response = await restClient.PATCH(`/${errorOnHookSlug}`, { - query: { where: { text: { equals: text } } }, body: JSON.stringify({ text: update, }), + query: { where: { text: { equals: text } } }, }) const result = await response.json() @@ -544,6 +544,29 @@ describe('collections-rest', () => { expect(result.docs).toEqual([post]) expect(result.totalDocs).toEqual(1) }) + + it('should query LIKE by ID', async () => { + const post = await payload.create({ + collection: slug, + data: { + title: 'find me buddy', + }, + }) + + const response = await restClient.GET(`/${slug}`, { + query: { + where: { + id: { + like: post.id, + }, + }, + }, + }) + + const result = await response.json() + expect(response.status).toStrictEqual(200) + expect(result.totalDocs).toStrictEqual(1) + }) }) it('should query nested relationship - hasMany', async () => { @@ -1058,13 +1081,13 @@ describe('collections-rest', () => { const { docs } = await restClient .GET(`/${pointSlug}`, { query: { + limit: 5, where: { point: { // querying large enough range to include all docs near: '0, 0, 100000, 0', }, }, - limit: 5, }, }) .then((res) => res.json()) @@ -1094,8 +1117,8 @@ describe('collections-rest', () => { where: { point: { within: { - coordinates: [polygon], type: 'Polygon', + coordinates: [polygon], }, }, }, @@ -1113,8 +1136,8 @@ describe('collections-rest', () => { where: { point: { within: { - coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size type: 'Polygon', + coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size }, }, }, @@ -1144,8 +1167,8 @@ describe('collections-rest', () => { where: { point: { intersects: { - coordinates: [polygon], type: 'Polygon', + coordinates: [polygon], }, }, }, @@ -1163,8 +1186,8 @@ describe('collections-rest', () => { where: { point: { intersects: { - coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size type: 'Polygon', + coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size }, }, }, @@ -1397,12 +1420,12 @@ describe('collections-rest', () => { it('should query a limited set of docs', async () => { const response = await restClient.GET(`/${slug}`, { query: { + limit: 15, where: { title: { equals: 'limit-test', }, }, - limit: 15, }, }) const result = await response.json() @@ -1414,12 +1437,12 @@ describe('collections-rest', () => { it('should query all docs when limit=0', async () => { const response = await restClient.GET(`/${slug}`, { query: { + limit: 0, where: { title: { equals: 'limit-test', }, }, - limit: 0, }, }) const result = await response.json()