diff --git a/docs/DEPLOY_WEBINY_PROJECT_CF_TEMPLATE.yaml b/docs/DEPLOY_WEBINY_PROJECT_CF_TEMPLATE.yaml index 08be64cd103..3d4677eb150 100644 --- a/docs/DEPLOY_WEBINY_PROJECT_CF_TEMPLATE.yaml +++ b/docs/DEPLOY_WEBINY_PROJECT_CF_TEMPLATE.yaml @@ -233,6 +233,7 @@ Resources: # https://www.webiny.com/docs/architecture/introduction#different-database-setups - Effect: Allow Action: + - iam:GetRole - iam:CreateServiceLinkedRole Resource: - arn:aws:iam::*:role/aws-service-role/es.amazonaws.com/AWSServiceRoleForAmazonElasticsearchService diff --git a/packages/api-elasticsearch/src/normalize.ts b/packages/api-elasticsearch/src/normalize.ts index d0aea515655..211cdcc2db7 100644 --- a/packages/api-elasticsearch/src/normalize.ts +++ b/packages/api-elasticsearch/src/normalize.ts @@ -3,6 +3,8 @@ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_reserved_characters */ +const specialCharactersToRemove = [`\\?`]; + const specialCharacterToSpace = ["-"]; const specialCharacters = [ @@ -10,11 +12,11 @@ const specialCharacters = [ "\\+", // "\\-", "\\=", - "\\&\\&", - "\\|\\|", + `\&`, + `\\|`, ">", "<", - "\\!", + `\!`, "\\(", "\\)", "\\{", @@ -25,14 +27,19 @@ const specialCharacters = [ '\\"', "\\~", "\\*", - "\\?", - "\\:", + `\:`, `\/`, "\\#" ]; export const normalizeValue = (value: string) => { let result = value || ""; + if (!result) { + return result; + } + for (const character of specialCharactersToRemove) { + result = result.replace(new RegExp(character, "g"), ""); + } for (const character of specialCharacterToSpace) { result = result.replace(new RegExp(character, "g"), " "); } @@ -44,6 +51,13 @@ export const normalizeValue = (value: string) => { return result || ""; }; +const hasSpecialChar = (value: string): boolean | null => { + if (!value) { + return null; + } + return value.match(/^([0-9a-zA-Z]+)$/i) === null; +}; + export const normalizeValueWithAsterisk = (initial: string) => { const value = normalizeValue(initial); const results = value.split(" "); @@ -53,14 +67,14 @@ export const normalizeValueWithAsterisk = (initial: string) => { * If there is a / in the first word, do not put asterisk in front of it. */ const firstWord = results[0]; - if (firstWord && firstWord.includes("/") === false) { + if (hasSpecialChar(firstWord) === false) { result = `*${result}`; } /** * If there is a / in the last word, do not put asterisk at the end of it. */ const lastWord = results[results.length - 1]; - if (lastWord && lastWord.includes("/") === false) { + if (hasSpecialChar(lastWord) === false) { result = `${result}*`; } diff --git a/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts b/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts index 03a12fc7967..13a4613bbee 100644 --- a/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts +++ b/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts @@ -204,9 +204,7 @@ describe("filtering", () => { fields }); - expect(resultNumber3).toHaveLength(0); - - expect(resultNumber3).toMatchObject([]); + expect(resultNumber3).toHaveLength(19); }); it("should filter by nested options variant colors", async () => { diff --git a/packages/api-headless-cms/__tests__/contentAPI/search.test.ts b/packages/api-headless-cms/__tests__/contentAPI/search.test.ts index f1f3c076114..ec14da3ea3d 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/search.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/search.test.ts @@ -1,7 +1,12 @@ import { useFruitManageHandler } from "~tests/testHelpers/useFruitManageHandler"; import { setupContentModelGroup, setupContentModels } from "~tests/testHelpers/setup"; +import { useCategoryManageHandler } from "~tests/testHelpers/useCategoryManageHandler"; +import { toSlug } from "~/utils/toSlug"; describe("search", () => { + const categoryManager = useCategoryManageHandler({ + path: "manage/en-US" + }); const fruitManager = useFruitManageHandler({ path: "manage/en-US" }); @@ -13,6 +18,7 @@ describe("search", () => { }); if (response.data.createFruit.error) { + console.log(JSON.stringify(response.data.createFruit.error, null, 2)); throw new Error(response.data.createFruit.error.message); } return response.data.createFruit.data; @@ -41,11 +47,11 @@ describe("search", () => { const setupFruits = async (input?: string[]) => { const group = await setupContentModelGroup(fruitManager); - await setupContentModels(fruitManager, group, ["fruit"]); + await setupContentModels(fruitManager, group, ["fruit", "category"]); return createFruits(input); }; - it.skip("should find record with dash in the middle of two words", async () => { + it("should find record with dash in the middle of two words", async () => { await setupFruits(); const [response] = await listFruits({ where: { @@ -331,4 +337,166 @@ describe("search", () => { } }); }); + + it("should find a record with special characters in the title", async () => { + await setupFruits(); + const categories = { + apple: "A Tasty Fruit: Apple w/ Black Dots?", + banana: "A Not So Tasty Fruit: Banana w/ Yellow Dots", + orange: "Awesome Fruit: Orange w/ Leaves", + grape: "Wine - An Autumn Fruit: Grape w/ Seeds?", + tangerine: "An Autumn Fruit: Tangerine w/ Seeds?", + cleaning: "Clean Building Day | The Ultimate Cleaning Trick Tips!", + car: "2001 CarMaker Car type: SVO Reborn? - Burn Epi. 917" + }; + const results: any[] = []; + for (const title of Object.values(categories)) { + const [result] = await categoryManager.createCategory({ + data: { + title, + slug: toSlug(title) + } + }); + results.push(result?.data?.createCategory?.data); + } + expect(results).toHaveLength(Object.values(categories).length); + + const [initialResponse] = await categoryManager.listCategories({ + sort: ["createdOn_ASC"] + }); + expect(initialResponse).toMatchObject({ + data: { + listCategories: { + data: expect.any(Array), + meta: { + totalCount: Object.values(categories).length, + hasMoreItems: false, + cursor: null + }, + error: null + } + } + }); + + const [appleListResponse] = await categoryManager.listCategories({ + where: { + title_contains: "tasty fruit: apple w/ black dots" + } + }); + expect(appleListResponse).toMatchObject({ + data: { + listCategories: { + data: [ + { + title: categories.apple + } + ], + meta: { + totalCount: 1, + hasMoreItems: false, + cursor: null + }, + error: null + } + } + }); + + const [dotsListResponse] = await categoryManager.listCategories({ + where: { + title_contains: "tasty fruit: w/ dots" + } + }); + expect(dotsListResponse).toMatchObject({ + data: { + listCategories: { + data: [ + { + title: categories.banana + }, + { + title: categories.apple + } + ], + meta: { + totalCount: 2, + hasMoreItems: false, + cursor: null + }, + error: null + } + } + }); + + const [questionMarksListResponse] = await categoryManager.listCategories({ + where: { + title_contains: "autumn fruit: seeds?" + } + }); + expect(questionMarksListResponse).toMatchObject({ + data: { + listCategories: { + data: [ + { + title: categories.tangerine + }, + { + title: categories.grape + } + ], + meta: { + totalCount: 2, + hasMoreItems: false, + cursor: null + }, + error: null + } + } + }); + + const [cleaningListResponse] = await categoryManager.listCategories({ + where: { + title_contains: "Clean Building Day | The Ultimate Cleaning Trick Tips!" + } + }); + expect(cleaningListResponse).toMatchObject({ + data: { + listCategories: { + data: [ + { + title: categories.cleaning + } + ], + meta: { + totalCount: 1, + hasMoreItems: false, + cursor: null + }, + error: null + } + } + }); + + const [carListResponse] = await categoryManager.listCategories({ + where: { + title_contains: "2001 CarMaker Car type: SVO Reborn? - Burn Epi. 917" + } + }); + expect(carListResponse).toMatchObject({ + data: { + listCategories: { + data: [ + { + title: categories.car + } + ], + meta: { + totalCount: 1, + hasMoreItems: false, + cursor: null + }, + error: null + } + } + }); + }); }); diff --git a/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts b/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts index 0ec37bd034d..f1c213949dd 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/usePageManageHandler.ts @@ -37,6 +37,27 @@ const pageFields = ` _templateId __typename } + ...on ${singularPageApiName}_Content_Author { + author { + id + modelId + } + authors { + id + modelId + } + _templateId + dynamicZone { + __typename + ... on ${singularPageApiName}_Content_Objecting_DynamicZone_SuperNestedObject { + authors { + id + modelId + } + } + } + __typename + } ...on ${singularPageApiName}_Content_Author { author { id diff --git a/packages/api-headless-cms/src/crud/contentEntry/referenceFieldsMapping.ts b/packages/api-headless-cms/src/crud/contentEntry/referenceFieldsMapping.ts index 9f7b2780985..9764b115ff3 100644 --- a/packages/api-headless-cms/src/crud/contentEntry/referenceFieldsMapping.ts +++ b/packages/api-headless-cms/src/crud/contentEntry/referenceFieldsMapping.ts @@ -287,12 +287,14 @@ async function validateReferencedEntries({ /** * Load all models and use only those used in the input references. */ - const models = (await context.cms.listModels()).filter(model => { - const entries = referencesByModel.get(model.modelId); - if (!entries || !entries.length) { - return false; - } - return true; + const models = await context.security.withoutAuthorization(async () => { + return (await context.cms.listModels()).filter(model => { + const entries = referencesByModel.get(model.modelId); + if (!entries?.length) { + return false; + } + return true; + }); }); if (!models.length) { @@ -302,8 +304,10 @@ async function validateReferencedEntries({ /** * Load all the entries by their IDs. */ - const promises = models.map(model => { - return context.cms.getEntriesByIds(model, referencesByModel.get(model.modelId) || []); + const promises = await context.security.withoutAuthorization(async () => { + return models.map(model => { + return context.cms.getEntriesByIds(model, referencesByModel.get(model.modelId) || []); + }); }); const allEntries = await Promise.all(promises).then(res => res.flat()); diff --git a/packages/api-page-builder-import-export/src/import/process/blocks/importBlock.ts b/packages/api-page-builder-import-export/src/import/process/blocks/importBlock.ts index c7d976e9136..4855294f916 100644 --- a/packages/api-page-builder-import-export/src/import/process/blocks/importBlock.ts +++ b/packages/api-page-builder-import-export/src/import/process/blocks/importBlock.ts @@ -77,7 +77,7 @@ export async function importBlock({ if (category) { loadedCategory = await context.pageBuilder.getBlockCategory(category?.slug); if (!loadedCategory) { - await context.pageBuilder.createBlockCategory({ + loadedCategory = await context.pageBuilder.createBlockCategory({ name: category.name, slug: category.slug, icon: category.icon, diff --git a/packages/api-page-builder/src/graphql/crud/pages.crud.ts b/packages/api-page-builder/src/graphql/crud/pages.crud.ts index 7a8e71d8c96..4a1151556d9 100644 --- a/packages/api-page-builder/src/graphql/crud/pages.crud.ts +++ b/packages/api-page-builder/src/graphql/crud/pages.crud.ts @@ -943,10 +943,10 @@ export const createPageCrud = (params: CreatePageCrudParams): PagesCrud => { }); const newPublishedPageRaw = await storageOperations.pages.publish({ - original, - page, - latestPage, - publishedPage, + original: await compressPage(original), + page: await compressPage(page), + latestPage: await compressPage(latestPage), + publishedPage: publishedPage ? await compressPage(publishedPage) : null, publishedPathPage }); diff --git a/packages/api-prerendering-service-so-ddb/src/operations/tenant.ts b/packages/api-prerendering-service-so-ddb/src/operations/tenant.ts index 2f2a2b9b429..8485ee2f770 100644 --- a/packages/api-prerendering-service-so-ddb/src/operations/tenant.ts +++ b/packages/api-prerendering-service-so-ddb/src/operations/tenant.ts @@ -1,19 +1,22 @@ import { PrerenderingServiceTenantStorageOperations } from "@webiny/api-prerendering-service/types"; import { Entity } from "dynamodb-toolbox"; import { queryAll } from "@webiny/db-dynamodb/utils/query"; -import { cleanupItems } from "@webiny/db-dynamodb/utils/cleanup"; export interface CreateTenantStorageOperationsParams { entity: Entity; } +interface Tenant { + data: { id: string }; +} + export const createTenantStorageOperations = ( params: CreateTenantStorageOperationsParams ): PrerenderingServiceTenantStorageOperations => { const { entity } = params; const getTenantIds = async (): Promise => { - const tenants = await queryAll<{ id: string }>({ + const tenants = await queryAll({ entity, partitionKey: "TENANTS", options: { @@ -22,10 +25,8 @@ export const createTenantStorageOperations = ( } }); - return cleanupItems(entity, tenants).map(tenant => tenant.id); + return tenants.map(tenant => tenant.data.id); }; - return { - getTenantIds - }; + return { getTenantIds }; }; diff --git a/packages/app-aco/src/contexts/acoList.tsx b/packages/app-aco/src/contexts/acoList.tsx index 85f2fbf3b83..7272f65d5f8 100644 --- a/packages/app-aco/src/contexts/acoList.tsx +++ b/packages/app-aco/src/contexts/acoList.tsx @@ -102,6 +102,7 @@ const getCurrentRecordList = ( export interface AcoListProviderProps { children: React.ReactNode; own?: boolean; + titleFieldId: string | null; } export const AcoListProvider: React.VFC = ({ children, ...props }) => { @@ -213,7 +214,10 @@ export const AcoListProvider: React.VFC = ({ children, ... */ useEffect(() => { setFolders(prev => { - return sortTableItems(prev, state.listSort); + const titleField = props?.titleFieldId || "id"; + return sortTableItems(prev, state.listSort, { + [titleField]: "title" + }); }); }, [state.listSort]); diff --git a/packages/app-aco/src/contexts/app.tsx b/packages/app-aco/src/contexts/app.tsx index b82cad490b9..bde42bb45db 100644 --- a/packages/app-aco/src/contexts/app.tsx +++ b/packages/app-aco/src/contexts/app.tsx @@ -252,7 +252,7 @@ export const AcoAppProvider: React.VFC = ({ createListLink={createNavigateFolderListLink} createStorageKey={createNavigateFolderStorageKey} > - + {children} diff --git a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx index eac49d4782b..cb6377367b7 100644 --- a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx +++ b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx @@ -242,7 +242,7 @@ const FileManagerView = () => { view.loadMoreFiles(); } }, 200), - [view.meta] + [view.meta, view.loadMoreFiles] ); return ( diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/Table/index.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/Table/index.tsx index 4a9e07b988a..40f07a59959 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntries/Table/index.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/Table/index.tsx @@ -79,8 +79,10 @@ export const Table = forwardRef((props, ref) => { ); const columns: Columns = useMemo(() => { + const titleColumnId = model.titleFieldId || "id"; + return { - title: { + [titleColumnId]: { header: "Name", className: "cms-aco-list-title", cell: (record: Entry) => { diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/refInputs.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/refInputs.tsx index e143b4cf5ec..aeb4670f5be 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/refInputs.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/refInputs.tsx @@ -1,17 +1,18 @@ import React from "react"; -import { CmsContentEntry, CmsModelField, CmsModelFieldRendererPlugin } from "~/types"; +import { CmsModelField, CmsModelFieldRendererPlugin } from "~/types"; import ContentEntriesMultiAutocomplete from "./components/ContentEntriesMultiAutoComplete"; import { i18n } from "@webiny/app/i18n"; -import { BindComponentRenderProp } from "@webiny/form"; const t = i18n.ns("app-headless-cms/admin/fields/ref"); const getKey = ( field: CmsModelField, - bind: BindComponentRenderProp + data?: { + id?: string; + } ): string => { - return bind.form.data.id + "." + field.fieldId; + return (data?.id || "unknown") + "." + field.fieldId; }; const plugin: CmsModelFieldRendererPlugin = { @@ -30,7 +31,7 @@ const plugin: CmsModelFieldRendererPlugin = { {bind => ( diff --git a/packages/app-headless-cms/src/admin/views/contentModels/ContentModelsDataList.tsx b/packages/app-headless-cms/src/admin/views/contentModels/ContentModelsDataList.tsx index ac4c8e65baf..5e8b4817ec7 100644 --- a/packages/app-headless-cms/src/admin/views/contentModels/ContentModelsDataList.tsx +++ b/packages/app-headless-cms/src/admin/views/contentModels/ContentModelsDataList.tsx @@ -302,18 +302,22 @@ const ContentModelsDataList: React.FC = ({ /> )} + + } + label={t`View entries`} + onClick={() => onClone(contentModel)} + /> + )} - - } - label={t`View entries`} - onClick={() => onClone(contentModel)} - /> - - {canDelete(contentModel, "cms.contentModel") && ( <> {contentModel.plugin ? ( diff --git a/packages/db-dynamodb/__tests__/plugins/filters.test.ts b/packages/db-dynamodb/__tests__/plugins/filters.test.ts index 0f0826d6bbd..117d2d1ac9d 100644 --- a/packages/db-dynamodb/__tests__/plugins/filters.test.ts +++ b/packages/db-dynamodb/__tests__/plugins/filters.test.ts @@ -27,7 +27,11 @@ describe("filters", () => { const findFilterPlugin = (operation: string): ValueFilterPlugin => { const byType = plugins.byType(ValueFilterPlugin.type); - return byType.find(plugin => plugin.operation === operation); + const plugin = byType.find(plugin => plugin.operation === operation); + if (plugin) { + return plugin; + } + throw new Error(`Filter plugin "${operation}" not found.`); }; test.each(operations)("has the filter plugin registered - %s", (operation: string) => { @@ -36,14 +40,14 @@ describe("filters", () => { expect(exists).toBe(true); }); - const equalList = [ + const equalList: ([number, number] | [string, string] | [boolean, boolean])[] = [ [1, 1], [932, 932], ["some text", "some text"], [true, true], [false, false] ]; - test.each(equalList)("values should be equal", (value: any, compareValue: any) => { + test.each(equalList)("values should be equal", (value, compareValue) => { const plugin = findFilterPlugin("eq"); const result = plugin.matches({ @@ -54,14 +58,14 @@ describe("filters", () => { expect(result).toBe(true); }); - const notEqualList = [ + const notEqualList: ([number, number] | [string, string] | [boolean, boolean])[] = [ [1, 2], [932, 132], ["some text", "some text 2"], [true, false], [false, true] ]; - test.each(notEqualList)("values should not be equal", (value: any, compareValue: any) => { + test.each(notEqualList)("values should not be equal", (value, compareValue) => { const plugin = findFilterPlugin("eq"); const result = plugin.matches({ @@ -72,14 +76,20 @@ describe("filters", () => { expect(result).toBe(false); }); - const inList = [ + const inList: [ + [number, [number, number, number]], + [number, [number, string, Date]], + [string, [string, number, boolean]], + [boolean, [boolean, boolean, string, number]], + [boolean, [boolean, string, Date]] + ] = [ [1, [1, 2, 3]], [932, [932, "text", new Date()]], ["some text", ["some text", 2, true]], [true, [true, false, "2", 1]], [false, [false, "4", new Date()]] ]; - test.each(inList)("values should be in", (value: any, compareValue: any) => { + test.each(inList)("values should be in", (value, compareValue) => { const plugin = findFilterPlugin("in"); const result = plugin.matches({ @@ -90,14 +100,20 @@ describe("filters", () => { expect(result).toBe(true); }); - const notInList = [ + const notInList: [ + [number, [number, number, number]], + [number, [string, string, Date]], + [string, [string, number, boolean]], + [boolean, [string, boolean, string, number]], + [boolean, [string, string, Date]] + ] = [ [1, [5, 2, 3]], [932, ["932", "text", new Date()]], ["some text", ["some text 2", 2, true]], [true, ["true", false, "2", 1]], [false, ["false", "4", new Date()]] ]; - test.each(notInList)("values should not be in", (value: any, compareValue: any) => { + test.each(notInList)("values should not be in", (value, compareValue) => { const plugin = findFilterPlugin("in"); const result = plugin.matches({ @@ -108,7 +124,7 @@ describe("filters", () => { expect(result).toBe(false); }); - const gtList = [ + const gtList: ([number, number] | [Date, Date])[] = [ [2, 1], [933, 932], [new Date("2021-01-02T23:23:23.000Z"), new Date("2021-01-02T23:23:22.999Z")], @@ -116,7 +132,7 @@ describe("filters", () => { [new Date("2021-01-02T23:24:23.000Z"), new Date("2021-01-02T23:23:23.000Z")], [new Date("2021-01-03T00:23:23.000Z"), new Date("2021-01-02T23:23:23.000Z")] ]; - test.each(gtList)("value should be greater", (value: any, compareValue: any) => { + test.each(gtList)("value should be greater", (value, compareValue) => { const plugin = findFilterPlugin("gt"); const result = plugin.matches({ @@ -127,7 +143,7 @@ describe("filters", () => { expect(result).toBe(true); }); - const notGtList = [ + const notGtList: ([number, number] | [Date, Date])[] = [ [2, 3], [2, 2], [933, 934], @@ -138,7 +154,7 @@ describe("filters", () => { [new Date("2021-01-02T23:24:23.000Z"), new Date("2021-01-03T00:24:23.000Z")], [new Date("2021-01-03T00:23:23.000Z"), new Date("2021-01-04T00:23:23.000Z")] ]; - test.each(notGtList)("value should not be greater", (value: any, compareValue: any) => { + test.each(notGtList)("value should not be greater", (value, compareValue) => { const plugin = findFilterPlugin("gt"); const result = plugin.matches({ @@ -149,7 +165,7 @@ describe("filters", () => { expect(result).toBe(false); }); - const gteList = [ + const gteList: ([number, number] | [Date, Date])[] = [ [2, 1], [2, 2], [933, 932], @@ -163,7 +179,7 @@ describe("filters", () => { [new Date("2021-01-03T00:23:23.000Z"), new Date("2021-01-02T23:23:23.000Z")], [new Date("2021-01-03T23:23:23.000Z"), new Date("2021-01-03T23:23:23.000Z")] ]; - test.each(gteList)("value should be greater or equal", (value: any, compareValue: any) => { + test.each(gteList)("value should be greater or equal", (value, compareValue) => { const plugin = findFilterPlugin("gte"); const result = plugin.matches({ @@ -174,7 +190,7 @@ describe("filters", () => { expect(result).toBe(true); }); - const notGteList = [ + const notGteList: ([number, number] | [Date, Date])[] = [ [2, 3], [933, 934], [new Date("2021-01-02T23:23:23.000Z"), new Date("2021-01-02T23:23:23.001Z")], @@ -182,21 +198,18 @@ describe("filters", () => { [new Date("2021-01-02T23:23:23.000Z"), new Date("2021-01-02T23:24:23.000Z")], [new Date("2021-01-02T23:23:23.000Z"), new Date("2021-01-03T00:23:23.000Z")] ]; - test.each(notGteList)( - "value should not be greater or equal", - (value: any, compareValue: any) => { - const plugin = findFilterPlugin("gte"); + test.each(notGteList)("value should not be greater or equal", (value, compareValue) => { + const plugin = findFilterPlugin("gte"); - const result = plugin.matches({ - value, - compareValue - }); + const result = plugin.matches({ + value, + compareValue + }); - expect(result).toBe(false); - } - ); + expect(result).toBe(false); + }); - const ltList = [ + const ltList: ([number, number] | [Date, Date])[] = [ [2, 3], [933, 934], [new Date("2021-01-02T23:23:23.000Z"), new Date("2021-01-02T23:23:23.001Z")], @@ -204,7 +217,7 @@ describe("filters", () => { [new Date("2021-01-02T23:24:23.000Z"), new Date("2021-01-03T00:25:23.000Z")], [new Date("2021-01-03T00:23:23.000Z"), new Date("2021-01-04T00:23:24.000Z")] ]; - test.each(ltList)("value should be lesser", (value: any, compareValue: any) => { + test.each(ltList)("value should be lesser", (value, compareValue) => { const plugin = findFilterPlugin("lt"); const result = plugin.matches({ @@ -215,7 +228,7 @@ describe("filters", () => { expect(result).toBe(true); }); - const notLtList = [ + const notLtList: ([number, number] | [Date, Date])[] = [ [4, 3], [3, 2], [935, 934], @@ -226,7 +239,7 @@ describe("filters", () => { [new Date("2021-01-02T23:24:23.000Z"), new Date("2021-01-02T23:23:23.000Z")], [new Date("2021-01-03T00:23:23.000Z"), new Date("2021-01-02T23:23:23.000Z")] ]; - test.each(notLtList)("value should not be lesser", (value: any, compareValue: any) => { + test.each(notLtList)("value should not be lesser", (value, compareValue) => { const plugin = findFilterPlugin("lt"); const result = plugin.matches({ @@ -237,7 +250,7 @@ describe("filters", () => { expect(result).toBe(false); }); - const lteList = [ + const lteList: ([number, number] | [Date, Date])[] = [ [2, 3], [2, 2], [933, 934], @@ -251,7 +264,7 @@ describe("filters", () => { [new Date("2021-01-03T00:23:23.000Z"), new Date("2021-01-04T00:23:24.000Z")], [new Date("2021-01-03T00:23:23.000Z"), new Date("2021-01-03T00:23:23.000Z")] ]; - test.each(lteList)("value should be lesser or equal", (value: any, compareValue: any) => { + test.each(lteList)("value should be lesser or equal", (value, compareValue) => { const plugin = findFilterPlugin("lte"); const result = plugin.matches({ @@ -262,32 +275,29 @@ describe("filters", () => { expect(result).toBe(true); }); - const notLteList = [ + const notLteList: ([number, number] | [Date, Date])[] = [ [4, 3], [935, 934], [new Date("2021-01-02T23:23:23.001Z"), new Date("2021-01-02T23:23:23.000Z")], [new Date("2021-01-02T23:23:24.000Z"), new Date("2021-01-02T23:23:23.000Z")], [new Date("2021-01-02T23:24:23.000Z"), new Date("2021-01-02T23:23:23.000Z")] ]; - test.each(notLteList)( - "value should not be lesser or equal", - (value: any, compareValue: any) => { - const plugin = findFilterPlugin("lte"); + test.each(notLteList)("value should not be lesser or equal", (value, compareValue) => { + const plugin = findFilterPlugin("lte"); - const result = plugin.matches({ - value, - compareValue - }); + const result = plugin.matches({ + value, + compareValue + }); - expect(result).toBe(false); - } - ); + expect(result).toBe(false); + }); const containsList = [ ["some text witH description", "wIth"], ["some texT witH description", "text wiTh"] ]; - test.each(containsList)("value should contain", (value: any, compareValue: any) => { + test.each(containsList)("value should contain", (value, compareValue) => { const plugin = findFilterPlugin("contains"); const result = plugin.matches({ @@ -299,10 +309,10 @@ describe("filters", () => { }); const notContainsList = [ - ["Some text wiTh description", "with tExt"], - ["sOme text with description", "with soMe"] + ["Some text wiTh description", "with tExta"], + ["sOme text with description", "with soMeE"] ]; - test.each(notContainsList)("value should not contain", (value: any, compareValue: any) => { + test.each(notContainsList)("value should not contain", (value, compareValue) => { const plugin = findFilterPlugin("contains"); const result = plugin.matches({ @@ -317,7 +327,7 @@ describe("filters", () => { ["some text witH description", "some"], ["some texT witH description", "some text"] ]; - test.each(startsWithList)("value should startsWith", (value: any, compareValue: any) => { + test.each(startsWithList)("value should startsWith", (value, compareValue) => { const plugin = findFilterPlugin("startsWith"); const result = plugin.matches({ @@ -332,7 +342,7 @@ describe("filters", () => { ["Some text wiTh description", "text"], ["sOme text with description", "Ome text"] ]; - test.each(notStartsWith)("value should not startsWith", (value: any, compareValue: any) => { + test.each(notStartsWith)("value should not startsWith", (value, compareValue) => { const plugin = findFilterPlugin("startsWith"); const result = plugin.matches({ @@ -343,7 +353,12 @@ describe("filters", () => { expect(result).toBe(false); }); - const betweenList = [ + const betweenList: [ + [number, [number, number]], + [number, [number, number]], + [Date, [Date, Date]], + [Date, [Date, Date]] + ] = [ [5, [4, 6]], [5, [4, 5]], [ @@ -355,7 +370,7 @@ describe("filters", () => { [new Date("2021-01-02T23:23:22.999Z"), new Date("2021-01-02T23:23:23.001Z")] ] ]; - test.each(betweenList)("values should be in between", (value: any, compareValue: any) => { + test.each(betweenList)("values should be in between", (value, compareValue) => { const plugin = findFilterPlugin("between"); const result = plugin.matches({ @@ -366,7 +381,12 @@ describe("filters", () => { expect(result).toBe(true); }); - const notBetweenList = [ + const notBetweenList: [ + [number, [number, number]], + [number, [number, number]], + [Date, [Date, Date]], + [Date, [Date, Date]] + ] = [ [3, [4, 6]], [8, [4, 7]], [ @@ -378,19 +398,16 @@ describe("filters", () => { [new Date("2021-01-02T23:23:22.999Z"), new Date("2021-01-02T23:23:23.001Z")] ] ]; - test.each(notBetweenList)( - "values should not be in between", - (value: any, compareValue: any) => { - const plugin = findFilterPlugin("between"); + test.each(notBetweenList)("values should not be in between", (value, compareValue) => { + const plugin = findFilterPlugin("between"); - const result = plugin.matches({ - value, - compareValue - }); + const result = plugin.matches({ + value, + compareValue + }); - expect(result).toBe(false); - } - ); + expect(result).toBe(false); + }); test("target value should contain all required values", () => { const plugin = findFilterPlugin("and_in"); @@ -414,7 +431,7 @@ describe("filters", () => { expect(result).toBe(false); }); - const fuzzySearchList = [ + const fuzzySearchList: [string, string, boolean][] = [ ["Crafting a good page title for SEO", "why go serverless", false], ["What is Serverless and is it worth it?", "why go serverless", true], ["Why should you go Serverless today?", "why go serverless", true], @@ -423,7 +440,7 @@ describe("filters", () => { test.each(fuzzySearchList)( `should perform fuzzy search on "%s"`, - (value: string, compareValue: string, expected: boolean) => { + (value, compareValue, expected) => { const plugin = findFilterPlugin("fuzzy"); const result = plugin.matches({ diff --git a/packages/db-dynamodb/src/plugins/filters/contains.ts b/packages/db-dynamodb/src/plugins/filters/contains.ts index 27d368f2d3b..64f34cfc112 100644 --- a/packages/db-dynamodb/src/plugins/filters/contains.ts +++ b/packages/db-dynamodb/src/plugins/filters/contains.ts @@ -1,19 +1,39 @@ import { ValueFilterPlugin } from "../definitions/ValueFilterPlugin"; +const createValues = (initialValue: string | string[]) => { + return Array.isArray(initialValue) ? initialValue : [initialValue]; +}; + +const createCompareValues = (value: string) => { + return value + .replace(/\s+/g, " ") + .trim() + .replace(/\?/g, `\\?`) + .replace(/\//g, `\\/`) + .replace(/:/g, ``) + .replace(/\-/g, `\\-`) + .split(" ") + .filter(val => { + return val.length > 0; + }); +}; + const plugin = new ValueFilterPlugin({ operation: "contains", - matches: ({ value, compareValue }) => { - if (typeof value !== "string") { - if (Array.isArray(value) === true) { - const re = new RegExp(compareValue, "i"); - return value.some((v: string) => { - return v.match(re) !== null; - }); - } + matches: ({ value: initialValue, compareValue: initialCompareValue }) => { + if (!initialValue || (Array.isArray(initialValue) && initialValue.length === 0)) { return false; + } else if (initialCompareValue === undefined || initialCompareValue === null) { + return true; } - const re = new RegExp(compareValue, "i"); - return value.match(re) !== null; + const values = createValues(initialValue); + const compareValues = createCompareValues(initialCompareValue); + return values.some(target => { + // return target.match(compareValues) !== null; + return compareValues.every(compareValue => { + return target.match(new RegExp(compareValue, "gi")) !== null; + }); + }); } }); diff --git a/packages/db-dynamodb/src/plugins/filters/fuzzy.ts b/packages/db-dynamodb/src/plugins/filters/fuzzy.ts index f26f29ae3d2..8517853625e 100644 --- a/packages/db-dynamodb/src/plugins/filters/fuzzy.ts +++ b/packages/db-dynamodb/src/plugins/filters/fuzzy.ts @@ -16,7 +16,8 @@ const plugin = new ValueFilterPlugin({ const f = new Fuse([value], { includeScore: true, minMatchCharLength: 3, - threshold: 0.6 + threshold: 0.6, + isCaseSensitive: false }); const result = f.search(compareValue); diff --git a/packages/migrations/src/migrations/5.37.0/002/ddb-es/index.ts b/packages/migrations/src/migrations/5.37.0/002/ddb-es/index.ts index 8c2ee96c83c..2d19060aac2 100644 --- a/packages/migrations/src/migrations/5.37.0/002/ddb-es/index.ts +++ b/packages/migrations/src/migrations/5.37.0/002/ddb-es/index.ts @@ -285,8 +285,9 @@ export class CmsEntriesRootFolder_5_37_0_002 logger.trace("Storing the DynamoDB records..."); await executeWithRetry(execute, { onFailedAttempt: error => { - logger.error(`"batchWriteAll" attempt #${error.attemptNumber} failed.`); - logger.error(error.message); + logger.error( + `"batchWriteAll" attempt #${error.attemptNumber} failed: ${error.message}` + ); } }); logger.trace("...stored."); @@ -295,9 +296,8 @@ export class CmsEntriesRootFolder_5_37_0_002 await executeWithRetry(executeDdbEs, { onFailedAttempt: error => { logger.error( - `"batchWriteAll ddb + es" attempt #${error.attemptNumber} failed.` + `"batchWriteAll ddb + es" attempt #${error.attemptNumber} failed: ${error.message}` ); - logger.error(error.message); } }); logger.trace("...stored."); @@ -343,12 +343,12 @@ export class CmsEntriesRootFolder_5_37_0_002 }; } catch (ex) { logger.error(`Failed to fetch original Elasticsearch settings for index "${index}".`); - logger.error(ex.message); - logger.info(ex.code); - logger.info(JSON.stringify(ex.data)); - if (ex.stack) { - logger.info(ex.stack); - } + logger.error({ + ...ex, + message: ex.message, + code: ex.code, + data: ex.data + }); } return null; } @@ -379,12 +379,12 @@ export class CmsEntriesRootFolder_5_37_0_002 logger.error( `Failed to restore original settings for index "${index}". Please do it manually.` ); - logger.error(ex.message); - logger.info(ex.code); - logger.info(JSON.stringify(ex.data)); - if (ex.stack) { - logger.info(ex.stack); - } + logger.error({ + ...ex, + message: ex.message, + code: ex.code, + data: ex.data + }); } } } @@ -405,12 +405,12 @@ export class CmsEntriesRootFolder_5_37_0_002 }); } catch (ex) { logger.error(`Failed to disable indexing for index "${index}".`); - logger.error(ex.message); - logger.info(ex.code); - logger.info(JSON.stringify(ex.data)); - if (ex.stack) { - logger.info(ex.stack); - } + logger.error({ + ...ex, + message: ex.message, + code: ex.code, + data: ex.data + }); } } } diff --git a/packages/pulumi/package.json b/packages/pulumi/package.json index 49c1450c215..e4d89316f65 100644 --- a/packages/pulumi/package.json +++ b/packages/pulumi/package.json @@ -15,7 +15,8 @@ }, "dependencies": { "@pulumi/pulumi": "^3.34.0", - "find-up": "^5.0.0" + "find-up": "^5.0.0", + "lodash": "^4.17.21" }, "devDependencies": { "@babel/cli": "^7.22.6", diff --git a/packages/pulumi/src/PulumiAppResource.ts b/packages/pulumi/src/PulumiAppResource.ts index 9dea95304d4..fc3d6e7af24 100644 --- a/packages/pulumi/src/PulumiAppResource.ts +++ b/packages/pulumi/src/PulumiAppResource.ts @@ -28,6 +28,8 @@ export interface PulumiAppResourceConfigSetter { export type PulumiAppResourceConfigProxy = { readonly [K in keyof T]-?: PulumiAppResourceConfigSetter; +} & { + clone(): T; }; export interface PulumiAppResource { diff --git a/packages/pulumi/src/createPulumiApp.ts b/packages/pulumi/src/createPulumiApp.ts index c2b1e591795..e5777b0b6f6 100644 --- a/packages/pulumi/src/createPulumiApp.ts +++ b/packages/pulumi/src/createPulumiApp.ts @@ -20,6 +20,7 @@ import { ResourceHandler } from "~/types"; import { PulumiAppRemoteResource } from "~/PulumiAppRemoteResource"; +import cloneDeep from "lodash/cloneDeep"; export function createPulumiApp>( params: CreatePulumiAppParams @@ -219,6 +220,10 @@ export function createPulumiApp>( function createPulumiAppResourceConfigProxy(obj: T) { return new Proxy(obj, { get(target, p: string) { + if (p === "clone") { + return () => cloneDeep(obj); + } + type V = T[keyof T]; const key = p as keyof T; const setter: PulumiAppResourceConfigSetter = ( diff --git a/scripts/updateDeployTemplateInS3.js b/scripts/updateDeployTemplateInS3.js new file mode 100644 index 00000000000..7907f2d7a9b --- /dev/null +++ b/scripts/updateDeployTemplateInS3.js @@ -0,0 +1,34 @@ +const S3 = require("aws-sdk/clients/s3"); +const yargs = require("yargs"); +const fs = require("fs"); +const path = require("path"); + +const args = yargs.argv; + +if (!args.source) { + console.error(`Please specify a "--source" parameter!`); + process.exit(1); +} + +(async () => { + const s3 = new S3({ region: process.env["AWS_REGION"] ?? "us-east-1" }); + const templateKey = "cloudformation/DEPLOY_WEBINY_PROJECT_CF_TEMPLATE.yaml"; + + const fileSource = path.resolve(args.source); + + console.log(`Updating key: ${templateKey}`); + console.log(`Source file: ${fileSource}`); + const newBody = fs.readFileSync(fileSource, "utf8"); + + const bucket = "webiny-public"; + const config = { Bucket: bucket, Key: templateKey, Body: newBody, ACL: "public-read" }; + + console.log(`Uploading to "${bucket}" bucket...`); + try { + await s3.putObject(config).promise(); + console.log(`\nSUCCESS: File was updated!`); + } catch (err) { + console.error(`\nERROR: ${err.message}`); + process.exit(1); + } +})(); diff --git a/yarn.lock b/yarn.lock index 126c451aac1..78d94d8272a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16520,6 +16520,7 @@ __metadata: "@webiny/cli": 0.0.0 "@webiny/project-utils": 0.0.0 find-up: ^5.0.0 + lodash: ^4.17.21 rimraf: ^3.0.2 typescript: 4.7.4 languageName: unknown