From b988ac6b89173e2dce9185408acdc584b9d81004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Zori=C4=87?= Date: Tue, 27 Aug 2024 12:28:44 +0200 Subject: [PATCH] fix(api-headless-cms): filtering ref fields with null values (#4237) --- .../src/plugins/operator/equal.ts | 2 +- .../src/plugins/operator/not.ts | 2 +- .../filtering/plugins/refFilterPlugin.ts | 16 ++- packages/api-headless-cms-ddb/package.json | 1 + .../filtering/plugins/refFilterCreate.ts | 12 +- .../src/plugins/CmsEntryFieldFilterPlugin.ts | 16 +-- .../api-headless-cms-ddb/tsconfig.build.json | 1 + packages/api-headless-cms-ddb/tsconfig.json | 3 + .../__tests__/contentAPI/refField.test.ts | 129 +++++++++++++++++- packages/app-file-manager/tsconfig.build.json | 2 +- packages/app-file-manager/tsconfig.json | 6 +- .../tsconfig.json | 12 +- .../db-dynamodb/src/plugins/filters/eq.ts | 4 +- packages/pulumi-aws/tsconfig.build.json | 4 +- packages/pulumi-aws/tsconfig.json | 10 +- yarn.lock | 1 + 16 files changed, 183 insertions(+), 38 deletions(-) diff --git a/packages/api-elasticsearch/src/plugins/operator/equal.ts b/packages/api-elasticsearch/src/plugins/operator/equal.ts index 8aaed119139..879e8db291b 100644 --- a/packages/api-elasticsearch/src/plugins/operator/equal.ts +++ b/packages/api-elasticsearch/src/plugins/operator/equal.ts @@ -14,7 +14,7 @@ export class ElasticsearchQueryBuilderOperatorEqualPlugin extends ElasticsearchQ ): void { const { value, path, basePath } = params; - if (value === null) { + if (value === null || value === undefined) { query.must_not.push({ exists: { field: path diff --git a/packages/api-elasticsearch/src/plugins/operator/not.ts b/packages/api-elasticsearch/src/plugins/operator/not.ts index dc5a7523f23..1e918b8de5f 100644 --- a/packages/api-elasticsearch/src/plugins/operator/not.ts +++ b/packages/api-elasticsearch/src/plugins/operator/not.ts @@ -14,7 +14,7 @@ export class ElasticsearchQueryBuilderOperatorNotPlugin extends ElasticsearchQue ): void { const { value, path, basePath } = params; - if (value === null) { + if (value === null || value === undefined) { query.filter.push({ exists: { field: path diff --git a/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/plugins/refFilterPlugin.ts b/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/plugins/refFilterPlugin.ts index 1050e9adbf1..3ebea2f5527 100644 --- a/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/plugins/refFilterPlugin.ts +++ b/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/plugins/refFilterPlugin.ts @@ -3,10 +3,12 @@ import { CmsEntryFilterPlugin } from "~/plugins/CmsEntryFilterPlugin"; import { parseWhereKey } from "@webiny/api-elasticsearch"; export const createRefFilterPlugin = () => { - return new CmsEntryFilterPlugin({ + const plugin = new CmsEntryFilterPlugin({ fieldType: "ref", exec: params => { - const { applyFiltering, value: values, query, field } = params; + const { applyFiltering, query, field } = params; + + let values = params.value; /** * We must have an object when querying in the ref field. */ @@ -20,6 +22,12 @@ export const createRefFilterPlugin = () => { ); } + if (values === null || values === undefined) { + values = { + entryId: null + }; + } + for (const key in values) { const { operator } = parseWhereKey(key); const value = values[key]; @@ -37,4 +45,8 @@ export const createRefFilterPlugin = () => { } } }); + + plugin.name = `${plugin.type}.default.ref`; + + return plugin; }; diff --git a/packages/api-headless-cms-ddb/package.json b/packages/api-headless-cms-ddb/package.json index 21b54213125..96e94c4f676 100644 --- a/packages/api-headless-cms-ddb/package.json +++ b/packages/api-headless-cms-ddb/package.json @@ -23,6 +23,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.0", + "@webiny/api": "0.0.0", "@webiny/api-headless-cms": "0.0.0", "@webiny/aws-sdk": "0.0.0", "@webiny/db-dynamodb": "0.0.0", diff --git a/packages/api-headless-cms-ddb/src/operations/entry/filtering/plugins/refFilterCreate.ts b/packages/api-headless-cms-ddb/src/operations/entry/filtering/plugins/refFilterCreate.ts index 37d29dc94d1..962cda6d476 100644 --- a/packages/api-headless-cms-ddb/src/operations/entry/filtering/plugins/refFilterCreate.ts +++ b/packages/api-headless-cms-ddb/src/operations/entry/filtering/plugins/refFilterCreate.ts @@ -5,17 +5,23 @@ import { } from "~/plugins/CmsEntryFieldFilterPlugin"; import { extractWhereParams } from "~/operations/entry/filtering/where"; import { transformValue } from "~/operations/entry/filtering/transform"; +import { GenericRecord } from "@webiny/api/types"; export const createRefFilterCreate = () => { - const plugin = new CmsEntryFieldFilterPlugin({ + const plugin = new CmsEntryFieldFilterPlugin({ fieldType: "ref", create: params => { - const { value, valueFilterPlugins, transformValuePlugins, field } = params; + const { valueFilterPlugins, transformValuePlugins, field } = params; + let value = params.value; + if (!value) { + value = { + entryId: null + }; + } const propertyFilters = Object.keys(value); if (propertyFilters.length === 0) { return null; } - const filters: CmsEntryFieldFilterPluginCreateResponse[] = []; for (const propertyFilter of propertyFilters) { diff --git a/packages/api-headless-cms-ddb/src/plugins/CmsEntryFieldFilterPlugin.ts b/packages/api-headless-cms-ddb/src/plugins/CmsEntryFieldFilterPlugin.ts index 81fa14a9c77..1ba4db03aef 100644 --- a/packages/api-headless-cms-ddb/src/plugins/CmsEntryFieldFilterPlugin.ts +++ b/packages/api-headless-cms-ddb/src/plugins/CmsEntryFieldFilterPlugin.ts @@ -8,16 +8,16 @@ import { CmsFieldFilterValueTransformPlugin } from "~/types"; * Internally we have default one + the one for the reference field - because it is actually an object when filtering. */ -interface CmsEntryFieldFilterPluginParams { +interface CmsEntryFieldFilterPluginParams { fieldType: string; create: ( - params: CmsEntryFieldFilterPluginCreateParams + params: CmsEntryFieldFilterPluginCreateParams ) => null | CmsEntryFieldFilterPluginCreateResponse | CmsEntryFieldFilterPluginCreateResponse[]; } -interface CmsEntryFieldFilterPluginCreateParams { +interface CmsEntryFieldFilterPluginCreateParams { key: string; - value: any; + value: T; field: Field; fields: Record; operation: string; @@ -39,21 +39,21 @@ export interface CmsEntryFieldFilterPluginCreateResponse { transformValue: (value: I) => O; } -export class CmsEntryFieldFilterPlugin extends Plugin { +export class CmsEntryFieldFilterPlugin extends Plugin { public static override readonly type: string = "cms.dynamodb.entry.field.filter"; public static readonly ALL: string = "*"; - private readonly config: CmsEntryFieldFilterPluginParams; + private readonly config: CmsEntryFieldFilterPluginParams; public readonly fieldType: string; - public constructor(config: CmsEntryFieldFilterPluginParams) { + public constructor(config: CmsEntryFieldFilterPluginParams) { super(); this.config = config; this.fieldType = this.config.fieldType; } - public create(params: CmsEntryFieldFilterPluginCreateParams) { + public create(params: CmsEntryFieldFilterPluginCreateParams) { return this.config.create(params); } } diff --git a/packages/api-headless-cms-ddb/tsconfig.build.json b/packages/api-headless-cms-ddb/tsconfig.build.json index bf17bbe28dd..b22550f95d2 100644 --- a/packages/api-headless-cms-ddb/tsconfig.build.json +++ b/packages/api-headless-cms-ddb/tsconfig.build.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.build.json", "include": ["src"], "references": [ + { "path": "../api/tsconfig.build.json" }, { "path": "../api-headless-cms/tsconfig.build.json" }, { "path": "../aws-sdk/tsconfig.build.json" }, { "path": "../db-dynamodb/tsconfig.build.json" }, diff --git a/packages/api-headless-cms-ddb/tsconfig.json b/packages/api-headless-cms-ddb/tsconfig.json index 9e355ca7a3f..ab91f5e37b0 100644 --- a/packages/api-headless-cms-ddb/tsconfig.json +++ b/packages/api-headless-cms-ddb/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "include": ["src", "__tests__"], "references": [ + { "path": "../api" }, { "path": "../api-headless-cms" }, { "path": "../aws-sdk" }, { "path": "../db-dynamodb" }, @@ -17,6 +18,8 @@ "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], + "@webiny/api/*": ["../api/src/*"], + "@webiny/api": ["../api/src"], "@webiny/api-headless-cms/*": ["../api-headless-cms/src/*"], "@webiny/api-headless-cms": ["../api-headless-cms/src"], "@webiny/aws-sdk/*": ["../aws-sdk/src/*"], diff --git a/packages/api-headless-cms/__tests__/contentAPI/refField.test.ts b/packages/api-headless-cms/__tests__/contentAPI/refField.test.ts index 0d15e59aa27..3a283574655 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/refField.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/refField.test.ts @@ -123,7 +123,7 @@ describe("refField", () => { return publishAuthorResponse.data.publishAuthor.data; }; - test("should create review connected to a product", async () => { + it("should create review connected to a product", async () => { await setupContentModels(mainHandler); const category = await createCategory(); @@ -441,4 +441,131 @@ describe("refField", () => { } }); }); + + it("should create a product which is not connected to category and list and filter by the category value", async () => { + await setupContentModels(mainHandler); + const { createProduct, listProducts } = useProductManageHandler({ + ...manageOpts + }); + + const [listEmptyResult] = await listProducts({ + where: { + category: null + } + }); + expect(listEmptyResult).toMatchObject({ + data: { + listProducts: { + data: [], + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 0 + } + } + } + }); + + const data = { + title: "Potato", + price: 100, + availableOn: "2020-12-25", + color: "white", + availableSizes: ["s", "m"], + image: "file.jpg", + category: null + }; + const [createResponse] = await createProduct({ + data + }); + + expect(createResponse).toMatchObject({ + data: { + createProduct: { + data: { + id: expect.any(String), + ...data, + category: null + }, + error: null + } + } + }); + + const [listResult] = await listProducts(); + + expect(listResult).toMatchObject({ + data: { + listProducts: { + data: [ + { + id: expect.any(String), + entryId: expect.any(String), + ...data, + category: null + } + ], + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 1 + } + } + } + }); + + const [whereNullResult] = await listProducts({ + where: { + category: null + } + }); + expect(whereNullResult).toMatchObject({ + data: { + listProducts: { + data: [ + { + id: expect.any(String), + entryId: expect.any(String), + ...data, + category: null + } + ], + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 1 + } + } + } + }); + + const [whereUndefinedResult] = await listProducts({ + where: { + category: undefined + } + }); + expect(whereUndefinedResult).toMatchObject({ + data: { + listProducts: { + data: [ + { + id: expect.any(String), + entryId: expect.any(String), + ...data, + category: null + } + ], + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 1 + } + } + } + }); + }); }); diff --git a/packages/app-file-manager/tsconfig.build.json b/packages/app-file-manager/tsconfig.build.json index 74817e22279..217862e1934 100644 --- a/packages/app-file-manager/tsconfig.build.json +++ b/packages/app-file-manager/tsconfig.build.json @@ -5,9 +5,9 @@ { "path": "../app/tsconfig.build.json" }, { "path": "../app-aco/tsconfig.build.json" }, { "path": "../app-admin/tsconfig.build.json" }, - { "path": "../app-i18n/tsconfig.build.json" }, { "path": "../app-headless-cms/tsconfig.build.json" }, { "path": "../app-headless-cms-common/tsconfig.build.json" }, + { "path": "../app-i18n/tsconfig.build.json" }, { "path": "../app-security/tsconfig.build.json" }, { "path": "../app-tenancy/tsconfig.build.json" }, { "path": "../error/tsconfig.build.json" }, diff --git a/packages/app-file-manager/tsconfig.json b/packages/app-file-manager/tsconfig.json index df85064dc1e..c6f1dd20495 100644 --- a/packages/app-file-manager/tsconfig.json +++ b/packages/app-file-manager/tsconfig.json @@ -5,9 +5,9 @@ { "path": "../app" }, { "path": "../app-aco" }, { "path": "../app-admin" }, - { "path": "../app-i18n" }, { "path": "../app-headless-cms" }, { "path": "../app-headless-cms-common" }, + { "path": "../app-i18n" }, { "path": "../app-security" }, { "path": "../app-tenancy" }, { "path": "../error" }, @@ -32,12 +32,12 @@ "@webiny/app-aco": ["../app-aco/src"], "@webiny/app-admin/*": ["../app-admin/src/*"], "@webiny/app-admin": ["../app-admin/src"], - "@webiny/app-i18n/*": ["../app-i18n/src/*"], - "@webiny/app-i18n": ["../app-i18n/src"], "@webiny/app-headless-cms/*": ["../app-headless-cms/src/*"], "@webiny/app-headless-cms": ["../app-headless-cms/src"], "@webiny/app-headless-cms-common/*": ["../app-headless-cms-common/src/*"], "@webiny/app-headless-cms-common": ["../app-headless-cms-common/src"], + "@webiny/app-i18n/*": ["../app-i18n/src/*"], + "@webiny/app-i18n": ["../app-i18n/src"], "@webiny/app-security/*": ["../app-security/src/*"], "@webiny/app-security": ["../app-security/src"], "@webiny/app-tenancy/*": ["../app-tenancy/src/*"], diff --git a/packages/cli-plugin-scaffold-extensions/tsconfig.json b/packages/cli-plugin-scaffold-extensions/tsconfig.json index 5cbebb5ff09..0a46a2edd0a 100644 --- a/packages/cli-plugin-scaffold-extensions/tsconfig.json +++ b/packages/cli-plugin-scaffold-extensions/tsconfig.json @@ -2,15 +2,9 @@ "extends": "../../tsconfig.json", "include": ["src", "__tests__"], "references": [ - { - "path": "../aws-sdk" - }, - { - "path": "../cli-plugin-scaffold" - }, - { - "path": "../error" - } + { "path": "../aws-sdk" }, + { "path": "../cli-plugin-scaffold" }, + { "path": "../error" } ], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], diff --git a/packages/db-dynamodb/src/plugins/filters/eq.ts b/packages/db-dynamodb/src/plugins/filters/eq.ts index f055e9a498a..918061b32e8 100644 --- a/packages/db-dynamodb/src/plugins/filters/eq.ts +++ b/packages/db-dynamodb/src/plugins/filters/eq.ts @@ -12,10 +12,10 @@ const plugin = new ValueFilterPlugin({ }); } else if (Array.isArray(compareValue) === true) { return compareValue.every((v: string) => { - return value === v; + return value == v; }); } - return value === compareValue; + return value == compareValue; } }); diff --git a/packages/pulumi-aws/tsconfig.build.json b/packages/pulumi-aws/tsconfig.build.json index 95d2ea4294f..88d73a8f7d3 100644 --- a/packages/pulumi-aws/tsconfig.build.json +++ b/packages/pulumi-aws/tsconfig.build.json @@ -3,9 +3,9 @@ "include": ["src"], "references": [ { "path": "../aws-sdk/tsconfig.build.json" }, - { "path": "../feature-flags/tsconfig.build.json" }, { "path": "../pulumi/tsconfig.build.json" }, - { "path": "../api-page-builder/tsconfig.build.json" } + { "path": "../api-page-builder/tsconfig.build.json" }, + { "path": "../feature-flags/tsconfig.build.json" } ], "compilerOptions": { "rootDir": "./src", diff --git a/packages/pulumi-aws/tsconfig.json b/packages/pulumi-aws/tsconfig.json index 5d3c7a0055d..ce4d5c491fb 100644 --- a/packages/pulumi-aws/tsconfig.json +++ b/packages/pulumi-aws/tsconfig.json @@ -3,9 +3,9 @@ "include": ["src", "__tests__"], "references": [ { "path": "../aws-sdk" }, - { "path": "../feature-flags" }, { "path": "../pulumi" }, - { "path": "../api-page-builder" } + { "path": "../api-page-builder" }, + { "path": "../feature-flags" } ], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], @@ -16,12 +16,12 @@ "~tests/*": ["./__tests__/*"], "@webiny/aws-sdk/*": ["../aws-sdk/src/*"], "@webiny/aws-sdk": ["../aws-sdk/src"], - "@webiny/feature-flags/*": ["../feature-flags/src/*"], - "@webiny/feature-flags": ["../feature-flags/src"], "@webiny/pulumi/*": ["../pulumi/src/*"], "@webiny/pulumi": ["../pulumi/src"], "@webiny/api-page-builder/*": ["../api-page-builder/src/*"], - "@webiny/api-page-builder": ["../api-page-builder/src"] + "@webiny/api-page-builder": ["../api-page-builder/src"], + "@webiny/feature-flags/*": ["../feature-flags/src/*"], + "@webiny/feature-flags": ["../feature-flags/src"] }, "baseUrl": "." } diff --git a/yarn.lock b/yarn.lock index 6f585502cd7..a7c77c339fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15939,6 +15939,7 @@ __metadata: "@babel/preset-env": ^7.24.0 "@babel/runtime": ^7.24.0 "@types/jsonpack": ^1.1.0 + "@webiny/api": 0.0.0 "@webiny/api-headless-cms": 0.0.0 "@webiny/aws-sdk": 0.0.0 "@webiny/cli": 0.0.0