From 705277f7629b60c5960641646ffc16ccf1743bba Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Fri, 12 Apr 2024 10:35:55 +0200 Subject: [PATCH] feat(api-headless-cms): add crud operation to restore entry from trash bin (#4064) --- .github/workflows/pullRequests.yml | 9 +- .github/workflows/pushDev.yml | 9 +- .github/workflows/pushNext.yml | 9 +- .../wac/utils/listPackagesWithJestTests.ts | 9 +- apps/api/graphql/package.json | 1 + apps/api/graphql/src/index.ts | 4 +- apps/api/graphql/src/types.ts | 4 +- apps/api/graphql/tsconfig.json | 5 + .../__tests__/snapshots/customAppsSchema.ts | 26 ++ .../__tests__/snapshots/defaultAppsSchema.ts | 26 ++ .../__tests__/mocks/file.sdl.ts | 26 ++ packages/api-headless-cms-aco/.babelrc.js | 1 + packages/api-headless-cms-aco/LICENSE | 21 + packages/api-headless-cms-aco/README.md | 18 + .../__tests__/entries.hooks.test.ts | 215 +++++++++ .../__tests__/graphql/cms.gql.ts | 97 ++++ .../__tests__/graphql/folder.gql.ts | 53 +++ .../__tests__/mocks/lifecycle.mock.ts | 16 + .../__tests__/utils/createTestWcpLicense.ts | 44 ++ .../__tests__/utils/tenancySecurity.ts | 61 +++ .../__tests__/utils/useGraphQlHandler.ts | 231 ++++++++++ packages/api-headless-cms-aco/jest.setup.js | 13 + packages/api-headless-cms-aco/package.json | 52 +++ .../src/hooks/entry/index.ts | 9 + .../entry/onEntryBeforeRestoreFromBin.hook.ts | 37 ++ packages/api-headless-cms-aco/src/index.ts | 17 + packages/api-headless-cms-aco/src/types.ts | 5 + .../api-headless-cms-aco/tsconfig.build.json | 27 ++ packages/api-headless-cms-aco/tsconfig.json | 58 +++ .../api-headless-cms-aco/webiny.config.js | 8 + .../src/definitions/entry.ts | 7 + .../operations/entry/elasticsearch/fields.ts | 14 + .../src/operations/entry/index.ts | 198 +++++++- .../entry/filtering/createFields.test.ts | 50 ++ .../src/definitions/entry.ts | 7 + .../src/operations/entry/index.ts | 110 ++++- .../contentAPI/contentEntry.delete.test.ts | 103 ++++- .../contentAPI/contentEntry.restore.test.ts | 435 ++++++++++++++++++ .../__tests__/contentAPI/filtering.test.ts | 2 +- .../contentAPI/snapshots/category.manage.ts | 38 ++ .../contentAPI/snapshots/category.read.ts | 30 ++ .../contentAPI/snapshots/page.manage.ts | 36 ++ .../contentAPI/snapshots/page.read.ts | 30 ++ .../contentAPI/snapshots/product.manage.ts | 36 ++ .../contentAPI/snapshots/product.read.ts | 30 ++ .../contentAPI/snapshots/review.manage.ts | 36 ++ .../contentAPI/snapshots/review.read.ts | 30 ++ .../__tests__/contentAPI/sorting.test.ts | 2 +- .../__tests__/helpers/renderSortEnum.test.ts | 8 + .../testHelpers/useCategoryManageHandler.ts | 24 + packages/api-headless-cms/src/constants.ts | 24 +- .../src/crud/contentEntry.crud.ts | 58 ++- .../abstractions/IRestoreEntryFromBin.ts | 5 + .../IRestoreEntryFromBinOperation.ts | 8 + .../crud/contentEntry/abstractions/index.ts | 2 + .../entryDataFactories/createEntryData.ts | 4 + .../createUpdateEntryData.ts | 4 + .../DeleteEntry/TransformEntryMoveToBin.ts | 9 + .../GetLatestRevisionByEntryIdDeleted.ts | 20 + .../GetLatestRevisionByEntryId/index.ts | 7 +- .../RestoreEntryFromBin.ts | 38 ++ .../RestoreEntryFromBinOperation.ts | 18 + .../RestoreEntryFromBinOperationWithEvents.ts | 51 ++ .../RestoreEntryFromBinSecure.ts | 18 + .../TransformEntryRestoreFromBin.ts | 68 +++ .../useCases/RestoreEntryFromBin/index.ts | 50 ++ .../src/crud/contentEntry/useCases/index.ts | 1 + .../graphql/schema/createManageResolvers.ts | 2 + .../src/graphql/schema/createManageSDL.ts | 2 + .../resolvers/manage/resolveRestoreFromBin.ts | 19 + .../api-headless-cms/src/types/context.ts | 11 + packages/api-headless-cms/src/types/types.ts | 87 +++- .../src/entries.graphql.ts | 12 +- .../TrashBinRestoreItemGraphQLGateway.ts | 14 +- .../ddb-es/apps/api/graphql/package.json | 1 + .../ddb-es/apps/api/graphql/src/index.ts | 2 + .../ddb-es/apps/api/graphql/src/types.ts | 2 + .../ddb-os/apps/api/graphql/package.json | 1 + .../ddb-os/apps/api/graphql/src/index.ts | 2 + .../ddb-os/apps/api/graphql/src/types.ts | 2 + .../ddb/apps/api/graphql/package.json | 1 + .../ddb/apps/api/graphql/src/index.ts | 2 + .../ddb/apps/api/graphql/src/types.ts | 2 + scripts/listPackagesWithTests.js | 6 + yarn.lock | 32 ++ 85 files changed, 2871 insertions(+), 51 deletions(-) create mode 100644 packages/api-headless-cms-aco/.babelrc.js create mode 100644 packages/api-headless-cms-aco/LICENSE create mode 100644 packages/api-headless-cms-aco/README.md create mode 100644 packages/api-headless-cms-aco/__tests__/entries.hooks.test.ts create mode 100644 packages/api-headless-cms-aco/__tests__/graphql/cms.gql.ts create mode 100644 packages/api-headless-cms-aco/__tests__/graphql/folder.gql.ts create mode 100644 packages/api-headless-cms-aco/__tests__/mocks/lifecycle.mock.ts create mode 100644 packages/api-headless-cms-aco/__tests__/utils/createTestWcpLicense.ts create mode 100644 packages/api-headless-cms-aco/__tests__/utils/tenancySecurity.ts create mode 100644 packages/api-headless-cms-aco/__tests__/utils/useGraphQlHandler.ts create mode 100644 packages/api-headless-cms-aco/jest.setup.js create mode 100644 packages/api-headless-cms-aco/package.json create mode 100644 packages/api-headless-cms-aco/src/hooks/entry/index.ts create mode 100644 packages/api-headless-cms-aco/src/hooks/entry/onEntryBeforeRestoreFromBin.hook.ts create mode 100644 packages/api-headless-cms-aco/src/index.ts create mode 100644 packages/api-headless-cms-aco/src/types.ts create mode 100644 packages/api-headless-cms-aco/tsconfig.build.json create mode 100644 packages/api-headless-cms-aco/tsconfig.json create mode 100644 packages/api-headless-cms-aco/webiny.config.js create mode 100644 packages/api-headless-cms/__tests__/contentAPI/contentEntry.restore.test.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBin.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBinOperation.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/GetLatestRevisionByEntryIdDeleted.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBin.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperation.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperationWithEvents.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinSecure.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/TransformEntryRestoreFromBin.ts create mode 100644 packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/index.ts create mode 100644 packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveRestoreFromBin.ts diff --git a/.github/workflows/pullRequests.yml b/.github/workflows/pullRequests.yml index 6d4c362b12f..3a7ddb9d0bf 100644 --- a/.github/workflows/pullRequests.yml +++ b/.github/workflows/pullRequests.yml @@ -226,7 +226,8 @@ jobs: --storage=ddb","storage":"ddb","id":"api-audit-logs_ddb"},{"cmd":"packages/api-file-manager --storage=ddb","storage":"ddb","id":"api-file-manager_ddb"},{"cmd":"packages/api-form-builder --storage=ddb","storage":"ddb","id":"api-form-builder_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb","storage":"ddb","id":"api-headless-cms_ddb"},{"cmd":"packages/api-i18n + --storage=ddb","storage":"ddb","id":"api-headless-cms_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb","storage":"ddb","id":"api-headless-cms-aco_ddb"},{"cmd":"packages/api-i18n --storage=ddb","storage":"ddb","id":"api-i18n_ddb"},{"cmd":"packages/api-mailer --storage=ddb","storage":"ddb","id":"api-mailer_ddb"},{"cmd":"packages/api-page-builder --storage=ddb","storage":"ddb","id":"api-page-builder_ddb"},{"cmd":"packages/api-page-builder-aco @@ -286,7 +287,8 @@ jobs: --storage=ddb-es,ddb","storage":"ddb-es","id":"api-file-manager_ddb-es_ddb"},{"cmd":"packages/api-form-builder --storage=ddb-es,ddb","storage":"ddb-es","id":"api-form-builder_ddb-es_ddb"},{"cmd":"packages/api-form-builder-so-ddb-es --storage=ddb-es,ddb","storage":"ddb-es","id":"api-form-builder-so-ddb-es_ddb-es_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-ddb-es + --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms-aco_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-ddb-es --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms-ddb-es_ddb-es_ddb"},{"cmd":"packages/api-mailer --storage=ddb-es,ddb","storage":"ddb-es","id":"api-mailer_ddb-es_ddb"},{"cmd":"packages/api-page-builder --storage=ddb-es,ddb","storage":"ddb-es","id":"api-page-builder_ddb-es_ddb"},{"cmd":"packages/api-page-builder-aco @@ -353,7 +355,8 @@ jobs: --storage=ddb-os,ddb","storage":"ddb-os","id":"api-file-manager_ddb-os_ddb"},{"cmd":"packages/api-form-builder --storage=ddb-os,ddb","storage":"ddb-os","id":"api-form-builder_ddb-os_ddb"},{"cmd":"packages/api-form-builder-so-ddb-es --storage=ddb-os,ddb","storage":"ddb-os","id":"api-form-builder-so-ddb-es_ddb-os_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-ddb-es + --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms-aco_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-ddb-es --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms-ddb-es_ddb-os_ddb"},{"cmd":"packages/api-mailer --storage=ddb-os,ddb","storage":"ddb-os","id":"api-mailer_ddb-os_ddb"},{"cmd":"packages/api-page-builder --storage=ddb-os,ddb","storage":"ddb-os","id":"api-page-builder_ddb-os_ddb"},{"cmd":"packages/api-page-builder-aco diff --git a/.github/workflows/pushDev.yml b/.github/workflows/pushDev.yml index 5827a144334..eb83c508f24 100644 --- a/.github/workflows/pushDev.yml +++ b/.github/workflows/pushDev.yml @@ -192,7 +192,8 @@ jobs: --storage=ddb","storage":"ddb","id":"api-audit-logs_ddb"},{"cmd":"packages/api-file-manager --storage=ddb","storage":"ddb","id":"api-file-manager_ddb"},{"cmd":"packages/api-form-builder --storage=ddb","storage":"ddb","id":"api-form-builder_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb","storage":"ddb","id":"api-headless-cms_ddb"},{"cmd":"packages/api-i18n + --storage=ddb","storage":"ddb","id":"api-headless-cms_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb","storage":"ddb","id":"api-headless-cms-aco_ddb"},{"cmd":"packages/api-i18n --storage=ddb","storage":"ddb","id":"api-i18n_ddb"},{"cmd":"packages/api-mailer --storage=ddb","storage":"ddb","id":"api-mailer_ddb"},{"cmd":"packages/api-page-builder --storage=ddb","storage":"ddb","id":"api-page-builder_ddb"},{"cmd":"packages/api-page-builder-aco @@ -252,7 +253,8 @@ jobs: --storage=ddb-es,ddb","storage":"ddb-es","id":"api-file-manager_ddb-es_ddb"},{"cmd":"packages/api-form-builder --storage=ddb-es,ddb","storage":"ddb-es","id":"api-form-builder_ddb-es_ddb"},{"cmd":"packages/api-form-builder-so-ddb-es --storage=ddb-es,ddb","storage":"ddb-es","id":"api-form-builder-so-ddb-es_ddb-es_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-ddb-es + --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms-aco_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-ddb-es --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms-ddb-es_ddb-es_ddb"},{"cmd":"packages/api-mailer --storage=ddb-es,ddb","storage":"ddb-es","id":"api-mailer_ddb-es_ddb"},{"cmd":"packages/api-page-builder --storage=ddb-es,ddb","storage":"ddb-es","id":"api-page-builder_ddb-es_ddb"},{"cmd":"packages/api-page-builder-aco @@ -318,7 +320,8 @@ jobs: --storage=ddb-os,ddb","storage":"ddb-os","id":"api-file-manager_ddb-os_ddb"},{"cmd":"packages/api-form-builder --storage=ddb-os,ddb","storage":"ddb-os","id":"api-form-builder_ddb-os_ddb"},{"cmd":"packages/api-form-builder-so-ddb-es --storage=ddb-os,ddb","storage":"ddb-os","id":"api-form-builder-so-ddb-es_ddb-os_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-ddb-es + --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms-aco_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-ddb-es --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms-ddb-es_ddb-os_ddb"},{"cmd":"packages/api-mailer --storage=ddb-os,ddb","storage":"ddb-os","id":"api-mailer_ddb-os_ddb"},{"cmd":"packages/api-page-builder --storage=ddb-os,ddb","storage":"ddb-os","id":"api-page-builder_ddb-os_ddb"},{"cmd":"packages/api-page-builder-aco diff --git a/.github/workflows/pushNext.yml b/.github/workflows/pushNext.yml index 251ed589291..a194415c365 100644 --- a/.github/workflows/pushNext.yml +++ b/.github/workflows/pushNext.yml @@ -192,7 +192,8 @@ jobs: --storage=ddb","storage":"ddb","id":"api-audit-logs_ddb"},{"cmd":"packages/api-file-manager --storage=ddb","storage":"ddb","id":"api-file-manager_ddb"},{"cmd":"packages/api-form-builder --storage=ddb","storage":"ddb","id":"api-form-builder_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb","storage":"ddb","id":"api-headless-cms_ddb"},{"cmd":"packages/api-i18n + --storage=ddb","storage":"ddb","id":"api-headless-cms_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb","storage":"ddb","id":"api-headless-cms-aco_ddb"},{"cmd":"packages/api-i18n --storage=ddb","storage":"ddb","id":"api-i18n_ddb"},{"cmd":"packages/api-mailer --storage=ddb","storage":"ddb","id":"api-mailer_ddb"},{"cmd":"packages/api-page-builder --storage=ddb","storage":"ddb","id":"api-page-builder_ddb"},{"cmd":"packages/api-page-builder-aco @@ -252,7 +253,8 @@ jobs: --storage=ddb-es,ddb","storage":"ddb-es","id":"api-file-manager_ddb-es_ddb"},{"cmd":"packages/api-form-builder --storage=ddb-es,ddb","storage":"ddb-es","id":"api-form-builder_ddb-es_ddb"},{"cmd":"packages/api-form-builder-so-ddb-es --storage=ddb-es,ddb","storage":"ddb-es","id":"api-form-builder-so-ddb-es_ddb-es_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-ddb-es + --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms-aco_ddb-es_ddb"},{"cmd":"packages/api-headless-cms-ddb-es --storage=ddb-es,ddb","storage":"ddb-es","id":"api-headless-cms-ddb-es_ddb-es_ddb"},{"cmd":"packages/api-mailer --storage=ddb-es,ddb","storage":"ddb-es","id":"api-mailer_ddb-es_ddb"},{"cmd":"packages/api-page-builder --storage=ddb-es,ddb","storage":"ddb-es","id":"api-page-builder_ddb-es_ddb"},{"cmd":"packages/api-page-builder-aco @@ -318,7 +320,8 @@ jobs: --storage=ddb-os,ddb","storage":"ddb-os","id":"api-file-manager_ddb-os_ddb"},{"cmd":"packages/api-form-builder --storage=ddb-os,ddb","storage":"ddb-os","id":"api-form-builder_ddb-os_ddb"},{"cmd":"packages/api-form-builder-so-ddb-es --storage=ddb-os,ddb","storage":"ddb-os","id":"api-form-builder-so-ddb-es_ddb-os_ddb"},{"cmd":"packages/api-headless-cms - --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-ddb-es + --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-aco + --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms-aco_ddb-os_ddb"},{"cmd":"packages/api-headless-cms-ddb-es --storage=ddb-os,ddb","storage":"ddb-os","id":"api-headless-cms-ddb-es_ddb-os_ddb"},{"cmd":"packages/api-mailer --storage=ddb-os,ddb","storage":"ddb-os","id":"api-mailer_ddb-os_ddb"},{"cmd":"packages/api-page-builder --storage=ddb-os,ddb","storage":"ddb-os","id":"api-page-builder_ddb-os_ddb"},{"cmd":"packages/api-page-builder-aco diff --git a/.github/workflows/wac/utils/listPackagesWithJestTests.ts b/.github/workflows/wac/utils/listPackagesWithJestTests.ts index 325d862c6b4..e8da1ff66f6 100644 --- a/.github/workflows/wac/utils/listPackagesWithJestTests.ts +++ b/.github/workflows/wac/utils/listPackagesWithJestTests.ts @@ -142,6 +142,13 @@ const CUSTOM_HANDLERS: Record Array> = { } ]; }, + "api-headless-cms-aco": () => { + return [ + { cmd: "packages/api-headless-cms-aco --storage=ddb", storage: "ddb" }, + { cmd: "packages/api-headless-cms-aco --storage=ddb-es,ddb", storage: "ddb-es" }, + { cmd: "packages/api-headless-cms-aco --storage=ddb-os,ddb", storage: "ddb-os" } + ]; + }, "api-apw": () => { return [ { cmd: "packages/api-apw --storage=ddb", storage: "ddb" } @@ -250,7 +257,7 @@ function hasTestFiles(packageFolderPath: string) { } const files = fs.readdirSync(packageFolderPath); - for (let filename of files) { + for (const filename of files) { const filepath = path.join(packageFolderPath, filename); if (fs.statSync(filepath).isDirectory()) { const hasTFiles = hasTestFiles(filepath); diff --git a/apps/api/graphql/package.json b/apps/api/graphql/package.json index 5d829153eb7..1524079762c 100644 --- a/apps/api/graphql/package.json +++ b/apps/api/graphql/package.json @@ -19,6 +19,7 @@ "@webiny/api-form-builder": "0.0.0", "@webiny/api-form-builder-so-ddb": "0.0.0", "@webiny/api-headless-cms": "0.0.0", + "@webiny/api-headless-cms-aco": "0.0.0", "@webiny/api-headless-cms-ddb": "0.0.0", "@webiny/api-i18n": "0.0.0", "@webiny/api-i18n-content": "0.0.0", diff --git a/apps/api/graphql/src/index.ts b/apps/api/graphql/src/index.ts index fbdbc286faa..794f0426d75 100644 --- a/apps/api/graphql/src/index.ts +++ b/apps/api/graphql/src/index.ts @@ -27,6 +27,7 @@ import fileManagerS3, { createAssetDelivery } from "@webiny/api-file-manager-s3" import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; +import { createAcoHcmsContext } from "@webiny/api-headless-cms-aco"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; import securityPlugins from "./security"; import tenantManager from "@webiny/api-tenant-manager"; @@ -128,7 +129,8 @@ export const handler = createHandler({ }), createAuditLogs(), createCountDynamoDbTask(), - createContinuingTask() + createContinuingTask(), + createAcoHcmsContext() ], debug }); diff --git a/apps/api/graphql/src/types.ts b/apps/api/graphql/src/types.ts index e4d9455dbde..bcb3d40a9cf 100644 --- a/apps/api/graphql/src/types.ts +++ b/apps/api/graphql/src/types.ts @@ -8,6 +8,7 @@ import { CmsContext } from "@webiny/api-headless-cms/types"; import { AcoContext } from "@webiny/api-aco/types"; import { PbAcoContext } from "@webiny/api-page-builder-aco/types"; import { Context as TasksContext } from "@webiny/tasks/types"; +import { HcmsAcoContext } from "@webiny/api-headless-cms-aco/types"; // When working with the `context` object (for example while defining a new GraphQL resolver function), // you can import this interface and assign it to it. This will give you full autocomplete functionality @@ -25,4 +26,5 @@ export interface Context FormBuilderContext, AcoContext, TasksContext, - PbAcoContext {} + PbAcoContext, + HcmsAcoContext {} diff --git a/apps/api/graphql/tsconfig.json b/apps/api/graphql/tsconfig.json index 4a9ba5ff956..1f20df996a2 100644 --- a/apps/api/graphql/tsconfig.json +++ b/apps/api/graphql/tsconfig.json @@ -38,6 +38,9 @@ { "path": "../../../packages/api-headless-cms/tsconfig.build.json" }, + { + "path": "../../../packages/api-headless-cms-aco/tsconfig.build.json" + }, { "path": "../../../packages/api-headless-cms-ddb/tsconfig.build.json" }, @@ -143,6 +146,8 @@ "@webiny/api-form-builder-so-ddb": ["../../../packages/api-form-builder-so-ddb/src"], "@webiny/api-headless-cms/*": ["../../../packages/api-headless-cms/src/*"], "@webiny/api-headless-cms": ["../../../packages/api-headless-cms/src"], + "@webiny/api-headless-cms-aco/*": ["../../../packages/api-headless-cms-aco/src/*"], + "@webiny/api-headless-cms-aco": ["../../../packages/api-headless-cms-aco/src"], "@webiny/api-headless-cms-ddb/*": ["../../../packages/api-headless-cms-ddb/src/*"], "@webiny/api-headless-cms-ddb": ["../../../packages/api-headless-cms-ddb/src"], "@webiny/api-i18n/*": ["../../../packages/api-i18n/src/*"], diff --git a/packages/api-aco/__tests__/snapshots/customAppsSchema.ts b/packages/api-aco/__tests__/snapshots/customAppsSchema.ts index 8a3a72736f3..9d3e4b28b60 100644 --- a/packages/api-aco/__tests__/snapshots/customAppsSchema.ts +++ b/packages/api-aco/__tests__/snapshots/customAppsSchema.ts @@ -216,6 +216,13 @@ export const createCustomAppsSchemaSnapshot = () => { deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -246,6 +253,10 @@ export const createCustomAppsSchemaSnapshot = () => { deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -282,6 +293,13 @@ export const createCustomAppsSchemaSnapshot = () => { revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -312,6 +330,10 @@ export const createCustomAppsSchemaSnapshot = () => { revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -385,6 +407,8 @@ export const createCustomAppsSchemaSnapshot = () => { savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -397,6 +421,8 @@ export const createCustomAppsSchemaSnapshot = () => { revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC diff --git a/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts b/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts index bbdf5aa3af4..17272ed66bd 100644 --- a/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts +++ b/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts @@ -190,6 +190,13 @@ export const createDefaultAppsSchemaSnapshot = () => { deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -220,6 +227,10 @@ export const createDefaultAppsSchemaSnapshot = () => { deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -256,6 +267,13 @@ export const createDefaultAppsSchemaSnapshot = () => { revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -286,6 +304,10 @@ export const createDefaultAppsSchemaSnapshot = () => { revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -359,6 +381,8 @@ export const createDefaultAppsSchemaSnapshot = () => { savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -371,6 +395,8 @@ export const createDefaultAppsSchemaSnapshot = () => { revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC diff --git a/packages/api-file-manager/__tests__/mocks/file.sdl.ts b/packages/api-file-manager/__tests__/mocks/file.sdl.ts index 0588cba100a..bcde474f696 100644 --- a/packages/api-file-manager/__tests__/mocks/file.sdl.ts +++ b/packages/api-file-manager/__tests__/mocks/file.sdl.ts @@ -211,6 +211,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -241,6 +248,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -277,6 +288,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -307,6 +325,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -399,6 +421,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -411,6 +435,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC diff --git a/packages/api-headless-cms-aco/.babelrc.js b/packages/api-headless-cms-aco/.babelrc.js new file mode 100644 index 00000000000..9da7674cb52 --- /dev/null +++ b/packages/api-headless-cms-aco/.babelrc.js @@ -0,0 +1 @@ +module.exports = require("@webiny/project-utils").createBabelConfigForNode({ path: __dirname }); diff --git a/packages/api-headless-cms-aco/LICENSE b/packages/api-headless-cms-aco/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/api-headless-cms-aco/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Webiny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/api-headless-cms-aco/README.md b/packages/api-headless-cms-aco/README.md new file mode 100644 index 00000000000..02afe6e5b00 --- /dev/null +++ b/packages/api-headless-cms-aco/README.md @@ -0,0 +1,18 @@ +# @webiny/api-headless-cms-aco + +[![](https://img.shields.io/npm/dw/@webiny/api-headless-cms-aco.svg)](https://www.npmjs.com/package/@webiny/api-headless-cms-aco) +[![](https://img.shields.io/npm/v/@webiny/api-headless-cms-aco.svg)](https://www.npmjs.com/package/@webiny/api-headless-cms-aco) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + +## Install + +``` +npm install --save @webiny/api-headless-cms-aco +``` + +Or if you prefer yarn: + +``` +yarn add @webiny/api-headless-cms-aco +``` \ No newline at end of file diff --git a/packages/api-headless-cms-aco/__tests__/entries.hooks.test.ts b/packages/api-headless-cms-aco/__tests__/entries.hooks.test.ts new file mode 100644 index 00000000000..13395af7da5 --- /dev/null +++ b/packages/api-headless-cms-aco/__tests__/entries.hooks.test.ts @@ -0,0 +1,215 @@ +import { useGraphQlHandler } from "./utils/useGraphQlHandler"; +import { assignCmsLifecycleEvents, tracker } from "./mocks/lifecycle.mock"; +import { ROOT_FOLDER } from "@webiny/api-headless-cms/constants"; + +describe("HCMS Entries -> onEntryBeforeRestoreFromBin", () => { + beforeEach(async () => { + tracker.reset(); + }); + + it("should have HCMS and ACO GraphQL Query and Mutation", async () => { + const { introspect } = useGraphQlHandler({}); + const [result] = await introspect(); + expect(result).not.toBeNull(); + }); + + it(`should not change the location if folderId is ${ROOT_FOLDER}`, async () => { + const { cms } = useGraphQlHandler({ + plugins: [assignCmsLifecycleEvents()] + }); + + // Let's create the model group and model + const modelGroup = await cms.createTestModelGroup(); + const model = await cms.createBasicModel({ modelGroup: modelGroup.id }); + + // Let's create the entry + const [createEntryResponse] = await cms.createEntry(model, { + data: { title: "Entry 1" } + }); + + const entry = createEntryResponse.data[`create${model.singularApiName}`]?.data; + + // Let's move the entry to the trash bin + await cms.deleteEntry(model, { + revision: entry.entryId, + options: { + permanently: false + } + }); + + // Let's restore the entry from the trash bin + const [restoreEntryResponse] = await cms.restoreEntry(model, { + revision: entry.entryId + }); + + expect(restoreEntryResponse).toMatchObject({ + data: { + [`restore${model.singularApiName}FromBin`]: { + data: entry + } + } + }); + + // Let's get back the entry + const [getEntryResponse] = await cms.getEntry(model, { + revision: entry.id + }); + + expect(getEntryResponse).toMatchObject({ + data: { + [`get${model.singularApiName}`]: { + data: entry + } + } + }); + }); + + it(`should not change the location if folder exists`, async () => { + const { cms, aco } = useGraphQlHandler({ + plugins: [assignCmsLifecycleEvents()] + }); + + // Let's create the model group and model + const modelGroup = await cms.createTestModelGroup(); + const model = await cms.createBasicModel({ modelGroup: modelGroup.id }); + + // Let's create the folder + const [createFolderResponse] = await aco.createFolder({ + data: { + title: "Folder 1", + slug: "folder-1", + type: `cms:${model.modelId}` + } + }); + + const folder = createFolderResponse.data?.aco?.createFolder?.data; + + // Let's create the entry inside the folder we have just created + const [createEntryResponse] = await cms.createEntry(model, { + data: { + title: "Entry 1", + wbyAco_location: { + folderId: folder.id + } + } + }); + + const entry = createEntryResponse.data[`create${model.singularApiName}`]?.data; + + // Let's move the entry to the trash bin + await cms.deleteEntry(model, { + revision: entry.entryId, + options: { + permanently: false + } + }); + + // Let's restore the entry from the trash bin + const [restoreEntryResponse] = await cms.restoreEntry(model, { + revision: entry.entryId + }); + + expect(restoreEntryResponse).toMatchObject({ + data: { + [`restore${model.singularApiName}FromBin`]: { + data: entry + } + } + }); + + // Let's get back the entry + const [getEntryResponse] = await cms.getEntry(model, { + revision: entry.id + }); + + expect(getEntryResponse).toMatchObject({ + data: { + [`get${model.singularApiName}`]: { + data: entry + } + } + }); + }); + + it(`should change the location to ${ROOT_FOLDER} if the folder does not exist`, async () => { + const { cms, aco } = useGraphQlHandler({ + plugins: [assignCmsLifecycleEvents()] + }); + + // Let's create the model group and model + const modelGroup = await cms.createTestModelGroup(); + const model = await cms.createBasicModel({ modelGroup: modelGroup.id }); + + // Let's create the folder + const [createFolderResponse] = await aco.createFolder({ + data: { + title: "Folder 1", + slug: "folder-1", + type: `cms:${model.modelId}` + } + }); + + const folder = createFolderResponse.data?.aco?.createFolder?.data; + + // Let's create the entry inside the folder we have just created + const [createEntryResponse] = await cms.createEntry(model, { + data: { + title: "Entry 1", + wbyAco_location: { + folderId: folder.id + } + } + }); + + const entry = createEntryResponse.data[`create${model.singularApiName}`]?.data; + + // Let's move the entry to the trash bin + await cms.deleteEntry(model, { + revision: entry.entryId, + options: { + permanently: false + } + }); + + // Let's delete the folder + await aco.deleteFolder({ + id: folder.id + }); + + // Let's restore the entry from the trash bin + const [restoreEntryResponse] = await cms.restoreEntry(model, { + revision: entry.entryId + }); + + expect(restoreEntryResponse).toMatchObject({ + data: { + [`restore${model.singularApiName}FromBin`]: { + data: { + ...entry, + wbyAco_location: { + folderId: ROOT_FOLDER + } + } + } + } + }); + + // Let's get back the entry + const [getEntryResponse] = await cms.getEntry(model, { + revision: entry.id + }); + + expect(getEntryResponse).toMatchObject({ + data: { + [`get${model.singularApiName}`]: { + data: { + ...entry, + wbyAco_location: { + folderId: ROOT_FOLDER + } + } + } + } + }); + }); +}); diff --git a/packages/api-headless-cms-aco/__tests__/graphql/cms.gql.ts b/packages/api-headless-cms-aco/__tests__/graphql/cms.gql.ts new file mode 100644 index 00000000000..ec80d2a35e3 --- /dev/null +++ b/packages/api-headless-cms-aco/__tests__/graphql/cms.gql.ts @@ -0,0 +1,97 @@ +import { CmsModel } from "@webiny/api-headless-cms/types"; + +const ERROR_FIELD = /* GraphQL */ ` + { + code + data + message + } +`; + +const DATA_FIELD = /* GraphQL */ ` + { + id + entryId + title + wbyAco_location { + folderId + } + } +`; + +export const CREATE_CONTENT_MODEL = /* GraphQL */ ` + mutation CmsCreateContentModel($data: CmsContentModelCreateInput!) { + createContentModel(data: $data) { + data { + modelId + singularApiName + pluralApiName + } + error ${ERROR_FIELD} + } + } +`; + +export const CREATE_CONTENT_MODEL_GROUP = /* GraphQL */ ` + mutation CreateContentModelGroupMutation($data: CmsContentModelGroupInput!) { + createContentModelGroup(data: $data) { + data { + id + name + } + error ${ERROR_FIELD} + } + } +`; + +export const CREATE_ENTRY = (model: CmsModel) => { + const Entry = model.singularApiName; + + return /* GraphQL */ ` + mutation Create${Entry}($data: ${Entry}Input!) { + create${Entry}: create${Entry}(data: $data) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + `; +}; + +export const DELETE_ENTRY = (model: CmsModel) => { + const Entry = model.singularApiName; + + return /* GraphQL */ ` + mutation Delete${Entry}($revision: ID!, $options: CmsDeleteEntryOptions) { + delete${Entry}: delete${Entry}(revision: $revision, options: $options) { + data + error ${ERROR_FIELD} + } + } + `; +}; + +export const RESTORE_ENTRY = (model: CmsModel) => { + const Entry = model.singularApiName; + + return /* GraphQL */ ` + mutation Restore${Entry}FromBin($revision: ID!) { + restore${Entry}FromBin: restore${Entry}FromBin(revision: $revision) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + `; +}; + +export const GET_ENTRY = (model: CmsModel) => { + const Entry = model.singularApiName; + + return /* GraphQL */ ` + query Get${Entry}($revision: ID!) { + get${Entry}: get${Entry}(revision: $revision) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + `; +}; diff --git a/packages/api-headless-cms-aco/__tests__/graphql/folder.gql.ts b/packages/api-headless-cms-aco/__tests__/graphql/folder.gql.ts new file mode 100644 index 00000000000..f5875ea2024 --- /dev/null +++ b/packages/api-headless-cms-aco/__tests__/graphql/folder.gql.ts @@ -0,0 +1,53 @@ +const DATA_FIELD = /* GraphQL */ ` + { + id + title + slug + type + parentId + permissions { + target + level + inheritedFrom + } + hasNonInheritedPermissions + canManagePermissions + canManageStructure + canManageContent + createdBy { + id + displayName + type + } + } +`; + +const ERROR_FIELD = /* GraphQL */ ` + { + code + data + message + } +`; + +export const CREATE_FOLDER = /* GraphQL */ ` + mutation CreateFolder($data: FolderCreateInput!) { + aco { + createFolder(data: $data) { + data ${DATA_FIELD} + error ${ERROR_FIELD} + } + } + } +`; + +export const DELETE_FOLDER = /* GraphQL */ ` + mutation DeleteFolder($id: ID!) { + aco { + deleteFolder(id: $id) { + data + error ${ERROR_FIELD} + } + } + } +`; diff --git a/packages/api-headless-cms-aco/__tests__/mocks/lifecycle.mock.ts b/packages/api-headless-cms-aco/__tests__/mocks/lifecycle.mock.ts new file mode 100644 index 00000000000..361e37a5baf --- /dev/null +++ b/packages/api-headless-cms-aco/__tests__/mocks/lifecycle.mock.ts @@ -0,0 +1,16 @@ +import { ContextPlugin } from "@webiny/api"; +import { LifecycleEventTracker } from "@webiny/project-utils/testing/helpers/lifecycleTracker"; +import { HcmsAcoContext } from "~/types"; + +export const tracker = new LifecycleEventTracker(); + +export const assignCmsLifecycleEvents = () => { + return new ContextPlugin(async context => { + context.cms.onEntryBeforeRestoreFromBin.subscribe(async params => { + tracker.track("entry:beforeRestore", params); + }); + context.cms.onEntryBeforeRestoreFromBin.subscribe(async params => { + tracker.track("entry:afterRestore", params); + }); + }); +}; diff --git a/packages/api-headless-cms-aco/__tests__/utils/createTestWcpLicense.ts b/packages/api-headless-cms-aco/__tests__/utils/createTestWcpLicense.ts new file mode 100644 index 00000000000..0c92c342cff --- /dev/null +++ b/packages/api-headless-cms-aco/__tests__/utils/createTestWcpLicense.ts @@ -0,0 +1,44 @@ +import { + DecryptedWcpProjectLicense, + MT_OPTIONS_MAX_COUNT_TYPE, + PROJECT_PACKAGE_FEATURE_NAME +} from "@webiny/wcp/types"; + +export const createTestWcpLicense = (): DecryptedWcpProjectLicense => { + return { + orgId: "org-id", + projectId: "project-id", + package: { + features: { + [PROJECT_PACKAGE_FEATURE_NAME.AACL]: { + enabled: true, + options: { + teams: true, + folderLevelPermissions: true, + privateFiles: true + } + }, + [PROJECT_PACKAGE_FEATURE_NAME.MT]: { + enabled: true, + options: { + maxCount: { + type: MT_OPTIONS_MAX_COUNT_TYPE.SEAT_BASED + } + } + }, + [PROJECT_PACKAGE_FEATURE_NAME.APW]: { + enabled: false + }, + [PROJECT_PACKAGE_FEATURE_NAME.AUDIT_LOGS]: { + enabled: false + }, + [PROJECT_PACKAGE_FEATURE_NAME.SEATS]: { + enabled: true, + options: { + maxCount: 100 + } + } + } + } + }; +}; diff --git a/packages/api-headless-cms-aco/__tests__/utils/tenancySecurity.ts b/packages/api-headless-cms-aco/__tests__/utils/tenancySecurity.ts new file mode 100644 index 00000000000..66639ef1764 --- /dev/null +++ b/packages/api-headless-cms-aco/__tests__/utils/tenancySecurity.ts @@ -0,0 +1,61 @@ +import { createTenancyContext, createTenancyGraphQL } from "@webiny/api-tenancy"; +import { createSecurityContext, createSecurityGraphQL } from "@webiny/api-security"; +import { + SecurityContext, + SecurityIdentity, + SecurityPermission, + SecurityStorageOperations +} from "@webiny/api-security/types"; +import { ContextPlugin } from "@webiny/api"; +import { BeforeHandlerPlugin } from "@webiny/handler"; +import { TenancyContext, TenancyStorageOperations } from "@webiny/api-tenancy/types"; +import { getStorageOps } from "@webiny/project-utils/testing/environment"; + +interface Config { + permissions?: SecurityPermission[]; + identity?: SecurityIdentity | null; +} + +export const createTenancyAndSecurity = ({ permissions, identity }: Config) => { + const securityStorage = getStorageOps("security"); + const tenancyStorage = getStorageOps("tenancy"); + return [ + createTenancyContext({ storageOperations: tenancyStorage.storageOperations }), + createTenancyGraphQL(), + createSecurityContext({ storageOperations: securityStorage.storageOperations }), + createSecurityGraphQL(), + new ContextPlugin(context => { + context.tenancy.setCurrentTenant({ + id: "root", + name: "Root", + settings: { + domains: [] + }, + status: "unknown", + description: "", + parent: null, + tags: [], + savedOn: new Date().toISOString(), + createdOn: new Date().toISOString(), + webinyVersion: "w.w.w" + }); + + context.security.addAuthenticator(async () => { + return ( + identity || { + id: "12345678", + type: "admin", + displayName: "John Doe" + } + ); + }); + + context.security.addAuthorizer(async () => { + return permissions || [{ name: "*" }]; + }); + }), + new BeforeHandlerPlugin(context => { + return context.security.authenticate(""); + }) + ]; +}; diff --git a/packages/api-headless-cms-aco/__tests__/utils/useGraphQlHandler.ts b/packages/api-headless-cms-aco/__tests__/utils/useGraphQlHandler.ts new file mode 100644 index 00000000000..92e37b01c76 --- /dev/null +++ b/packages/api-headless-cms-aco/__tests__/utils/useGraphQlHandler.ts @@ -0,0 +1,231 @@ +import createGraphQLHandler from "@webiny/handler-graphql"; +import { createI18NContext } from "@webiny/api-i18n"; +import { + CmsParametersPlugin, + createHeadlessCmsContext, + createHeadlessCmsGraphQL +} from "@webiny/api-headless-cms"; +import { mockLocalesPlugins } from "@webiny/api-i18n/graphql/testing"; +import { SecurityIdentity, SecurityPermission } from "@webiny/api-security/types"; +import { createHandler } from "@webiny/handler-aws"; +import { Plugin, PluginCollection } from "@webiny/plugins/types"; +import { createTenancyAndSecurity } from "./tenancySecurity"; +import { createAcoHcmsContext } from "~/index"; +import { createAco } from "@webiny/api-aco"; +import { getStorageOps } from "@webiny/project-utils/testing/environment"; +import { CmsModel, HeadlessCmsStorageOperations } from "@webiny/api-headless-cms/types"; +import { getIntrospectionQuery } from "graphql"; +import { APIGatewayEvent, LambdaContext } from "@webiny/handler-aws/types"; +import { DecryptedWcpProjectLicense } from "@webiny/wcp/types"; +import createAdminUsersApp from "@webiny/api-admin-users"; +import { createTestWcpLicense } from "~tests/utils/createTestWcpLicense"; +import { createWcpContext } from "@webiny/api-wcp"; +import { AdminUsersStorageOperations } from "@webiny/api-admin-users/types"; +import { until } from "@webiny/project-utils/testing/helpers/until"; +import { + CREATE_CONTENT_MODEL, + CREATE_CONTENT_MODEL_GROUP, + CREATE_ENTRY, + DELETE_ENTRY, + GET_ENTRY, + RESTORE_ENTRY +} from "~tests/graphql/cms.gql"; + +import { CREATE_FOLDER, DELETE_FOLDER } from "~tests/graphql/folder.gql"; + +export interface UseGQLHandlerParams { + permissions?: SecurityPermission[]; + identity?: SecurityIdentity; + plugins?: Plugin | Plugin[] | Plugin[][] | PluginCollection; + storageOperationPlugins?: any[]; + testProjectLicense?: DecryptedWcpProjectLicense; +} + +interface InvokeParams { + httpMethod?: "POST"; + type?: string; + locale?: string; + body: { + query: string; + variables?: Record; + }; + headers?: Record; +} + +const defaultIdentity: SecurityIdentity = { + id: "12345678", + type: "admin", + displayName: "John Doe" +}; + +export const useGraphQlHandler = (params: UseGQLHandlerParams = {}) => { + const { permissions, identity, plugins = [] } = params; + + const cmsStorage = getStorageOps("cms"); + const i18nStorage = getStorageOps("i18n"); + const adminUsersStorage = getStorageOps("adminUsers"); + + const testProjectLicense = params.testProjectLicense || createTestWcpLicense(); + + const handler = createHandler({ + plugins: [ + ...cmsStorage.plugins, + createWcpContext({ testProjectLicense }), + createGraphQLHandler(), + ...createTenancyAndSecurity({ permissions, identity: identity || defaultIdentity }), + createI18NContext(), + ...i18nStorage.storageOperations, + mockLocalesPlugins(), + createAdminUsersApp({ + storageOperations: adminUsersStorage.storageOperations + }), + new CmsParametersPlugin(async () => { + return { + locale: "en-US", + type: "manage" + }; + }), + createHeadlessCmsContext({ + storageOperations: cmsStorage.storageOperations + }), + createHeadlessCmsGraphQL(), + createAco(), + createAcoHcmsContext(), + plugins + ], + debug: false + }); + + // Let's also create the "invoke" function. This will make handler invocations in actual tests easier and nicer. + const invoke = async ({ httpMethod = "POST", body, headers = {}, ...rest }: InvokeParams) => { + const response = await handler( + { + path: "/graphql", + httpMethod, + headers: { + ["x-tenant"]: "root", + ["Content-Type"]: "application/json", + ...headers + }, + body: JSON.stringify(body), + ...rest + } as unknown as APIGatewayEvent, + {} as LambdaContext + ); + + // The first element is the response body, and the second is the raw response. + return [JSON.parse(response.body), response]; + }; + + const invokeCms = async ({ + httpMethod = "POST", + type = "manage", + locale = "en-US", + body, + headers = {}, + ...rest + }: InvokeParams) => { + const response = await handler( + { + path: `/cms/${type}/${locale}`, + httpMethod, + headers: { + ["x-tenant"]: "root", + ["Content-Type"]: "application/json", + ...headers + }, + body: JSON.stringify(body), + ...rest + } as unknown as APIGatewayEvent, + {} as LambdaContext + ); + + // The first element is the response body, and the second is the raw response. + return [JSON.parse(response.body), response]; + }; + + const aco = { + async createFolder(variables = {}) { + return invoke({ body: { query: CREATE_FOLDER, variables } }); + }, + + async deleteFolder(variables = {}) { + return invoke({ body: { query: DELETE_FOLDER, variables } }); + } + }; + + const cms = { + // Models, model groups, entries. + async createContentModel(variables: Record) { + return invokeCms({ body: { query: CREATE_CONTENT_MODEL, variables } }); + }, + + async createContentModelGroup(variables: Record) { + return invokeCms({ body: { query: CREATE_CONTENT_MODEL_GROUP, variables } }); + }, + + async createTestModelGroup() { + return cms + .createContentModelGroup({ + data: { + name: "Group", + slug: "group", + icon: "ico/ico", + description: "description" + } + }) + .then(([response]) => { + return response.data.createContentModelGroup.data; + }); + }, + + async createBasicModel(variables: Record) { + return cms + .createContentModel({ + data: { + modelId: "basicTestModel", + group: variables.modelGroup, + defaultFields: true, + name: "BasicTestModel", + singularApiName: "BasicTestModel", + pluralApiName: "BasicTestModels" + } + }) + .then(([response]) => { + return response.data.createContentModel.data as CmsModel; + }); + }, + + async createEntry(model: CmsModel, variables: Record) { + return invokeCms({ body: { query: CREATE_ENTRY(model), variables } }); + }, + + async deleteEntry(model: CmsModel, variables: Record) { + return invokeCms({ body: { query: DELETE_ENTRY(model), variables } }); + }, + + async restoreEntry(model: CmsModel, variables: Record) { + return invokeCms({ body: { query: RESTORE_ENTRY(model), variables } }); + }, + + async getEntry(model: CmsModel, variables: Record) { + return invokeCms({ body: { query: GET_ENTRY(model), variables } }); + } + }; + + return { + until, + params, + handler, + invoke, + aco, + cms, + async introspect() { + return invoke({ + body: { + query: getIntrospectionQuery() + } + }); + } + }; +}; diff --git a/packages/api-headless-cms-aco/jest.setup.js b/packages/api-headless-cms-aco/jest.setup.js new file mode 100644 index 00000000000..4ddf6ca63eb --- /dev/null +++ b/packages/api-headless-cms-aco/jest.setup.js @@ -0,0 +1,13 @@ +const base = require("../../jest.config.base"); +const presets = require("@webiny/project-utils/testing/presets")( + ["@webiny/api-admin-users", "storage-operations"], + ["@webiny/api-headless-cms", "storage-operations"], + ["@webiny/api-page-builder", "storage-operations"], + ["@webiny/api-i18n", "storage-operations"], + ["@webiny/api-security", "storage-operations"], + ["@webiny/api-tenancy", "storage-operations"] +); + +module.exports = { + ...base({ path: __dirname }, presets) +}; diff --git a/packages/api-headless-cms-aco/package.json b/packages/api-headless-cms-aco/package.json new file mode 100644 index 00000000000..ac14945a155 --- /dev/null +++ b/packages/api-headless-cms-aco/package.json @@ -0,0 +1,52 @@ +{ + "name": "@webiny/api-headless-cms-aco", + "version": "0.0.0", + "main": "index.js", + "keywords": [ + "api-headless-cms-aco-aco:base" + ], + "repository": { + "type": "git", + "url": "https://github.com/webiny/webiny-js.git", + "directory": "packages/api-headless-cms-aco" + }, + "description": "Connect Headless CMS to ACO", + "author": "Webiny Ltd.", + "license": "MIT", + "scripts": { + "build": "yarn webiny run build", + "watch": "yarn webiny run watch" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "devDependencies": { + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "@babel/preset-typescript": "^7.23.3", + "@babel/runtime": "^7.24.0", + "@webiny/api-admin-users": "0.0.0", + "@webiny/api-i18n": "0.0.0", + "@webiny/api-security": "0.0.0", + "@webiny/api-tenancy": "0.0.0", + "@webiny/api-wcp": "0.0.0", + "@webiny/cli": "0.0.0", + "@webiny/handler-aws": "0.0.0", + "@webiny/handler-graphql": "0.0.0", + "@webiny/plugins": "0.0.0", + "@webiny/project-utils": "0.0.0", + "@webiny/wcp": "0.0.0", + "graphql": "^15.8.0", + "ttypescript": "^1.5.13", + "typescript": "^4.7.4" + }, + "dependencies": { + "@webiny/api": "0.0.0", + "@webiny/api-aco": "0.0.0", + "@webiny/api-headless-cms": "0.0.0", + "@webiny/error": "0.0.0", + "@webiny/handler": "0.0.0" + } +} diff --git a/packages/api-headless-cms-aco/src/hooks/entry/index.ts b/packages/api-headless-cms-aco/src/hooks/entry/index.ts new file mode 100644 index 00000000000..652840f27f0 --- /dev/null +++ b/packages/api-headless-cms-aco/src/hooks/entry/index.ts @@ -0,0 +1,9 @@ +import { onEntryBeforeRestoreFromBinHook } from "~/hooks/entry/onEntryBeforeRestoreFromBin.hook"; + +export { onEntryBeforeRestoreFromBinHook } from "./onEntryBeforeRestoreFromBin.hook"; + +import { HcmsAcoContext } from "~/types"; + +export const createEntryHooks = (context: HcmsAcoContext) => { + onEntryBeforeRestoreFromBinHook(context); +}; diff --git a/packages/api-headless-cms-aco/src/hooks/entry/onEntryBeforeRestoreFromBin.hook.ts b/packages/api-headless-cms-aco/src/hooks/entry/onEntryBeforeRestoreFromBin.hook.ts new file mode 100644 index 00000000000..922ec65f912 --- /dev/null +++ b/packages/api-headless-cms-aco/src/hooks/entry/onEntryBeforeRestoreFromBin.hook.ts @@ -0,0 +1,37 @@ +import { HcmsAcoContext } from "~/types"; +import WebinyError from "@webiny/error"; +import { ROOT_FOLDER } from "@webiny/api-headless-cms/constants"; + +export const onEntryBeforeRestoreFromBinHook = (context: HcmsAcoContext) => { + const { aco, cms } = context; + + cms.onEntryBeforeRestoreFromBin.subscribe(async ({ entry }) => { + /** + * Skip further execution if folderId is falsy or equals ROOT_FOLDER. + */ + if (!entry.location?.folderId || entry.location.folderId === ROOT_FOLDER) { + return; + } + + try { + /** + * Retrieve the folder: if it exists, no additional operations are necessary. + */ + await aco.folder.get(entry.location.folderId); + return; + } catch (error) { + /** + * If the folder is not found, set ROOT_FOLDER as the location. + */ + if (error.code === "NOT_FOUND") { + entry.location.folderId = ROOT_FOLDER; + return; + } + + throw WebinyError.from(error, { + message: "Error while executing onEntryBeforeRestoreFromBin hook", + code: "HCMS_ACO_BEFORE_RESTORE_FROM_BIN_HOOK" + }); + } + }); +}; diff --git a/packages/api-headless-cms-aco/src/index.ts b/packages/api-headless-cms-aco/src/index.ts new file mode 100644 index 00000000000..83e5d3c5436 --- /dev/null +++ b/packages/api-headless-cms-aco/src/index.ts @@ -0,0 +1,17 @@ +import { ContextPlugin } from "@webiny/api"; +import { createEntryHooks } from "~/hooks/entry"; +import { HcmsAcoContext } from "~/types"; + +export const createAcoHcmsContext = () => { + const plugin = new ContextPlugin(async context => { + if (!context.aco) { + console.log(`There is no ACO initialized so we will not initialize the HCMS ACO.`); + return; + } + createEntryHooks(context); + }); + + plugin.name = "hcms-aco.createContext"; + + return plugin; +}; diff --git a/packages/api-headless-cms-aco/src/types.ts b/packages/api-headless-cms-aco/src/types.ts new file mode 100644 index 00000000000..7ca9b461a39 --- /dev/null +++ b/packages/api-headless-cms-aco/src/types.ts @@ -0,0 +1,5 @@ +import { AcoContext } from "@webiny/api-aco/types"; +import { CmsContext } from "@webiny/api-headless-cms/types"; +import { Context as BaseContext } from "@webiny/handler/types"; + +export interface HcmsAcoContext extends BaseContext, AcoContext, CmsContext {} diff --git a/packages/api-headless-cms-aco/tsconfig.build.json b/packages/api-headless-cms-aco/tsconfig.build.json new file mode 100644 index 00000000000..35fd5afcc72 --- /dev/null +++ b/packages/api-headless-cms-aco/tsconfig.build.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "references": [ + { "path": "../api/tsconfig.build.json" }, + { "path": "../api-aco/tsconfig.build.json" }, + { "path": "../error/tsconfig.build.json" }, + { "path": "../handler/tsconfig.build.json" }, + { "path": "../api-admin-users/tsconfig.build.json" }, + { "path": "../api-headless-cms/tsconfig.build.json" }, + { "path": "../api-i18n/tsconfig.build.json" }, + { "path": "../api-security/tsconfig.build.json" }, + { "path": "../api-tenancy/tsconfig.build.json" }, + { "path": "../api-wcp/tsconfig.build.json" }, + { "path": "../handler-aws/tsconfig.build.json" }, + { "path": "../handler-graphql/tsconfig.build.json" }, + { "path": "../plugins/tsconfig.build.json" }, + { "path": "../wcp/tsconfig.build.json" } + ], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"] }, + "baseUrl": "." + } +} diff --git a/packages/api-headless-cms-aco/tsconfig.json b/packages/api-headless-cms-aco/tsconfig.json new file mode 100644 index 00000000000..773d135b879 --- /dev/null +++ b/packages/api-headless-cms-aco/tsconfig.json @@ -0,0 +1,58 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "__tests__"], + "references": [ + { "path": "../api" }, + { "path": "../api-aco" }, + { "path": "../error" }, + { "path": "../handler" }, + { "path": "../api-admin-users" }, + { "path": "../api-headless-cms" }, + { "path": "../api-i18n" }, + { "path": "../api-security" }, + { "path": "../api-tenancy" }, + { "path": "../api-wcp" }, + { "path": "../handler-aws" }, + { "path": "../handler-graphql" }, + { "path": "../plugins" }, + { "path": "../wcp" } + ], + "compilerOptions": { + "rootDirs": ["./src", "./__tests__"], + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { + "~/*": ["./src/*"], + "~tests/*": ["./__tests__/*"], + "@webiny/api/*": ["../api/src/*"], + "@webiny/api": ["../api/src"], + "@webiny/api-aco/*": ["../api-aco/src/*"], + "@webiny/api-aco": ["../api-aco/src"], + "@webiny/error/*": ["../error/src/*"], + "@webiny/error": ["../error/src"], + "@webiny/handler/*": ["../handler/src/*"], + "@webiny/handler": ["../handler/src"], + "@webiny/api-admin-users/*": ["../api-admin-users/src/*"], + "@webiny/api-admin-users": ["../api-admin-users/src"], + "@webiny/api-headless-cms/*": ["../api-headless-cms/src/*"], + "@webiny/api-headless-cms": ["../api-headless-cms/src"], + "@webiny/api-i18n/*": ["../api-i18n/src/*"], + "@webiny/api-i18n": ["../api-i18n/src"], + "@webiny/api-security/*": ["../api-security/src/*"], + "@webiny/api-security": ["../api-security/src"], + "@webiny/api-tenancy/*": ["../api-tenancy/src/*"], + "@webiny/api-tenancy": ["../api-tenancy/src"], + "@webiny/api-wcp/*": ["../api-wcp/src/*"], + "@webiny/api-wcp": ["../api-wcp/src"], + "@webiny/handler-aws/*": ["../handler-aws/src/*"], + "@webiny/handler-aws": ["../handler-aws/src"], + "@webiny/handler-graphql/*": ["../handler-graphql/src/*"], + "@webiny/handler-graphql": ["../handler-graphql/src"], + "@webiny/plugins/*": ["../plugins/src/*"], + "@webiny/plugins": ["../plugins/src"], + "@webiny/wcp/*": ["../wcp/src/*"], + "@webiny/wcp": ["../wcp/src"] + }, + "baseUrl": "." + } +} diff --git a/packages/api-headless-cms-aco/webiny.config.js b/packages/api-headless-cms-aco/webiny.config.js new file mode 100644 index 00000000000..6dff86766c9 --- /dev/null +++ b/packages/api-headless-cms-aco/webiny.config.js @@ -0,0 +1,8 @@ +const { createWatchPackage, createBuildPackage } = require("@webiny/project-utils"); + +module.exports = { + commands: { + build: createBuildPackage({ cwd: __dirname }), + watch: createWatchPackage({ cwd: __dirname }) + } +}; diff --git a/packages/api-headless-cms-ddb-es/src/definitions/entry.ts b/packages/api-headless-cms-ddb-es/src/definitions/entry.ts index ed361b02ee8..c323a06dd3a 100644 --- a/packages/api-headless-cms-ddb-es/src/definitions/entry.ts +++ b/packages/api-headless-cms-ddb-es/src/definitions/entry.ts @@ -46,12 +46,14 @@ export const createEntryEntity = (params: CreateEntryEntityParams): Entity revisionSavedOn: { type: "string" }, revisionModifiedOn: { type: "string" }, revisionDeletedOn: { type: "string" }, + revisionRestoredOn: { type: "string" }, revisionFirstPublishedOn: { type: "string" }, revisionLastPublishedOn: { type: "string" }, revisionCreatedBy: { type: "map" }, revisionSavedBy: { type: "map" }, revisionModifiedBy: { type: "map" }, revisionDeletedBy: { type: "map" }, + revisionRestoredBy: { type: "map" }, revisionFirstPublishedBy: { type: "map" }, revisionLastPublishedBy: { type: "map" }, @@ -62,12 +64,14 @@ export const createEntryEntity = (params: CreateEntryEntityParams): Entity savedOn: { type: "string" }, modifiedOn: { type: "string" }, deletedOn: { type: "string" }, + restoredOn: { type: "string" }, firstPublishedOn: { type: "string" }, lastPublishedOn: { type: "string" }, createdBy: { type: "map" }, savedBy: { type: "map" }, modifiedBy: { type: "map" }, deletedBy: { type: "map" }, + restoredBy: { type: "map" }, firstPublishedBy: { type: "map" }, lastPublishedBy: { type: "map" }, @@ -92,6 +96,9 @@ export const createEntryEntity = (params: CreateEntryEntityParams): Entity deleted: { type: "boolean" }, + binOriginalFolderId: { + type: "string" + }, values: { type: "map" }, diff --git a/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/fields.ts b/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/fields.ts index ce40d2e1b39..51af25cb197 100644 --- a/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/fields.ts +++ b/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/fields.ts @@ -187,6 +187,20 @@ const createSystemFields = (): ModelFields => { type: "boolean" }), parents: [] + }, + binOriginalFolderId: { + type: "text", + unmappedType: undefined, + keyword: false, + systemField: true, + searchable: true, + sortable: false, + field: createSystemField({ + storageId: "binOriginalFolderId", + fieldId: "binOriginalFolderId", + type: "text" + }), + parents: [] } }; }; diff --git a/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts b/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts index 01a6b6db47f..c2eff05a403 100644 --- a/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts +++ b/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts @@ -47,7 +47,9 @@ import { batchReadAll, BatchReadItem, put } from "@webiny/db-dynamodb"; import { createTransformer } from "./transformations"; import { convertEntryKeysFromStorage } from "./transformations/convertEntryKeys"; import { + isDeletedEntryMetaField, isEntryLevelEntryMetaField, + isRestoredEntryMetaField, pickEntryMetaFields } from "@webiny/api-headless-cms/constants"; @@ -741,6 +743,11 @@ export const createEntriesStorageOperations = ( const publishedSortKey = createPublishedSortKey(); const records = await queryAll(queryAllParams); + /** + * Let's pick the `deleted` meta fields from the entry. + */ + const updatedEntryMetaFields = pickEntryMetaFields(entry, isDeletedEntryMetaField); + /** * Then update all the records with data received. */ @@ -752,7 +759,10 @@ export const createEntriesStorageOperations = ( items.push( entity.putBatch({ ...record, - ...storageEntry + ...updatedEntryMetaFields, + deleted: storageEntry.deleted, + location: storageEntry.location, + binOriginalFolderId: storageEntry.binOriginalFolderId }) ); /** @@ -838,7 +848,6 @@ export const createEntriesStorageOperations = ( /** * We update all ES records with data received. */ - const updatedEntryLevelMetaFields = pickEntryMetaFields(entry, isEntryLevelEntryMetaField); const esUpdateItems: BatchWriteItem[] = []; for (const item of esItems) { esUpdateItems.push( @@ -846,8 +855,10 @@ export const createEntriesStorageOperations = ( ...item, data: await compress(plugins, { ...item.data, - deleted: true, - ...updatedEntryLevelMetaFields + ...updatedEntryMetaFields, + deleted: entry.deleted, + location: entry.location, + binOriginalFolderId: entry.binOriginalFolderId }) }) ); @@ -875,6 +886,184 @@ export const createEntriesStorageOperations = ( } }; + const restoreFromBin: CmsEntryStorageOperations["restoreFromBin"] = async ( + initialModel, + params + ) => { + const { entry: initialEntry, storageEntry: initialStorageEntry } = params; + const model = getStorageOperationsModel(initialModel); + + const transformer = createTransformer({ + plugins, + model, + entry: initialEntry, + storageEntry: initialStorageEntry + }); + + const { entry, storageEntry } = transformer.transformEntryKeys(); + + /** + * Let's pick the `restored` meta fields from the storage entry. + */ + const updatedEntryMetaFields = pickEntryMetaFields(entry, isRestoredEntryMetaField); + + const partitionKey = createPartitionKey({ + id: entry.id, + locale: model.locale, + tenant: model.tenant + }); + + /** + * First we need to fetch all the records in the regular DynamoDB table. + */ + const queryAllParams: QueryAllParams = { + entity, + partitionKey, + options: { + gte: " " + } + }; + + const latestSortKey = createLatestSortKey(); + const publishedSortKey = createPublishedSortKey(); + const records = await queryAll(queryAllParams); + + /** + * Then update all the records with data received. + */ + let latestRecord: CmsEntry | undefined = undefined; + let publishedRecord: CmsEntry | undefined = undefined; + const items: BatchWriteItem[] = []; + + for (const record of records) { + items.push( + entity.putBatch({ + ...record, + ...updatedEntryMetaFields, + deleted: storageEntry.deleted, + location: storageEntry.location, + binOriginalFolderId: storageEntry.binOriginalFolderId + }) + ); + /** + * We need to get the published and latest records, so we can update the Elasticsearch. + */ + if (record.SK === publishedSortKey) { + publishedRecord = record; + } else if (record.SK === latestSortKey) { + latestRecord = record; + } + } + + /** + * We write the records back to the primary DynamoDB table. + */ + try { + await batchWriteAll({ + table: entity.table, + items + }); + dataLoaders.clearAll({ + model + }); + } catch (ex) { + throw new WebinyError( + ex.message || "Could not restore all entry records from in the DynamoDB table.", + ex.code || "RESTORE_ENTRY_ERROR", + { + error: ex, + entry, + storageEntry + } + ); + } + + /** + * We need to get the published and latest records from Elasticsearch. + */ + const esGetItems: BatchReadItem[] = []; + if (publishedRecord) { + esGetItems.push( + esEntity.getBatch({ + PK: partitionKey, + SK: publishedSortKey + }) + ); + } + if (latestRecord) { + esGetItems.push( + esEntity.getBatch({ + PK: partitionKey, + SK: latestSortKey + }) + ); + } + + const esRecords = await batchReadAll({ + table: esEntity.table, + items: esGetItems + }); + + const esItems = ( + await Promise.all( + esRecords.map(async record => { + if (!record) { + return null; + } + return { + ...record, + data: await decompress(plugins, record.data) + }; + }) + ) + ).filter(Boolean) as ElasticsearchDbRecord[]; + + if (esItems.length === 0) { + return initialStorageEntry; + } + + /** + * We update all ES records with data received. + */ + const esUpdateItems: BatchWriteItem[] = []; + for (const item of esItems) { + esUpdateItems.push( + esEntity.putBatch({ + ...item, + data: await compress(plugins, { + ...item.data, + ...updatedEntryMetaFields, + deleted: entry.deleted, + location: entry.location, + binOriginalFolderId: entry.binOriginalFolderId + }) + }) + ); + } + + /** + * We write the records back to the primary DynamoDB Elasticsearch table. + */ + try { + await batchWriteAll({ + table: esEntity.table, + items: esUpdateItems + }); + } catch (ex) { + throw new WebinyError( + ex.message || "Could not restore entry records from DynamoDB Elasticsearch table.", + ex.code || "RESTORE_ENTRY_ERROR", + { + error: ex, + entry, + storageEntry + } + ); + } + + return initialStorageEntry; + }; + const deleteEntry: CmsEntryStorageOperations["delete"] = async (initialModel, params) => { const { entry } = params; const id = entry.id || entry.entryId; @@ -1954,6 +2143,7 @@ export const createEntriesStorageOperations = ( move, delete: deleteEntry, moveToBin, + restoreFromBin, deleteRevision, deleteMultipleEntries, get, diff --git a/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/createFields.test.ts b/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/createFields.test.ts index 5918a100fa6..e6977a4f6fd 100644 --- a/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/createFields.test.ts +++ b/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/createFields.test.ts @@ -72,6 +72,17 @@ const expectedSystemFields: Record = { transform: expect.any(Function), label: "Revision Deleted On" }, + revisionRestoredOn: { + id: "revisionRestoredOn", + parents: [], + type: "datetime", + storageId: "revisionRestoredOn", + fieldId: "revisionRestoredOn", + createPath: expect.any(Function), + system: true, + transform: expect.any(Function), + label: "Revision Restored On" + }, revisionFirstPublishedOn: { id: "revisionFirstPublishedOn", parents: [], @@ -127,6 +138,17 @@ const expectedSystemFields: Record = { transform: expect.any(Function), label: "Deleted On" }, + restoredOn: { + id: "restoredOn", + parents: [], + type: "datetime", + storageId: "restoredOn", + fieldId: "restoredOn", + createPath: expect.any(Function), + system: true, + transform: expect.any(Function), + label: "Restored On" + }, savedOn: { id: "savedOn", parents: [], @@ -216,6 +238,20 @@ const expectedSystemFields: Record = { path: "revisionDeletedBy.id" } }, + revisionRestoredBy: { + id: "revisionRestoredBy", + parents: [], + type: "plainObject", + storageId: "revisionRestoredBy", + fieldId: "revisionRestoredBy", + createPath: expect.any(Function), + system: true, + transform: expect.any(Function), + label: "Revision Restored By", + settings: { + path: "revisionRestoredBy.id" + } + }, revisionFirstPublishedBy: { id: "revisionFirstPublishedBy", parents: [], @@ -300,6 +336,20 @@ const expectedSystemFields: Record = { path: "deletedBy.id" } }, + restoredBy: { + id: "restoredBy", + parents: [], + type: "plainObject", + storageId: "restoredBy", + fieldId: "restoredBy", + createPath: expect.any(Function), + system: true, + transform: expect.any(Function), + label: "Restored By", + settings: { + path: "restoredBy.id" + } + }, firstPublishedBy: { id: "firstPublishedBy", parents: [], diff --git a/packages/api-headless-cms-ddb/src/definitions/entry.ts b/packages/api-headless-cms-ddb/src/definitions/entry.ts index ee8f739fed3..e6672306eaa 100644 --- a/packages/api-headless-cms-ddb/src/definitions/entry.ts +++ b/packages/api-headless-cms-ddb/src/definitions/entry.ts @@ -59,12 +59,14 @@ export const createEntryEntity = (params: Params): Entity => { revisionModifiedOn: { type: "string" }, revisionSavedOn: { type: "string" }, revisionDeletedOn: { type: "string" }, + revisionRestoredOn: { type: "string" }, revisionFirstPublishedOn: { type: "string" }, revisionLastPublishedOn: { type: "string" }, revisionCreatedBy: { type: "map" }, revisionModifiedBy: { type: "map" }, revisionSavedBy: { type: "map" }, revisionDeletedBy: { type: "map" }, + revisionRestoredBy: { type: "map" }, revisionFirstPublishedBy: { type: "map" }, revisionLastPublishedBy: { type: "map" }, @@ -75,12 +77,14 @@ export const createEntryEntity = (params: Params): Entity => { modifiedOn: { type: "string" }, savedOn: { type: "string" }, deletedOn: { type: "string" }, + restoredOn: { type: "string" }, firstPublishedOn: { type: "string" }, lastPublishedOn: { type: "string" }, createdBy: { type: "map" }, modifiedBy: { type: "map" }, savedBy: { type: "map" }, deletedBy: { type: "map" }, + restoredBy: { type: "map" }, firstPublishedBy: { type: "map" }, lastPublishedBy: { type: "map" }, @@ -99,6 +103,9 @@ export const createEntryEntity = (params: Params): Entity => { deleted: { type: "boolean" }, + binOriginalFolderId: { + type: "string" + }, values: { type: "map" }, diff --git a/packages/api-headless-cms-ddb/src/operations/entry/index.ts b/packages/api-headless-cms-ddb/src/operations/entry/index.ts index 45c31bb6397..b487ed1d17e 100644 --- a/packages/api-headless-cms-ddb/src/operations/entry/index.ts +++ b/packages/api-headless-cms-ddb/src/operations/entry/index.ts @@ -37,7 +37,9 @@ import { filter, sort } from "~/operations/entry/filtering"; import { WriteRequest } from "@webiny/aws-sdk/client-dynamodb"; import { CmsEntryStorageOperations } from "~/types"; import { + isDeletedEntryMetaField, isEntryLevelEntryMetaField, + isRestoredEntryMetaField, pickEntryMetaFields } from "@webiny/api-headless-cms/constants"; @@ -512,13 +514,21 @@ export const createEntriesStorageOperations = ( storageEntry: initialStorageEntry }); + /** + * Let's pick the `deleted` meta fields from the storage entry. + */ + const updatedDeletedMetaFields = pickEntryMetaFields(storageEntry, isDeletedEntryMetaField); + /** * Then create the batch writes for the DynamoDB, with the updated data. */ const items = records.map(record => { return entity.putBatch({ ...record, - ...storageEntry + ...updatedDeletedMetaFields, + deleted: storageEntry.deleted, + location: storageEntry.location, + binOriginalFolderId: storageEntry.binOriginalFolderId }); }); /** @@ -600,6 +610,91 @@ export const createEntriesStorageOperations = ( } }; + const restoreFromBin: CmsEntryStorageOperations["restoreFromBin"] = async ( + initialModel, + params + ) => { + const { entry, storageEntry: initialStorageEntry } = params; + const model = getStorageOperationsModel(initialModel); + + /** + * First we need to load all the revisions and published / latest entries. + */ + const queryAllParams: QueryAllParams = { + entity, + partitionKey: createPartitionKey({ + id: entry.id, + locale: model.locale, + tenant: model.tenant + }), + options: { + gte: " " + } + }; + + let records: DbItem[] = []; + try { + records = await queryAll(queryAllParams); + } catch (ex) { + throw new WebinyError( + ex.message || "Could not load all records.", + ex.code || "LOAD_ALL_RECORDS_ERROR", + { + error: ex, + id: entry.id + } + ); + } + + const storageEntry = convertToStorageEntry({ + model, + storageEntry: initialStorageEntry + }); + + /** + * Let's pick the `restored` meta fields from the storage entry. + */ + const updatedRestoredMetaFields = pickEntryMetaFields( + storageEntry, + isRestoredEntryMetaField + ); + + const items = records.map(record => { + return entity.putBatch({ + ...record, + ...updatedRestoredMetaFields, + deleted: storageEntry.deleted, + location: storageEntry.location, + binOriginalFolderId: storageEntry.binOriginalFolderId + }); + }); + /** + * And finally write it... + */ + try { + await batchWriteAll({ + table: entity.table, + items + }); + + dataLoaders.clearAll({ + model + }); + + return initialStorageEntry; + } catch (ex) { + throw new WebinyError( + ex.message || "Could not restore the entry from the bin.", + ex.code || "RESTORE_ENTRY_ERROR", + { + error: ex, + entry, + storageEntry + } + ); + } + }; + const deleteRevision: CmsEntryStorageOperations["deleteRevision"] = async ( initialModel, params @@ -816,14 +911,12 @@ export const createEntriesStorageOperations = ( ids: [params.id] }); - return items - .filter(item => item.deleted !== true) - .map(item => { - return convertFromStorageEntry({ - storageEntry: item, - model - }); + return items.map(item => { + return convertFromStorageEntry({ + storageEntry: item, + model }); + }); }; const getByIds: CmsEntryStorageOperations["getByIds"] = async (initialModel, params) => { @@ -1383,6 +1476,7 @@ export const createEntriesStorageOperations = ( move, delete: deleteEntry, moveToBin, + restoreFromBin, deleteRevision, deleteMultipleEntries, getPreviousRevision, diff --git a/packages/api-headless-cms/__tests__/contentAPI/contentEntry.delete.test.ts b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.delete.test.ts index 0f8799ea76e..e46b724b252 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/contentEntry.delete.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.delete.test.ts @@ -3,6 +3,7 @@ import { useCategoryManageHandler } from "~tests/testHelpers/useCategoryManageHa import { CmsEntry } from "~/types"; import { toSlug } from "~/utils/toSlug"; import { useCategoryReadHandler } from "~tests/testHelpers/useCategoryReadHandler"; +import { ROOT_FOLDER } from "~/constants"; jest.setTimeout(100000); @@ -252,8 +253,8 @@ describe("delete entries", () => { const [getAfterDeleteManageResponse] = await manager.getCategory({ revision: categoryToDelete.id }); - const [getAfterDeleteReadResponse] = await manager.getCategory({ - revision: categoryToDelete.id + const [getAfterDeleteReadResponse] = await reader.getCategory({ + where: { id: categoryToDelete.id } }); expect(getAfterDeleteManageResponse).toMatchObject({ data: { @@ -401,7 +402,10 @@ describe("delete entries", () => { expect.objectContaining({ entryId: categoryToDelete.entryId, deletedBy: expect.any(Object), - deletedOn: expect.any(String) + deletedOn: expect.any(String), + wbyAco_location: { + folderId: ROOT_FOLDER + } }) ]), error: null, @@ -480,4 +484,97 @@ describe("delete entries", () => { } }); }); + + it("should delete an entry, moving it to the ROOT_FOLDER placed inside the bin", async () => { + const categories = await setupCategories(); + + const [listManageResponse] = await manager.listCategories(); + expect(listManageResponse.data.listCategories.data).toHaveLength(titles.length); + + const categoryToDelete = categories[0]; + const newFolderId = "anotherFolder"; + + /** + * Let's now move the entry into a different folder + */ + const [moveResponse] = await manager.moveCategory({ + revision: categoryToDelete.id, + folderId: newFolderId + }); + + expect(moveResponse).toMatchObject({ + data: { + moveCategory: { + data: true, + error: null + } + } + }); + + /** + * ...let's check the new location. + */ + const [getAfterMoveManageResponse] = await manager.getCategory({ + revision: categoryToDelete.id + }); + + expect(getAfterMoveManageResponse).toMatchObject({ + data: { + getCategory: { + data: { + ...categoryToDelete, + wbyAco_location: { + folderId: newFolderId + } + }, + error: null + } + } + }); + + /** + * Let's now delete one entry, marking it as deleted. + */ + const [deleteResponse] = await manager.deleteCategory({ + revision: categoryToDelete.entryId, + options: { + permanently: false + } + }); + expect(deleteResponse).toMatchObject({ + data: { + deleteCategory: { + data: true, + error: null + } + } + }); + + /** + * Let's list the deleted items found in the bin, we should find it inside ROOT_FOLDER + */ + const [listDeletedManageResponse] = await manager.listDeletedCategories(); + expect(listDeletedManageResponse).toEqual({ + data: { + listDeletedCategories: { + data: expect.arrayContaining([ + expect.objectContaining({ + entryId: categoryToDelete.entryId, + deletedBy: expect.any(Object), + deletedOn: expect.any(String), + wbyAco_location: { + folderId: ROOT_FOLDER + } + }) + ]), + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 1 + } + } + } + }); + }); }); diff --git a/packages/api-headless-cms/__tests__/contentAPI/contentEntry.restore.test.ts b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.restore.test.ts new file mode 100644 index 00000000000..fb24c6dc854 --- /dev/null +++ b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.restore.test.ts @@ -0,0 +1,435 @@ +import { setupContentModelGroup, setupContentModels } from "~tests/testHelpers/setup"; +import { useCategoryManageHandler } from "~tests/testHelpers/useCategoryManageHandler"; +import { useCategoryReadHandler } from "~tests/testHelpers/useCategoryReadHandler"; +import { CmsEntry } from "~/types"; +import { toSlug } from "~/utils/toSlug"; +import { ROOT_FOLDER } from "~/constants"; + +jest.setTimeout(100000); + +interface CreateCategoryParams { + title: string; + slug: string; +} +type Categories = CmsEntry[]; + +describe("restore entries", () => { + const manager = useCategoryManageHandler({ + path: "manage/en-US" + }); + const reader = useCategoryReadHandler({ + path: "read/en-US" + }); + + const createCategory = async (data: CreateCategoryParams) => { + const [response] = await manager.createCategory({ + data + }); + + const createdCategory = response.data.createCategory.data; + + if (response.data.createCategory.error) { + throw new Error(response.data.createCategory.error.message); + } + + const [publish] = await manager.publishCategory({ + revision: createdCategory.id + }); + if (publish.data.publishCategory.error) { + throw new Error(publish.data.publishCategory.error.message); + } + + return publish.data.publishCategory.data; + }; + + const titles = [ + "Space Exploration", + "Food Production", + "Tech Industry", + "Mental Health", + "Maritime Industry", + "Space Industry", + "Bug Reporting", + "Car Reviews", + "Mobile Phone Reviews", + "Movie Reviews", + "Book Reviews", + "Music Reviews", + "Game Reviews", + "TV Show Reviews" + ]; + + const createCategories = async () => { + const results: Categories = []; + for (const title of titles) { + const result = await createCategory({ + title, + slug: toSlug(title) + }); + results.push(result); + } + + return results; + }; + + const setupCategories = async () => { + const group = await setupContentModelGroup(manager); + await setupContentModels(manager, group, ["category"]); + return createCategories(); + }; + + it("should move an entry to trash bin and then restore it", async () => { + const categories = await setupCategories(); + + const [listManageResponse] = await manager.listCategories(); + expect(listManageResponse.data.listCategories.data).toHaveLength(titles.length); + const [listReadResponse] = await reader.listCategories(); + expect(listReadResponse.data.listCategories.data).toHaveLength(titles.length); + + const categoryToRestore = categories[0]; + + /** + * Let's move one entry to the trash bin. + */ + const [deleteResponse] = await manager.deleteCategory({ + revision: categoryToRestore.entryId, + options: { + permanently: false + } + }); + expect(deleteResponse).toMatchObject({ + data: { + deleteCategory: { + data: true, + error: null + } + } + }); + + /** + * Let's check that has been removed from the list. + */ + const [listAfterDeleteManageResponse] = await manager.listCategories(); + expect(listAfterDeleteManageResponse.data.listCategories.meta.totalCount).toBe( + titles.length - 1 + ); + const [listAfterDeleteReadResponse] = await reader.listCategories(); + expect(listAfterDeleteReadResponse.data.listCategories.data).toHaveLength( + titles.length - 1 + ); + + /** + * ...and we should not be able to get the entry anymore. + */ + const [getAfterDeleteManageResponse] = await manager.getCategory({ + revision: categoryToRestore.id + }); + const [getAfterDeleteReadResponse] = await manager.getCategory({ + revision: categoryToRestore.id + }); + expect(getAfterDeleteManageResponse).toMatchObject({ + data: { + getCategory: { + data: null, + error: { + code: "NOT_FOUND", + message: expect.any(String) + } + } + } + }); + expect(getAfterDeleteReadResponse).toMatchObject({ + data: { + getCategory: { + data: null, + error: { + code: "NOT_FOUND", + message: expect.any(String) + } + } + } + }); + + /** + * Let's list the deleted items found in the bin, via manage endpoint... + */ + const [listDeletedManageResponse] = await manager.listDeletedCategories(); + expect(listDeletedManageResponse).toEqual({ + data: { + listDeletedCategories: { + data: expect.arrayContaining([ + expect.objectContaining({ + entryId: categoryToRestore.entryId, + deletedBy: expect.any(Object), + deletedOn: expect.any(String) + }) + ]), + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 1 + } + } + } + }); + + /** + * Let's try to restore an entry from the bin, we should get the success response... + */ + const [restoreBinItemResponse] = await manager.restoreCategoryFromBin({ + revision: categoryToRestore.entryId + }); + + expect(restoreBinItemResponse).toMatchObject({ + data: { + restoreCategoryFromBin: { + data: { + ...categoryToRestore, + deletedOn: expect.any(String), + deletedBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + }, + restoredOn: expect.any(String), + restoredBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + } + }, + error: null + } + } + }); + + /** + * ...but, if we try to repeat the operation, it should fail. + */ + const [restoreAgainBinItemResponse] = await manager.restoreCategoryFromBin({ + revision: categoryToRestore.entryId + }); + expect(restoreAgainBinItemResponse).toMatchObject({ + data: { + restoreCategoryFromBin: { + data: null, + error: { + code: "NOT_FOUND", + data: null, + message: `Entry "${categoryToRestore.entryId}" was not found!` + } + } + } + }); + + /** + * Let's check that has been restored from the trash bin. + */ + const [listAfterRestoreManageResponse] = await manager.listCategories(); + expect(listAfterRestoreManageResponse.data.listCategories.meta.totalCount).toBe( + titles.length + ); + const [listAfterRestoreReadResponse] = await reader.listCategories(); + expect(listAfterRestoreReadResponse.data.listCategories.data).toHaveLength(titles.length); + + /** + * ...and we should be able to get the entry. + */ + const [getAfterRestoreManageResponse] = await manager.getCategory({ + revision: categoryToRestore.id + }); + const [getAfterRestoreReadResponse] = await manager.getCategory({ + revision: categoryToRestore.id + }); + expect(getAfterRestoreManageResponse).toMatchObject({ + data: { + getCategory: { + data: { + ...categoryToRestore, + deletedOn: expect.any(String), + deletedBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + }, + restoredOn: expect.any(String), + restoredBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + } + }, + error: null + } + } + }); + expect(getAfterRestoreReadResponse).toMatchObject({ + data: { + getCategory: { + data: { + ...categoryToRestore, + deletedOn: expect.any(String), + deletedBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + }, + restoredOn: expect.any(String), + restoredBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + } + }, + error: null + } + } + }); + + /** + * We should NOT be able to restore an entry that has not been moved to the trash bin. + */ + const [restoreItemNotInTrashResponse] = await manager.restoreCategoryFromBin({ + revision: categoryToRestore.entryId + }); + expect(restoreItemNotInTrashResponse).toMatchObject({ + data: { + restoreCategoryFromBin: { + data: null, + error: { + code: "NOT_FOUND", + data: null, + message: `Entry "${categoryToRestore.entryId}" was not found!` + } + } + } + }); + }); + + it("should restore an entry inside the original location", async () => { + const categories = await setupCategories(); + + const [listManageResponse] = await manager.listCategories(); + expect(listManageResponse.data.listCategories.data).toHaveLength(titles.length); + + const categoryToRestore = categories[0]; + const newFolderId = "anotherFolder"; + + /** + * Let's now move the entry into a different folder. + */ + const [moveResponse] = await manager.moveCategory({ + revision: categoryToRestore.id, + folderId: newFolderId + }); + + expect(moveResponse).toMatchObject({ + data: { + moveCategory: { + data: true, + error: null + } + } + }); + + /** + * ...let's check the new location. + */ + const [getAfterMoveManageResponse] = await manager.getCategory({ + revision: categoryToRestore.id + }); + + expect(getAfterMoveManageResponse).toMatchObject({ + data: { + getCategory: { + data: { + ...categoryToRestore, + wbyAco_location: { + folderId: newFolderId + } + }, + error: null + } + } + }); + + /** + * Let's now move the entry into the bin. + */ + const [deleteResponse] = await manager.deleteCategory({ + revision: categoryToRestore.entryId, + options: { + permanently: false + } + }); + expect(deleteResponse).toMatchObject({ + data: { + deleteCategory: { + data: true, + error: null + } + } + }); + + /** + * Let's list the deleted items found in the bin, we should find it inside ROOT_FOLDER + */ + const [listDeletedManageResponse] = await manager.listDeletedCategories(); + expect(listDeletedManageResponse).toEqual({ + data: { + listDeletedCategories: { + data: expect.arrayContaining([ + expect.objectContaining({ + entryId: categoryToRestore.entryId, + deletedBy: expect.any(Object), + deletedOn: expect.any(String), + wbyAco_location: { + folderId: ROOT_FOLDER + } + }) + ]), + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 1 + } + } + } + }); + + /** + * Let's try to restore an entry from the bin, we should get it with the original location + */ + const [restoreBinItemResponse] = await manager.restoreCategoryFromBin({ + revision: categoryToRestore.entryId + }); + + expect(restoreBinItemResponse).toMatchObject({ + data: { + restoreCategoryFromBin: { + data: { + ...categoryToRestore, + deletedOn: expect.any(String), + deletedBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + }, + restoredOn: expect.any(String), + restoredBy: { + id: "id-12345678", + displayName: "John Doe", + type: "admin" + }, + wbyAco_location: { + folderId: newFolderId + } + }, + error: null + } + } + }); + }); +}); diff --git a/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts b/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts index b99f9c62205..7827ecef751 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts @@ -72,7 +72,7 @@ describe("filtering", () => { ...manageOpts }); - const filterOutFields = ["meta", "deletedOn", "deletedBy"]; + const filterOutFields = ["meta", "deletedOn", "deletedBy", "restoredOn", "restoredBy"]; const createAndPublishFruit = async (data: any): Promise => { const [response] = await createFruit({ diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts index 246c24f988b..458e4e56f87 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts @@ -10,24 +10,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -68,24 +72,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentityInput modifiedBy: CmsIdentityInput savedBy: CmsIdentityInput deletedBy: CmsIdentityInput + restoredBy: CmsIdentityInput firstPublishedBy: CmsIdentityInput lastPublishedBy: CmsIdentityInput revisionCreatedOn: DateTime revisionModifiedOn: DateTime revisionSavedOn: DateTime revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentityInput revisionModifiedBy: CmsIdentityInput revisionSavedBy: CmsIdentityInput revisionDeletedBy: CmsIdentityInput + revisionRestoredBy: CmsIdentityInput revisionFirstPublishedBy: CmsIdentityInput revisionLastPublishedBy: CmsIdentityInput @@ -140,6 +148,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -170,6 +185,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -206,6 +225,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -236,6 +262,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -303,6 +333,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -315,6 +347,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC @@ -390,6 +424,10 @@ export default /* GraphQL */ ` options: CmsDeleteEntryOptions ): CmsDeleteResponse + restoreCategoryApiNameWhichIsABitDifferentThanModelIdFromBin( + revision: ID! + ): CategoryApiNameWhichIsABitDifferentThanModelIdResponse + deleteMultipleCategoriesApiModel(entries: [ID!]!): CmsDeleteMultipleResponse! publishCategoryApiNameWhichIsABitDifferentThanModelId( diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts index f741f3ba745..34ff5a32131 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts @@ -11,24 +11,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -80,6 +84,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -110,6 +121,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -146,6 +161,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -176,6 +198,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -218,6 +244,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -230,6 +258,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts index 6d617e0cdeb..48b9bf7b498 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts @@ -10,24 +10,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -362,24 +366,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentityInput modifiedBy: CmsIdentityInput savedBy: CmsIdentityInput deletedBy: CmsIdentityInput + restoredBy: CmsIdentityInput firstPublishedBy: CmsIdentityInput lastPublishedBy: CmsIdentityInput revisionCreatedOn: DateTime revisionModifiedOn: DateTime revisionSavedOn: DateTime revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentityInput revisionModifiedBy: CmsIdentityInput revisionSavedBy: CmsIdentityInput revisionDeletedBy: CmsIdentityInput + revisionRestoredBy: CmsIdentityInput revisionFirstPublishedBy: CmsIdentityInput revisionLastPublishedBy: CmsIdentityInput @@ -437,6 +445,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -467,6 +482,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -503,6 +522,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -533,6 +559,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -582,6 +612,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -594,6 +626,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC @@ -655,6 +689,8 @@ export default /* GraphQL */ ` deletePageModelApiName(revision: ID!, options: CmsDeleteEntryOptions): CmsDeleteResponse + restorePageModelApiNameFromBin(revision: ID!): PageModelApiNameResponse + deleteMultiplePagesModelApiName(entries: [ID!]!): CmsDeleteMultipleResponse! publishPageModelApiName(revision: ID!): PageModelApiNameResponse diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts index fbd1d37cd66..97b205db465 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts @@ -11,24 +11,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -226,6 +230,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -256,6 +267,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -292,6 +307,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -322,6 +344,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -346,6 +372,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -358,6 +386,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts index 9eb5d1d9aff..a90ec5c2f8f 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts @@ -10,24 +10,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -190,24 +194,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentityInput modifiedBy: CmsIdentityInput savedBy: CmsIdentityInput deletedBy: CmsIdentityInput + restoredBy: CmsIdentityInput firstPublishedBy: CmsIdentityInput lastPublishedBy: CmsIdentityInput revisionCreatedOn: DateTime revisionModifiedOn: DateTime revisionSavedOn: DateTime revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentityInput revisionModifiedBy: CmsIdentityInput revisionSavedBy: CmsIdentityInput revisionDeletedBy: CmsIdentityInput + revisionRestoredBy: CmsIdentityInput revisionFirstPublishedBy: CmsIdentityInput revisionLastPublishedBy: CmsIdentityInput @@ -277,6 +285,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -307,6 +322,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -343,6 +362,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -373,6 +399,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -491,6 +521,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -503,6 +535,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC @@ -578,6 +612,8 @@ export default /* GraphQL */ ` deleteProductApiSingular(revision: ID!, options: CmsDeleteEntryOptions): CmsDeleteResponse + restoreProductApiSingularFromBin(revision: ID!): ProductApiSingularResponse + deleteMultipleProductPluralApiName(entries: [ID!]!): CmsDeleteMultipleResponse! publishProductApiSingular(revision: ID!): ProductApiSingularResponse diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts index 7b47aa16309..7627bc7d802 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts @@ -11,24 +11,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -186,6 +190,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -216,6 +227,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -252,6 +267,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -282,6 +304,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -375,6 +401,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -387,6 +415,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts index 96bda08ce2f..4f5bb848cd2 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts @@ -10,24 +10,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -70,24 +74,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentityInput modifiedBy: CmsIdentityInput savedBy: CmsIdentityInput deletedBy: CmsIdentityInput + restoredBy: CmsIdentityInput firstPublishedBy: CmsIdentityInput lastPublishedBy: CmsIdentityInput revisionCreatedOn: DateTime revisionModifiedOn: DateTime revisionSavedOn: DateTime revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentityInput revisionModifiedBy: CmsIdentityInput revisionSavedBy: CmsIdentityInput revisionDeletedBy: CmsIdentityInput + revisionRestoredBy: CmsIdentityInput revisionFirstPublishedBy: CmsIdentityInput revisionLastPublishedBy: CmsIdentityInput @@ -144,6 +152,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -174,6 +189,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -210,6 +229,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -240,6 +266,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -315,6 +345,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -327,6 +359,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC @@ -392,6 +426,8 @@ export default /* GraphQL */ ` deleteReviewApiModel(revision: ID!, options: CmsDeleteEntryOptions): CmsDeleteResponse + restoreReviewApiModelFromBin(revision: ID!): ReviewApiModelResponse + deleteMultipleReviewsApiModel(entries: [ID!]!): CmsDeleteMultipleResponse! publishReviewApiModel(revision: ID!): ReviewApiModelResponse diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts index 14dcd6c7485..04730ee0802 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts @@ -11,24 +11,28 @@ export default /* GraphQL */ ` modifiedOn: DateTime savedOn: DateTime! deletedOn: DateTime + restoredOn: DateTime firstPublishedOn: DateTime lastPublishedOn: DateTime createdBy: CmsIdentity! modifiedBy: CmsIdentity savedBy: CmsIdentity! deletedBy: CmsIdentity + restoredBy: CmsIdentity firstPublishedBy: CmsIdentity lastPublishedBy: CmsIdentity revisionCreatedOn: DateTime! revisionModifiedOn: DateTime revisionSavedOn: DateTime! revisionDeletedOn: DateTime + revisionRestoredOn: DateTime revisionFirstPublishedOn: DateTime revisionLastPublishedOn: DateTime revisionCreatedBy: CmsIdentity! revisionModifiedBy: CmsIdentity revisionSavedBy: CmsIdentity! revisionDeletedBy: CmsIdentity + revisionRestoredBy: CmsIdentity revisionFirstPublishedBy: CmsIdentity revisionLastPublishedBy: CmsIdentity @@ -82,6 +86,13 @@ export default /* GraphQL */ ` deletedOn_lte: DateTime deletedOn_between: [DateTime!] deletedOn_not_between: [DateTime!] + restoredOn: DateTime + restoredOn_gt: DateTime + restoredOn_gte: DateTime + restoredOn_lt: DateTime + restoredOn_lte: DateTime + restoredOn_between: [DateTime!] + restoredOn_not_between: [DateTime!] firstPublishedOn: DateTime firstPublishedOn_gt: DateTime firstPublishedOn_gte: DateTime @@ -112,6 +123,10 @@ export default /* GraphQL */ ` deletedBy_not: ID deletedBy_in: [ID!] deletedBy_not_in: [ID!] + restoredBy: ID + restoredBy_not: ID + restoredBy_in: [ID!] + restoredBy_not_in: [ID!] firstPublishedBy: ID firstPublishedBy_not: ID firstPublishedBy_in: [ID!] @@ -148,6 +163,13 @@ export default /* GraphQL */ ` revisionDeletedOn_lte: DateTime revisionDeletedOn_between: [DateTime!] revisionDeletedOn_not_between: [DateTime!] + revisionRestoredOn: DateTime + revisionRestoredOn_gt: DateTime + revisionRestoredOn_gte: DateTime + revisionRestoredOn_lt: DateTime + revisionRestoredOn_lte: DateTime + revisionRestoredOn_between: [DateTime!] + revisionRestoredOn_not_between: [DateTime!] revisionFirstPublishedOn: DateTime revisionFirstPublishedOn_gt: DateTime revisionFirstPublishedOn_gte: DateTime @@ -178,6 +200,10 @@ export default /* GraphQL */ ` revisionDeletedBy_not: ID revisionDeletedBy_in: [ID!] revisionDeletedBy_not_in: [ID!] + revisionRestoredBy: ID + revisionRestoredBy_not: ID + revisionRestoredBy_in: [ID!] + revisionRestoredBy_not_in: [ID!] revisionFirstPublishedBy: ID revisionFirstPublishedBy_not: ID revisionFirstPublishedBy_in: [ID!] @@ -228,6 +254,8 @@ export default /* GraphQL */ ` savedOn_DESC deletedOn_ASC deletedOn_DESC + restoredOn_ASC + restoredOn_DESC firstPublishedOn_ASC firstPublishedOn_DESC lastPublishedOn_ASC @@ -240,6 +268,8 @@ export default /* GraphQL */ ` revisionSavedOn_DESC revisionDeletedOn_ASC revisionDeletedOn_DESC + revisionRestoredOn_ASC + revisionRestoredOn_DESC revisionFirstPublishedOn_ASC revisionFirstPublishedOn_DESC revisionLastPublishedOn_ASC diff --git a/packages/api-headless-cms/__tests__/contentAPI/sorting.test.ts b/packages/api-headless-cms/__tests__/contentAPI/sorting.test.ts index 6c3b6a1d844..7a48716f0c7 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/sorting.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/sorting.test.ts @@ -81,7 +81,7 @@ describe("sorting + cursor", () => { ...manageOpts }); - const filterOutFields = ["meta", "deletedOn", "deletedBy"]; + const filterOutFields = ["meta", "deletedOn", "deletedBy", "restoredOn", "restoredBy"]; const createAndPublishFruit = async (data: any) => { const [response] = await createFruit({ diff --git a/packages/api-headless-cms/__tests__/helpers/renderSortEnum.test.ts b/packages/api-headless-cms/__tests__/helpers/renderSortEnum.test.ts index b90abc6105e..35aea8de232 100644 --- a/packages/api-headless-cms/__tests__/helpers/renderSortEnum.test.ts +++ b/packages/api-headless-cms/__tests__/helpers/renderSortEnum.test.ts @@ -40,6 +40,8 @@ describe("Render GraphQL sort enum", () => { "savedOn_DESC", "deletedOn_ASC", "deletedOn_DESC", + "restoredOn_ASC", + "restoredOn_DESC", "firstPublishedOn_ASC", "firstPublishedOn_DESC", "lastPublishedOn_ASC", @@ -52,6 +54,8 @@ describe("Render GraphQL sort enum", () => { "revisionSavedOn_DESC", "revisionDeletedOn_ASC", "revisionDeletedOn_DESC", + "revisionRestoredOn_ASC", + "revisionRestoredOn_DESC", "revisionFirstPublishedOn_ASC", "revisionFirstPublishedOn_DESC", "revisionLastPublishedOn_ASC", @@ -98,6 +102,8 @@ describe("Render GraphQL sort enum", () => { "savedOn_DESC", "deletedOn_ASC", "deletedOn_DESC", + "restoredOn_ASC", + "restoredOn_DESC", "firstPublishedOn_ASC", "firstPublishedOn_DESC", "lastPublishedOn_ASC", @@ -110,6 +116,8 @@ describe("Render GraphQL sort enum", () => { "revisionSavedOn_DESC", "revisionDeletedOn_ASC", "revisionDeletedOn_DESC", + "revisionRestoredOn_ASC", + "revisionRestoredOn_DESC", "revisionFirstPublishedOn_ASC", "revisionFirstPublishedOn_DESC", "revisionLastPublishedOn_ASC", diff --git a/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts b/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts index f711759294a..b0a1dac2443 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts @@ -19,10 +19,12 @@ const categoryFields = ` firstPublishedOn lastPublishedOn deletedOn + restoredOn createdBy ${identityFields} modifiedBy ${identityFields} savedBy ${identityFields} deletedBy ${identityFields} + restoredBy ${identityFields} meta { title modelId @@ -195,6 +197,19 @@ const deleteCategoryMutation = (model: CmsModel) => { `; }; +const restoreCategoryFromBinMutation = (model: CmsModel) => { + return /* GraphQL */ ` + mutation RestoreCategoryFromBin($revision: ID!) { + restoreCategoryFromBin: restore${model.singularApiName}FromBin(revision: $revision) { + data { + ${categoryFields} + } + ${errorFields} + } + } + `; +}; + const deleteCategoriesMutation = (model: CmsModel) => { return /* GraphQL */ ` mutation DeleteCategories($entries: [ID!]!) { @@ -332,6 +347,15 @@ export const useCategoryManageHandler = (params: GraphQLHandlerParams) => { headers }); }, + async restoreCategoryFromBin( + variables: Record, + headers: Record = {} + ) { + return await contentHandler.invoke({ + body: { query: restoreCategoryFromBinMutation(model), variables }, + headers + }); + }, async deleteCategories(entries: string[]) { return await contentHandler.invoke({ body: { diff --git a/packages/api-headless-cms/src/constants.ts b/packages/api-headless-cms/src/constants.ts index ee6cf3e97f6..b218cfa352d 100644 --- a/packages/api-headless-cms/src/constants.ts +++ b/packages/api-headless-cms/src/constants.ts @@ -9,12 +9,14 @@ export const ENTRY_META_FIELDS = [ "modifiedOn", "savedOn", "deletedOn", + "restoredOn", "firstPublishedOn", "lastPublishedOn", "createdBy", "modifiedBy", "savedBy", "deletedBy", + "restoredBy", "firstPublishedBy", "lastPublishedBy", @@ -23,12 +25,14 @@ export const ENTRY_META_FIELDS = [ "revisionModifiedOn", "revisionSavedOn", "revisionDeletedOn", + "revisionRestoredOn", "revisionFirstPublishedOn", "revisionLastPublishedOn", "revisionCreatedBy", "revisionModifiedBy", "revisionSavedBy", "revisionDeletedBy", + "revisionRestoredBy", "revisionFirstPublishedBy", "revisionLastPublishedBy" ] as const; @@ -40,12 +44,14 @@ export interface RecordWithEntryMetaFields { revisionSavedOn: string; revisionModifiedOn: string | null; revisionDeletedOn: string | null; + revisionRestoredOn: string | null; revisionFirstPublishedOn: string | null; revisionLastPublishedOn: string | null; revisionCreatedBy: CmsIdentity; revisionSavedBy: CmsIdentity; revisionModifiedBy: CmsIdentity | null; revisionDeletedBy: CmsIdentity | null; + revisionRestoredBy: CmsIdentity | null; revisionFirstPublishedBy: CmsIdentity | null; revisionLastPublishedBy: CmsIdentity | null; @@ -54,12 +60,14 @@ export interface RecordWithEntryMetaFields { savedOn: string; modifiedOn: string | null; deletedOn: string | null; + restoredOn: string | null; firstPublishedOn: string | null; lastPublishedOn: string | null; createdBy: CmsIdentity; savedBy: CmsIdentity; modifiedBy: CmsIdentity | null; deletedBy: CmsIdentity | null; + restoredBy: CmsIdentity | null; firstPublishedBy: CmsIdentity | null; lastPublishedBy: CmsIdentity | null; } @@ -89,7 +97,8 @@ export const isNullableEntryMetaField = (fieldName: EntryMetaFieldName) => { return ( lcFieldName.includes("modified") || lcFieldName.includes("published") || - lcFieldName.includes("deleted") + lcFieldName.includes("deleted") || + lcFieldName.includes("restored") ); }; @@ -116,3 +125,16 @@ export const isEntryLevelEntryMetaField = (fieldName: string) => { !fieldName.startsWith("revision") ); }; + +export const isDeletedEntryMetaField = (fieldName: string) => { + return ( + ENTRY_META_FIELDS.includes(fieldName as EntryMetaFieldName) && fieldName.includes("deleted") + ); +}; + +export const isRestoredEntryMetaField = (fieldName: string) => { + return ( + ENTRY_META_FIELDS.includes(fieldName as EntryMetaFieldName) && + fieldName.includes("restored") + ); +}; diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index 72ca1147348..37e366b0dba 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -20,6 +20,7 @@ import { OnEntryAfterMoveTopicParams, OnEntryAfterPublishTopicParams, OnEntryAfterRepublishTopicParams, + OnEntryAfterRestoreFromBinTopicParams, OnEntryAfterUnpublishTopicParams, OnEntryAfterUpdateTopicParams, OnEntryBeforeCreateTopicParams, @@ -29,6 +30,7 @@ import { OnEntryBeforeMoveTopicParams, OnEntryBeforePublishTopicParams, OnEntryBeforeRepublishTopicParams, + OnEntryBeforeRestoreFromBinTopicParams, OnEntryBeforeUnpublishTopicParams, OnEntryBeforeUpdateTopicParams, OnEntryCreateErrorTopicParams, @@ -38,6 +40,7 @@ import { OnEntryMoveErrorTopicParams, OnEntryPublishErrorTopicParams, OnEntryRepublishErrorTopicParams, + OnEntryRestoreFromBinErrorTopicParams, OnEntryRevisionAfterCreateTopicParams, OnEntryRevisionAfterDeleteTopicParams, OnEntryRevisionBeforeCreateTopicParams, @@ -78,7 +81,8 @@ import { getLatestRevisionByEntryIdUseCases, getPreviousRevisionByEntryIdUseCases, getPublishedRevisionByEntryIdUseCases, - deleteEntryUseCases + deleteEntryUseCases, + restoreEntryFromBinUseCases } from "~/crud/contentEntry/useCases"; import { ContentEntryTraverser } from "~/utils/contentEntryTraverser/ContentEntryTraverser"; @@ -184,6 +188,19 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm const onEntryAfterDelete = createTopic("cms.onEntryAfterDelete"); const onEntryDeleteError = createTopic("cms.onEntryDeleteError"); + /** + * Restore from bin + */ + const onEntryBeforeRestoreFromBin = createTopic( + "cms.onEntryBeforeRestoreFromBin" + ); + const onEntryAfterRestoreFromBin = createTopic( + "cms.onEntryAfterRestoreFromBin" + ); + const onEntryRestoreFromBinError = createTopic( + "cms.onEntryRestoreFromBinError" + ); + /** * Delete revision */ @@ -294,10 +311,13 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm /** * Get latest revision by entryId */ - const { getLatestRevisionByEntryIdUseCase, getLatestRevisionByEntryIdWithDeletedUseCase } = - getLatestRevisionByEntryIdUseCases({ - operation: storageOperations.entries.getLatestRevisionByEntryId - }); + const { + getLatestRevisionByEntryIdUseCase, + getLatestRevisionByEntryIdWithDeletedUseCase, + getLatestRevisionByEntryIdDeletedUseCase + } = getLatestRevisionByEntryIdUseCases({ + operation: storageOperations.entries.getLatestRevisionByEntryId + }); /** * Get previous revision by entryId @@ -329,6 +349,22 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm } ); + /** + * Restore entry from bin + */ + const { restoreEntryFromBinUseCase } = restoreEntryFromBinUseCases({ + restoreOperation: storageOperations.entries.restoreFromBin, + getEntry: getLatestRevisionByEntryIdDeletedUseCase, + getIdentity: getSecurityIdentity, + topics: { + onEntryBeforeRestoreFromBin, + onEntryAfterRestoreFromBin, + onEntryRestoreFromBinError + }, + accessControl, + context + }); + const getEntryById: CmsEntryContext["getEntryById"] = async (model, id) => { const where: CmsEntryListWhere = { id @@ -1195,6 +1231,10 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm onEntryAfterDelete, onEntryDeleteError, + onEntryBeforeRestoreFromBin, + onEntryAfterRestoreFromBin, + onEntryRestoreFromBinError, + onEntryRevisionBeforeDelete, onEntryRevisionAfterDelete, onEntryRevisionDeleteError, @@ -1373,6 +1413,14 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm return deleteEntry(model, entryId, options); }); }, + async restoreEntryFromBin(model, entryId) { + return context.benchmark.measure( + "headlessCms.crud.entries.restoreEntryFromBin", + async () => { + return await restoreEntryFromBinUseCase.execute(model, entryId); + } + ); + }, async deleteMultipleEntries(model, ids) { return context.benchmark.measure( "headlessCms.crud.entries.deleteMultipleEntries", diff --git a/packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBin.ts b/packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBin.ts new file mode 100644 index 00000000000..43683010f42 --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBin.ts @@ -0,0 +1,5 @@ +import { CmsEntry, CmsModel } from "~/types"; + +export interface IRestoreEntryFromBin { + execute: (model: CmsModel, id: string) => Promise; +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBinOperation.ts b/packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBinOperation.ts new file mode 100644 index 00000000000..480d85401bf --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/abstractions/IRestoreEntryFromBinOperation.ts @@ -0,0 +1,8 @@ +import { CmsEntryStorageOperationsRestoreFromBinParams, CmsModel, CmsStorageEntry } from "~/types"; + +export interface IRestoreEntryFromBinOperation { + execute: ( + model: CmsModel, + options: CmsEntryStorageOperationsRestoreFromBinParams + ) => Promise; +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/abstractions/index.ts b/packages/api-headless-cms/src/crud/contentEntry/abstractions/index.ts index d5c76fa680b..31acf505780 100644 --- a/packages/api-headless-cms/src/crud/contentEntry/abstractions/index.ts +++ b/packages/api-headless-cms/src/crud/contentEntry/abstractions/index.ts @@ -12,3 +12,5 @@ export * from "./IGetRevisionsByEntryId"; export * from "./IListEntries"; export * from "./IListEntriesOperation"; export * from "./IMoveEntryToBinOperation"; +export * from "./IRestoreEntryFromBin"; +export * from "./IRestoreEntryFromBinOperation"; diff --git a/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createEntryData.ts b/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createEntryData.ts index 141963996fe..6e26ad26e22 100644 --- a/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createEntryData.ts +++ b/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createEntryData.ts @@ -146,10 +146,12 @@ export const createEntryData = async ({ modifiedOn: getDate(rawInput.modifiedOn, null), savedOn: getDate(rawInput.savedOn, currentDateTime), deletedOn: getDate(rawInput.deletedOn, null), + restoredOn: getDate(rawInput.restoredOn, null), createdBy: getIdentity(rawInput.createdBy, currentIdentity), modifiedBy: getIdentity(rawInput.modifiedBy, null), savedBy: getIdentity(rawInput.savedBy, currentIdentity), deletedBy: getIdentity(rawInput.deletedBy, null), + restoredBy: getIdentity(rawInput.restoredBy, null), ...entryLevelPublishingMetaFields, /** @@ -159,10 +161,12 @@ export const createEntryData = async ({ revisionModifiedOn: getDate(rawInput.revisionModifiedOn, null), revisionSavedOn: getDate(rawInput.revisionSavedOn, currentDateTime), revisionDeletedOn: getDate(rawInput.revisionDeletedOn, null), + revisionRestoredOn: getDate(rawInput.revisionRestoredOn, null), revisionCreatedBy: getIdentity(rawInput.revisionCreatedBy, currentIdentity), revisionModifiedBy: getIdentity(rawInput.revisionModifiedBy, null), revisionSavedBy: getIdentity(rawInput.revisionSavedBy, currentIdentity), revisionDeletedBy: getIdentity(rawInput.revisionDeletedBy, null), + revisionRestoredBy: getIdentity(rawInput.revisionRestoredBy, null), ...revisionLevelPublishingMetaFields, version, diff --git a/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createUpdateEntryData.ts b/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createUpdateEntryData.ts index da3844ba673..8776a616723 100644 --- a/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createUpdateEntryData.ts +++ b/packages/api-headless-cms/src/crud/contentEntry/entryDataFactories/createUpdateEntryData.ts @@ -93,6 +93,7 @@ export const createUpdateEntryData = async ({ revisionModifiedOn: getDate(rawInput.revisionModifiedOn, currentDateTime), revisionSavedOn: getDate(rawInput.revisionSavedOn, currentDateTime), revisionDeletedOn: getDate(rawInput.revisionDeletedOn, null), + revisionRestoredOn: getDate(rawInput.revisionRestoredOn, null), revisionFirstPublishedOn: getDate( rawInput.revisionFirstPublishedOn, originalEntry.revisionFirstPublishedOn @@ -105,6 +106,7 @@ export const createUpdateEntryData = async ({ revisionModifiedBy: getIdentity(rawInput.revisionModifiedBy, currentIdentity), revisionSavedBy: getIdentity(rawInput.revisionSavedBy, currentIdentity), revisionDeletedBy: getIdentity(rawInput.revisionSavedBy, null), + revisionRestoredBy: getIdentity(rawInput.revisionRestoredBy, null), revisionFirstPublishedBy: getIdentity( rawInput.revisionFirstPublishedBy, originalEntry.revisionFirstPublishedBy @@ -123,12 +125,14 @@ export const createUpdateEntryData = async ({ savedOn: getDate(rawInput.savedOn, currentDateTime), modifiedOn: getDate(rawInput.modifiedOn, currentDateTime), deletedOn: getDate(rawInput.deletedOn, null), + restoredOn: getDate(rawInput.restoredOn, null), firstPublishedOn: getDate(rawInput.firstPublishedOn, originalEntry.firstPublishedOn), lastPublishedOn: getDate(rawInput.lastPublishedOn, originalEntry.lastPublishedOn), createdBy: getIdentity(rawInput.createdBy, originalEntry.createdBy), savedBy: getIdentity(rawInput.savedBy, currentIdentity), modifiedBy: getIdentity(rawInput.modifiedBy, currentIdentity), deletedBy: getIdentity(rawInput.deletedBy, null), + restoredBy: getIdentity(rawInput.restoredBy, null), firstPublishedBy: getIdentity(rawInput.firstPublishedBy, originalEntry.firstPublishedBy), lastPublishedBy: getIdentity(rawInput.lastPublishedBy, originalEntry.lastPublishedBy), diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/DeleteEntry/TransformEntryMoveToBin.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/DeleteEntry/TransformEntryMoveToBin.ts index a918f9f07d7..2fc1e9d1cbf 100644 --- a/packages/api-headless-cms/src/crud/contentEntry/useCases/DeleteEntry/TransformEntryMoveToBin.ts +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/DeleteEntry/TransformEntryMoveToBin.ts @@ -4,6 +4,7 @@ import { getDate } from "~/utils/date"; import { getIdentity } from "~/utils/identity"; import { validateModelEntryDataOrThrow } from "~/crud/contentEntry/entryDataValidation"; import { CmsContext, CmsEntry, CmsEntryStorageOperationsMoveToBinParams, CmsModel } from "~/types"; +import { ROOT_FOLDER } from "~/constants"; export class TransformEntryMoveToBin { private context: CmsContext; @@ -42,6 +43,14 @@ export class TransformEntryMoveToBin { ...originalEntry, deleted: true, + /** + * Entry location fields. 👇 + */ + location: { + folderId: ROOT_FOLDER + }, + binOriginalFolderId: originalEntry.location?.folderId, + /** * Entry-level meta fields. 👇 */ diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/GetLatestRevisionByEntryIdDeleted.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/GetLatestRevisionByEntryIdDeleted.ts new file mode 100644 index 00000000000..412829c1cae --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/GetLatestRevisionByEntryIdDeleted.ts @@ -0,0 +1,20 @@ +import { IGetLatestRevisionByEntryId } from "../../abstractions"; +import { CmsEntryStorageOperationsGetLatestRevisionParams, CmsModel } from "~/types"; + +export class GetLatestRevisionByEntryIdDeleted implements IGetLatestRevisionByEntryId { + private getLatestRevisionByEntryId: IGetLatestRevisionByEntryId; + + constructor(getLatestRevisionByEntryId: IGetLatestRevisionByEntryId) { + this.getLatestRevisionByEntryId = getLatestRevisionByEntryId; + } + + async execute(model: CmsModel, params: CmsEntryStorageOperationsGetLatestRevisionParams) { + const entry = await this.getLatestRevisionByEntryId.execute(model, params); + + if (!entry || !entry.deleted) { + return null; + } + + return entry; + } +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/index.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/index.ts index ed6adbb9be1..8dc6ef2778c 100644 --- a/packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/index.ts +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/GetLatestRevisionByEntryId/index.ts @@ -1,4 +1,5 @@ import { GetLatestRevisionByEntryId } from "./GetLatestRevisionByEntryId"; +import { GetLatestRevisionByEntryIdDeleted } from "./GetLatestRevisionByEntryIdDeleted"; import { GetLatestRevisionByEntryIdNotDeleted } from "./GetLatestRevisionByEntryIdNotDeleted"; import { CmsEntryStorageOperations } from "~/types"; @@ -13,9 +14,13 @@ export const getLatestRevisionByEntryIdUseCases = ( const getLatestRevisionByEntryIdNotDeleted = new GetLatestRevisionByEntryIdNotDeleted( getLatestRevisionByEntryId ); + const getLatestRevisionByEntryIdDeleted = new GetLatestRevisionByEntryIdDeleted( + getLatestRevisionByEntryId + ); return { getLatestRevisionByEntryIdUseCase: getLatestRevisionByEntryIdNotDeleted, - getLatestRevisionByEntryIdWithDeletedUseCase: getLatestRevisionByEntryId + getLatestRevisionByEntryIdWithDeletedUseCase: getLatestRevisionByEntryId, + getLatestRevisionByEntryIdDeletedUseCase: getLatestRevisionByEntryIdDeleted }; }; diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBin.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBin.ts new file mode 100644 index 00000000000..58b68591a89 --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBin.ts @@ -0,0 +1,38 @@ +import { NotFoundError } from "@webiny/handler-graphql"; +import { + IGetLatestRevisionByEntryId, + IRestoreEntryFromBin, + IRestoreEntryFromBinOperation +} from "~/crud/contentEntry/abstractions"; +import { TransformEntryRestoreFromBin } from "./TransformEntryRestoreFromBin"; +import { CmsModel } from "~/types"; +import { parseIdentifier } from "@webiny/utils"; + +export class RestoreEntryFromBin implements IRestoreEntryFromBin { + private getEntry: IGetLatestRevisionByEntryId; + private transformEntry: TransformEntryRestoreFromBin; + private restoreEntry: IRestoreEntryFromBinOperation; + + constructor( + getEntry: IGetLatestRevisionByEntryId, + transformEntry: TransformEntryRestoreFromBin, + restoreEntry: IRestoreEntryFromBinOperation + ) { + this.getEntry = getEntry; + this.transformEntry = transformEntry; + this.restoreEntry = restoreEntry; + } + + async execute(model: CmsModel, id: string) { + const { id: entryId } = parseIdentifier(id); + const entryToRestore = await this.getEntry.execute(model, { id: entryId }); + + if (!entryToRestore) { + throw new NotFoundError(`Entry "${id}" was not found!`); + } + + const { entry, storageEntry } = await this.transformEntry.execute(model, entryToRestore); + + return await this.restoreEntry.execute(model, { entry, storageEntry }); + } +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperation.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperation.ts new file mode 100644 index 00000000000..97acc69f221 --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperation.ts @@ -0,0 +1,18 @@ +import { IRestoreEntryFromBinOperation } from "~/crud/contentEntry/abstractions"; +import { + CmsEntryStorageOperations, + CmsEntryStorageOperationsRestoreFromBinParams, + CmsModel +} from "~/types"; + +export class RestoreEntryFromBinOperation implements IRestoreEntryFromBinOperation { + private operation: CmsEntryStorageOperations["restoreFromBin"]; + + constructor(operation: CmsEntryStorageOperations["restoreFromBin"]) { + this.operation = operation; + } + + async execute(model: CmsModel, params: CmsEntryStorageOperationsRestoreFromBinParams) { + return await this.operation(model, params); + } +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperationWithEvents.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperationWithEvents.ts new file mode 100644 index 00000000000..ff2e493b0eb --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinOperationWithEvents.ts @@ -0,0 +1,51 @@ +import { IRestoreEntryFromBinOperation } from "~/crud/contentEntry/abstractions"; + +import { RestoreEntryFromBinUseCasesTopics } from "./index"; +import { CmsEntryStorageOperationsRestoreFromBinParams, CmsModel } from "~/types"; +import WebinyError from "@webiny/error"; + +export class RestoreEntryFromBinOperationWithEvents implements IRestoreEntryFromBinOperation { + private topics: RestoreEntryFromBinUseCasesTopics; + private operation: IRestoreEntryFromBinOperation; + + constructor( + topics: RestoreEntryFromBinUseCasesTopics, + operation: IRestoreEntryFromBinOperation + ) { + this.topics = topics; + this.operation = operation; + } + + async execute(model: CmsModel, params: CmsEntryStorageOperationsRestoreFromBinParams) { + const entry = params.entry; + try { + await this.topics.onEntryBeforeRestoreFromBin.publish({ + entry, + model + }); + + const result = await this.operation.execute(model, params); + + await this.topics.onEntryAfterRestoreFromBin.publish({ + entry, + storageEntry: result, + model + }); + + return result; + } catch (ex) { + await this.topics.onEntryRestoreFromBinError.publish({ + entry, + model, + error: ex + }); + throw new WebinyError( + ex.message || "Could not restore entry from bin.", + ex.code || "RESTORE_FROM_BIN_ERROR", + { + entry + } + ); + } + } +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinSecure.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinSecure.ts new file mode 100644 index 00000000000..29f2b78ef29 --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/RestoreEntryFromBinSecure.ts @@ -0,0 +1,18 @@ +import { IRestoreEntryFromBin } from "~/crud/contentEntry/abstractions"; +import { AccessControl } from "~/crud/AccessControl/AccessControl"; +import { CmsModel } from "~/types"; + +export class RestoreEntryFromBinSecure implements IRestoreEntryFromBin { + private accessControl: AccessControl; + private useCase: IRestoreEntryFromBin; + + constructor(accessControl: AccessControl, useCase: IRestoreEntryFromBin) { + this.accessControl = accessControl; + this.useCase = useCase; + } + + async execute(model: CmsModel, id: string) { + await this.accessControl.ensureCanAccessEntry({ model, rwd: "d" }); + return await this.useCase.execute(model, id); + } +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/TransformEntryRestoreFromBin.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/TransformEntryRestoreFromBin.ts new file mode 100644 index 00000000000..6a8503403b7 --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/TransformEntryRestoreFromBin.ts @@ -0,0 +1,68 @@ +import { SecurityIdentity } from "@webiny/api-security/types"; +import { entryFromStorageTransform, entryToStorageTransform } from "~/utils/entryStorage"; +import { getDate } from "~/utils/date"; +import { getIdentity } from "~/utils/identity"; +import { validateModelEntryDataOrThrow } from "~/crud/contentEntry/entryDataValidation"; +import { CmsContext, CmsEntry, CmsEntryStorageOperationsMoveToBinParams, CmsModel } from "~/types"; + +export class TransformEntryRestoreFromBin { + private context: CmsContext; + private getIdentity: () => SecurityIdentity; + + constructor(context: CmsContext, getIdentity: () => SecurityIdentity) { + this.context = context; + this.getIdentity = getIdentity; + } + async execute( + model: CmsModel, + initialEntry: CmsEntry + ): Promise { + const originalEntry = await entryFromStorageTransform(this.context, model, initialEntry); + const entry = await this.createRestoreFromBinEntryData(model, originalEntry); + const storageEntry = await entryToStorageTransform(this.context, model, entry); + + return { + entry, + storageEntry + }; + } + + private async createRestoreFromBinEntryData(model: CmsModel, originalEntry: CmsEntry) { + await validateModelEntryDataOrThrow({ + context: this.context, + model, + data: originalEntry.values, + entry: originalEntry + }); + + const currentDateTime = new Date().toISOString(); + const currentIdentity = this.getIdentity(); + + const entry: CmsEntry = { + ...originalEntry, + deleted: false, + + /** + * Entry location fields. 👇 + */ + location: { + folderId: originalEntry.binOriginalFolderId + }, + binOriginalFolderId: null, + + /** + * Entry-level meta fields. 👇 + */ + restoredOn: getDate(currentDateTime, null), + restoredBy: getIdentity(currentIdentity, null), + + /** + * Revision-level meta fields. 👇 + */ + revisionRestoredOn: getDate(currentDateTime, null), + revisionRestoredBy: getIdentity(currentIdentity, null) + }; + + return entry; + } +} diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/index.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/index.ts new file mode 100644 index 00000000000..a03caa65f28 --- /dev/null +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/RestoreEntryFromBin/index.ts @@ -0,0 +1,50 @@ +import { Topic } from "@webiny/pubsub/types"; +import { + CmsContext, + CmsEntryStorageOperations, + OnEntryAfterRestoreFromBinTopicParams, + OnEntryBeforeRestoreFromBinTopicParams, + OnEntryRestoreFromBinErrorTopicParams +} from "~/types"; +import { IGetLatestRevisionByEntryId } from "~/crud/contentEntry/abstractions"; +import { AccessControl } from "~/crud/AccessControl/AccessControl"; +import { SecurityIdentity } from "@webiny/api-security/types"; +import { RestoreEntryFromBinOperation } from "./RestoreEntryFromBinOperation"; +import { RestoreEntryFromBinOperationWithEvents } from "./RestoreEntryFromBinOperationWithEvents"; +import { TransformEntryRestoreFromBin } from "./TransformEntryRestoreFromBin"; +import { RestoreEntryFromBin } from "./RestoreEntryFromBin"; +import { RestoreEntryFromBinSecure } from "./RestoreEntryFromBinSecure"; + +export interface RestoreEntryFromBinUseCasesTopics { + onEntryBeforeRestoreFromBin: Topic; + onEntryAfterRestoreFromBin: Topic; + onEntryRestoreFromBinError: Topic; +} + +interface RestoreEntryFromBinUseCasesParams { + restoreOperation: CmsEntryStorageOperations["restoreFromBin"]; + getEntry: IGetLatestRevisionByEntryId; + accessControl: AccessControl; + topics: RestoreEntryFromBinUseCasesTopics; + context: CmsContext; + getIdentity: () => SecurityIdentity; +} + +export const restoreEntryFromBinUseCases = (params: RestoreEntryFromBinUseCasesParams) => { + const restoreEntryOperation = new RestoreEntryFromBinOperation(params.restoreOperation); + const restoreEntryOperationWithEvents = new RestoreEntryFromBinOperationWithEvents( + params.topics, + restoreEntryOperation + ); + const restoreTransform = new TransformEntryRestoreFromBin(params.context, params.getIdentity); + const restoreEntry = new RestoreEntryFromBin( + params.getEntry, + restoreTransform, + restoreEntryOperationWithEvents + ); + const restoreEntrySecure = new RestoreEntryFromBinSecure(params.accessControl, restoreEntry); + + return { + restoreEntryFromBinUseCase: restoreEntrySecure + }; +}; diff --git a/packages/api-headless-cms/src/crud/contentEntry/useCases/index.ts b/packages/api-headless-cms/src/crud/contentEntry/useCases/index.ts index 2a3aee9ad0e..0ae78a36603 100644 --- a/packages/api-headless-cms/src/crud/contentEntry/useCases/index.ts +++ b/packages/api-headless-cms/src/crud/contentEntry/useCases/index.ts @@ -8,3 +8,4 @@ export * from "./GetPublishedRevisionByEntryId"; export * from "./GetRevisionById"; export * from "./GetRevisionsByEntryId"; export * from "./ListEntries"; +export * from "./RestoreEntryFromBin"; diff --git a/packages/api-headless-cms/src/graphql/schema/createManageResolvers.ts b/packages/api-headless-cms/src/graphql/schema/createManageResolvers.ts index 909bfa8fc99..defdc30ad37 100644 --- a/packages/api-headless-cms/src/graphql/schema/createManageResolvers.ts +++ b/packages/api-headless-cms/src/graphql/schema/createManageResolvers.ts @@ -9,6 +9,7 @@ import { resolveUpdate } from "./resolvers/manage/resolveUpdate"; import { resolveValidate } from "./resolvers/manage/resolveValidate"; import { resolveMove } from "./resolvers/manage/resolveMove"; import { resolveDelete } from "./resolvers/manage/resolveDelete"; +import { resolveRestoreFromBin } from "./resolvers/manage/resolveRestoreFromBin"; import { resolveDeleteMultiple } from "./resolvers/manage/resolveDeleteMultiple"; import { resolvePublish } from "./resolvers/manage/resolvePublish"; import { resolveRepublish } from "./resolvers/manage/resolveRepublish"; @@ -82,6 +83,7 @@ export const createManageResolvers: CreateManageResolvers = ({ [`validate${model.singularApiName}`]: resolveValidate({ model }), [`move${model.singularApiName}`]: resolveMove({ model }), [`delete${model.singularApiName}`]: resolveDelete({ model }), + [`restore${model.singularApiName}FromBin`]: resolveRestoreFromBin({ model }), [`deleteMultiple${model.pluralApiName}`]: resolveDeleteMultiple({ model }), [`publish${model.singularApiName}`]: resolvePublish({ model }), [`republish${model.singularApiName}`]: resolveRepublish({ model }), diff --git a/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts b/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts index 697c2ee16f0..0f612aebeec 100644 --- a/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts +++ b/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts @@ -203,6 +203,8 @@ export const createManageSDL: CreateManageSDL = ({ move${singularName}(revision: ID!, folderId: ID!): ${singularName}MoveResponse delete${singularName}(revision: ID!, options: CmsDeleteEntryOptions): CmsDeleteResponse + + restore${singularName}FromBin(revision: ID!): ${singularName}Response deleteMultiple${pluralName}(entries: [ID!]!): CmsDeleteMultipleResponse! diff --git a/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveRestoreFromBin.ts b/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveRestoreFromBin.ts new file mode 100644 index 00000000000..5356bc8000d --- /dev/null +++ b/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveRestoreFromBin.ts @@ -0,0 +1,19 @@ +import { ErrorResponse, Response } from "@webiny/handler-graphql/responses"; +import { CmsEntryResolverFactory as ResolverFactory } from "~/types"; + +interface ResolveRestoreFromBinArgs { + revision: string; +} + +type ResolveRestoreFromBin = ResolverFactory; + +export const resolveRestoreFromBin: ResolveRestoreFromBin = + ({ model }) => + async (_, args: any, context) => { + try { + const entry = await context.cms.restoreEntryFromBin(model, args.revision); + return new Response(entry); + } catch (ex) { + return new ErrorResponse(ex); + } + }; diff --git a/packages/api-headless-cms/src/types/context.ts b/packages/api-headless-cms/src/types/context.ts index 266a62ed1d2..b05f9077084 100644 --- a/packages/api-headless-cms/src/types/context.ts +++ b/packages/api-headless-cms/src/types/context.ts @@ -22,6 +22,7 @@ import { OnEntryAfterMoveTopicParams, OnEntryAfterPublishTopicParams, OnEntryAfterRepublishTopicParams, + OnEntryAfterRestoreFromBinTopicParams, OnEntryAfterUnpublishTopicParams, OnEntryAfterUpdateTopicParams, OnEntryBeforeCreateTopicParams, @@ -30,6 +31,7 @@ import { OnEntryBeforeMoveTopicParams, OnEntryBeforePublishTopicParams, OnEntryBeforeRepublishTopicParams, + OnEntryBeforeRestoreFromBinTopicParams, OnEntryBeforeUnpublishTopicParams, OnEntryBeforeUpdateTopicParams, OnEntryCreateErrorTopicParams, @@ -38,6 +40,7 @@ import { OnEntryMoveErrorTopicParams, OnEntryPublishErrorTopicParams, OnEntryRepublishErrorTopicParams, + OnEntryRestoreFromBinErrorTopicParams, OnEntryRevisionAfterCreateTopicParams, OnEntryRevisionAfterDeleteTopicParams, OnEntryRevisionBeforeCreateTopicParams, @@ -161,6 +164,10 @@ export interface CmsEntryContext { * Delete entry with all its revisions. */ deleteEntry: (model: CmsModel, id: string, options?: CmsDeleteEntryOptions) => Promise; + /** + * Restore entry from trash bin with all its revisions. + */ + restoreEntryFromBin: (model: CmsModel, id: string) => Promise; /** * Delete multiple entries */ @@ -212,6 +219,10 @@ export interface CmsEntryContext { onEntryAfterDelete: Topic; onEntryDeleteError: Topic; + onEntryBeforeRestoreFromBin: Topic; + onEntryAfterRestoreFromBin: Topic; + onEntryRestoreFromBinError: Topic; + onEntryRevisionBeforeDelete: Topic; onEntryRevisionAfterDelete: Topic; onEntryRevisionDeleteError: Topic; diff --git a/packages/api-headless-cms/src/types/types.ts b/packages/api-headless-cms/src/types/types.ts index 39c3719643d..92bd46a42de 100644 --- a/packages/api-headless-cms/src/types/types.ts +++ b/packages/api-headless-cms/src/types/types.ts @@ -473,6 +473,10 @@ export interface CmsEntry { * An ISO 8601 date/time string. */ revisionDeletedOn: string | null; + /** + * An ISO 8601 date/time string. + */ + revisionRestoredOn: string | null; /** * An ISO 8601 date/time string. */ @@ -498,6 +502,10 @@ export interface CmsEntry { * Identity that last deleted the revision. */ revisionDeletedBy: CmsIdentity | null; + /** + * Identity that last restored the revision. + */ + revisionRestoredBy: CmsIdentity | null; /** * Identity that first published the entry. */ @@ -523,6 +531,10 @@ export interface CmsEntry { * An ISO 8601 date/time string. */ deletedOn: string | null; + /** + * An ISO 8601 date/time string. + */ + restoredOn: string | null; /** * An ISO 8601 date/time string. */ @@ -548,6 +560,10 @@ export interface CmsEntry { * Identity that last deleted the entry. */ deletedBy: CmsIdentity | null; + /** + * Identity that last restored the entry. + */ + restoredBy: CmsIdentity | null; /** * Identity that first published the entry. */ @@ -606,6 +622,11 @@ export interface CmsEntry { * Is the entry in the bin? */ deleted?: boolean | null; + /** + * This field preserves the original folderId value, as the ROOT_FOLDER is set upon deletion. + * The value is utilized when restoring the entry from the trash bin. + */ + binOriginalFolderId?: string | null; } export interface CmsStorageEntry extends CmsEntry { @@ -1146,7 +1167,6 @@ export interface OnEntryMoveErrorTopicParams { /** * Publish */ - export interface OnEntryBeforePublishTopicParams { original: CmsEntry; entry: CmsEntry; @@ -1190,7 +1210,6 @@ export interface OnEntryRepublishErrorTopicParams { /** * Unpublish */ - export interface OnEntryBeforeUnpublishTopicParams { entry: CmsEntry; model: CmsModel; @@ -1208,6 +1227,9 @@ export interface OnEntryUnpublishErrorTopicParams { model: CmsModel; } +/** + * Delete + */ export interface OnEntryBeforeDeleteTopicParams { entry: CmsEntry; model: CmsModel; @@ -1227,6 +1249,29 @@ export interface OnEntryDeleteErrorTopicParams { model: CmsModel; } +/** + * Restore from bin + */ +export interface OnEntryBeforeRestoreFromBinTopicParams { + entry: CmsEntry; + model: CmsModel; +} + +export interface OnEntryAfterRestoreFromBinTopicParams { + entry: CmsEntry; + model: CmsModel; + storageEntry: CmsEntry; +} + +export interface OnEntryRestoreFromBinErrorTopicParams { + error: Error; + entry: CmsEntry; + model: CmsModel; +} + +/** + * Delete Revision + */ export interface OnEntryRevisionBeforeDeleteTopicParams { entry: CmsEntry; model: CmsModel; @@ -1243,6 +1288,9 @@ export interface OnEntryRevisionDeleteErrorTopicParams { model: CmsModel; } +/** + * Delete multiple + */ export interface OnEntryBeforeDeleteMultipleTopicParams { model: CmsModel; entries: CmsEntry[]; @@ -1262,11 +1310,17 @@ export interface OnEntryDeleteMultipleErrorTopicParams { error: Error; } +/** + * Get + */ export interface OnEntryBeforeGetTopicParams { model: CmsModel; where: CmsEntryListWhere; } +/** + * List + */ export interface EntryBeforeListTopicParams { where: CmsEntryListWhere; model: CmsModel; @@ -1287,9 +1341,11 @@ export interface CreateCmsEntryInput { modifiedOn?: Date | string | null; savedOn?: Date | string; deletedOn?: Date | string | null; + restoredOn?: Date | string | null; createdBy?: CmsIdentity; savedBy?: CmsIdentity; deletedBy?: CmsIdentity | null; + restoredBy?: CmsIdentity | null; firstPublishedOn?: Date | string; lastPublishedOn?: Date | string; firstPublishedBy?: CmsIdentity; @@ -1302,10 +1358,12 @@ export interface CreateCmsEntryInput { revisionModifiedOn?: Date | string | null; revisionSavedOn?: Date | string; revisionDeletedOn?: Date | string | null; + revisionRestoredOn?: Date | string | null; revisionCreatedBy?: CmsIdentity; revisionModifiedBy?: CmsIdentity | null; revisionSavedBy?: CmsIdentity; revisionDeletedBy?: CmsIdentity | null; + revisionRestoredBy?: CmsIdentity | null; revisionFirstPublishedOn?: Date | string; revisionLastPublishedOn?: Date | string; revisionFirstPublishedBy?: CmsIdentity; @@ -1374,12 +1432,14 @@ export interface UpdateCmsEntryInput { revisionModifiedOn?: Date | string | null; revisionSavedOn?: Date | string | null; revisionDeletedOn?: Date | string | null; + revisionRestoredOn?: Date | string | null; revisionFirstPublishedOn?: Date | string | null; revisionLastPublishedOn?: Date | string | null; revisionModifiedBy?: CmsIdentity | null; revisionCreatedBy?: CmsIdentity | null; revisionSavedBy?: CmsIdentity | null; revisionDeletedBy?: CmsIdentity | null; + revisionRestoredBy?: CmsIdentity | null; revisionFirstPublishedBy?: CmsIdentity | null; revisionLastPublishedBy?: CmsIdentity | null; @@ -1390,12 +1450,14 @@ export interface UpdateCmsEntryInput { modifiedOn?: Date | string | null; savedOn?: Date | string | null; deletedOn?: Date | string | null; + restoredOn?: Date | string | null; firstPublishedOn?: Date | string | null; lastPublishedOn?: Date | string | null; createdBy?: CmsIdentity | null; modifiedBy?: CmsIdentity | null; savedBy?: CmsIdentity | null; deletedBy?: CmsIdentity | null; + restoredBy?: CmsIdentity | null; firstPublishedBy?: CmsIdentity | null; lastPublishedBy?: CmsIdentity | null; @@ -1746,6 +1808,20 @@ export interface CmsEntryStorageOperationsMoveToBinParams< storageEntry: T; } +export interface CmsEntryStorageOperationsRestoreFromBinParams< + T extends CmsStorageEntry = CmsStorageEntry +> { + /** + * The modified entry that is going to be saved as restored. + * Entry is in its original form. + */ + entry: CmsEntry; + /** + * The modified entry and prepared for the storage. + */ + storageEntry: T; +} + export interface CmsEntryStorageOperationsDeleteEntriesParams { entries: string[]; } @@ -1944,6 +2020,13 @@ export interface CmsEntryStorageOperations Promise; + /** + * Restore the entry from the bin. + */ + restoreFromBin: ( + model: CmsModel, + params: CmsEntryStorageOperationsRestoreFromBinParams + ) => Promise; /** * Delete multiple entries, with a limit on how much can be deleted in one call. */ diff --git a/packages/app-headless-cms-common/src/entries.graphql.ts b/packages/app-headless-cms-common/src/entries.graphql.ts index b3254a43399..e95c3a2a187 100644 --- a/packages/app-headless-cms-common/src/entries.graphql.ts +++ b/packages/app-headless-cms-common/src/entries.graphql.ts @@ -288,23 +288,23 @@ export const createDeleteMutation = (model: CmsEditorContentModel) => { /** * ############################################ - * Restore Mutation + * Restore from bin Mutation */ -export interface CmsEntryRestoreMutationResponse { +export interface CmsEntryRestoreFromBinMutationResponse { content: { data: CmsContentEntry | null; error: CmsErrorResponse | null; }; } -export interface CmsEntryRestoreMutationVariables { +export interface CmsEntryRestoreFromBinMutationVariables { revision: string; } -export const createRestoreMutation = (model: CmsEditorContentModel) => { +export const createRestoreFromBinMutation = (model: CmsEditorContentModel) => { return gql` - mutation CmsEntriesRestore${model.singularApiName}($revision: ID!) { - content: restore${model.singularApiName}(revision: $revision) { + mutation CmsEntriesRestore${model.singularApiName}FromBin($revision: ID!) { + content: restore${model.singularApiName}FromBin(revision: $revision) { data { ${CONTENT_ENTRY_SYSTEM_FIELDS} ${createFieldsList({ model, fields: model.fields })} diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts index 74a2e90b453..b13667848f7 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts @@ -1,9 +1,9 @@ import { ApolloClient } from "apollo-client"; import { CmsContentEntry, CmsModel } from "@webiny/app-headless-cms-common/types"; import { - CmsEntryRestoreMutationResponse, - CmsEntryRestoreMutationVariables, - createRestoreMutation + CmsEntryRestoreFromBinMutationResponse, + CmsEntryRestoreFromBinMutationVariables, + createRestoreFromBinMutation } from "@webiny/app-headless-cms-common"; import { ITrashBinRestoreItemGateway } from "@webiny/app-trash-bin"; @@ -20,17 +20,17 @@ export class TrashBinRestoreItemGraphQLGateway async execute(id: string) { const { data: response } = await this.client.mutate< - CmsEntryRestoreMutationResponse, - CmsEntryRestoreMutationVariables + CmsEntryRestoreFromBinMutationResponse, + CmsEntryRestoreFromBinMutationVariables >({ - mutation: createRestoreMutation(this.model), + mutation: createRestoreFromBinMutation(this.model), variables: { revision: id } }); if (!response) { - throw new Error("Network error while restoring entry."); + throw new Error("Network error while restoring entry from trash bin."); } const { data, error } = response.content; diff --git a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/package.json b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/package.json index afc942eb797..4da6d6d7bdb 100644 --- a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/package.json +++ b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/package.json @@ -20,6 +20,7 @@ "@webiny/api-i18n-ddb": "latest", "@webiny/api-i18n-content": "latest", "@webiny/api-headless-cms": "latest", + "@webiny/api-headless-cms-aco": "latest", "@webiny/api-headless-cms-ddb-es": "latest", "@webiny/api-page-builder": "latest", "@webiny/api-page-builder-aco": "latest", diff --git a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts index 517e047ab8b..fb5ca722c46 100644 --- a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts @@ -30,6 +30,7 @@ import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api- import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; import { createAco } from "@webiny/api-aco"; import { createAcoPageBuilderContext } from "@webiny/api-page-builder-aco"; +import { createAcoHcmsContext } from "@webiny/api-headless-cms-aco"; import securityPlugins from "./security"; import tenantManager from "@webiny/api-tenant-manager"; import { createAuditLogs } from "@webiny/api-audit-logs"; @@ -111,6 +112,7 @@ export const handler = createHandler({ }), createAco(), createAcoPageBuilderContext(), + createAcoHcmsContext(), createAuditLogs(), scaffoldsPlugins() ], diff --git a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/types.ts b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/types.ts index b10411e4512..acf9d317c1c 100644 --- a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/types.ts +++ b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/types.ts @@ -11,6 +11,7 @@ import { FormBuilderContext } from "@webiny/api-form-builder/types"; import { CmsContext } from "@webiny/api-headless-cms/types"; import { AcoContext } from "@webiny/api-aco/types"; import { PbAcoContext } from "@webiny/api-page-builder-aco/types"; +import { HcmsAcoContext } from "@webiny/api-headless-cms-aco/types"; // When working with the `context` object (for example while defining a new GraphQL resolver function), // you can import this interface and assign it to it. This will give you full autocomplete functionality @@ -31,4 +32,5 @@ export interface Context FormBuilderContext, AcoContext, PbAcoContext, + HcmsAcoContext, CmsContext {} diff --git a/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/package.json b/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/package.json index ca9a297c8ac..f1283e30464 100644 --- a/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/package.json +++ b/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/package.json @@ -20,6 +20,7 @@ "@webiny/api-i18n-ddb": "latest", "@webiny/api-i18n-content": "latest", "@webiny/api-headless-cms": "latest", + "@webiny/api-headless-cms-aco": "latest", "@webiny/api-headless-cms-ddb-es": "latest", "@webiny/api-page-builder": "latest", "@webiny/api-page-builder-aco": "latest", diff --git a/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/index.ts index df2b97afb03..2093e25d5d6 100644 --- a/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/index.ts @@ -30,6 +30,7 @@ import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api- import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; import { createAco } from "@webiny/api-aco"; import { createAcoPageBuilderContext } from "@webiny/api-page-builder-aco"; +import { createAcoHcmsContext } from "@webiny/api-headless-cms-aco"; import securityPlugins from "./security"; import tenantManager from "@webiny/api-tenant-manager"; import { createAuditLogs } from "@webiny/api-audit-logs"; @@ -111,6 +112,7 @@ export const handler = createHandler({ }), createAco(), createAcoPageBuilderContext(), + createAcoHcmsContext(), createAuditLogs(), scaffoldsPlugins() ], diff --git a/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/types.ts b/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/types.ts index b10411e4512..acf9d317c1c 100644 --- a/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/types.ts +++ b/packages/cwp-template-aws/template/ddb-os/apps/api/graphql/src/types.ts @@ -11,6 +11,7 @@ import { FormBuilderContext } from "@webiny/api-form-builder/types"; import { CmsContext } from "@webiny/api-headless-cms/types"; import { AcoContext } from "@webiny/api-aco/types"; import { PbAcoContext } from "@webiny/api-page-builder-aco/types"; +import { HcmsAcoContext } from "@webiny/api-headless-cms-aco/types"; // When working with the `context` object (for example while defining a new GraphQL resolver function), // you can import this interface and assign it to it. This will give you full autocomplete functionality @@ -31,4 +32,5 @@ export interface Context FormBuilderContext, AcoContext, PbAcoContext, + HcmsAcoContext, CmsContext {} diff --git a/packages/cwp-template-aws/template/ddb/apps/api/graphql/package.json b/packages/cwp-template-aws/template/ddb/apps/api/graphql/package.json index 40be1f3e2a0..d54702b79d9 100644 --- a/packages/cwp-template-aws/template/ddb/apps/api/graphql/package.json +++ b/packages/cwp-template-aws/template/ddb/apps/api/graphql/package.json @@ -20,6 +20,7 @@ "@webiny/api-i18n-ddb": "latest", "@webiny/api-i18n-content": "latest", "@webiny/api-headless-cms": "latest", + "@webiny/api-headless-cms-aco": "latest", "@webiny/api-headless-cms-ddb": "latest", "@webiny/api-page-builder": "latest", "@webiny/api-page-builder-aco": "latest", diff --git a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts index 74fb20ed071..74271d5a23d 100644 --- a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts @@ -26,6 +26,7 @@ import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api- import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; import { createAco } from "@webiny/api-aco"; import { createAcoPageBuilderContext } from "@webiny/api-page-builder-aco"; +import { createAcoHcmsContext } from "@webiny/api-headless-cms-aco"; import securityPlugins from "./security"; import tenantManager from "@webiny/api-tenant-manager"; import { createAuditLogs } from "@webiny/api-audit-logs"; @@ -96,6 +97,7 @@ export const handler = createHandler({ }), createAco(), createAcoPageBuilderContext(), + createAcoHcmsContext(), createAuditLogs(), scaffoldsPlugins() ], diff --git a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/types.ts b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/types.ts index 0afc073fa70..c019efdd243 100644 --- a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/types.ts +++ b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/types.ts @@ -10,6 +10,7 @@ import { FormBuilderContext } from "@webiny/api-form-builder/types"; import { CmsContext } from "@webiny/api-headless-cms/types"; import { AcoContext } from "@webiny/api-aco/types"; import { PbAcoContext } from "@webiny/api-page-builder-aco/types"; +import { HcmsAcoContext } from "@webiny/api-headless-cms-aco/types"; // When working with the `context` object (for example while defining a new GraphQL resolver function), // you can import this interface and assign it to it. This will give you full autocomplete functionality @@ -29,4 +30,5 @@ export interface Context FormBuilderContext, AcoContext, PbAcoContext, + HcmsAcoContext, CmsContext {} diff --git a/scripts/listPackagesWithTests.js b/scripts/listPackagesWithTests.js index 56fb2b5b397..5fd8a6371d3 100644 --- a/scripts/listPackagesWithTests.js +++ b/scripts/listPackagesWithTests.js @@ -90,6 +90,12 @@ const CUSTOM_HANDLERS = { "packages/api-headless-cms --storage=ddb-es,ddb" ]; }, + "api-headless-cms-aco": () => { + return [ + "packages/api-headless-cms-aco --storage=ddb", + "packages/api-headless-cms-aco --storage=ddb-es,ddb" + ]; + }, "api-headless-cms-ddb-es": () => { return ["packages/api-headless-cms-ddb-es --storage=ddb-es,ddb"]; }, diff --git a/yarn.lock b/yarn.lock index 63307b49b0d..1b3defdd8ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13422,6 +13422,37 @@ __metadata: languageName: unknown linkType: soft +"@webiny/api-headless-cms-aco@0.0.0, @webiny/api-headless-cms-aco@workspace:packages/api-headless-cms-aco": + version: 0.0.0-use.local + resolution: "@webiny/api-headless-cms-aco@workspace:packages/api-headless-cms-aco" + dependencies: + "@babel/cli": ^7.23.9 + "@babel/core": ^7.24.0 + "@babel/preset-env": ^7.24.0 + "@babel/preset-typescript": ^7.23.3 + "@babel/runtime": ^7.24.0 + "@webiny/api": 0.0.0 + "@webiny/api-aco": 0.0.0 + "@webiny/api-admin-users": 0.0.0 + "@webiny/api-headless-cms": 0.0.0 + "@webiny/api-i18n": 0.0.0 + "@webiny/api-security": 0.0.0 + "@webiny/api-tenancy": 0.0.0 + "@webiny/api-wcp": 0.0.0 + "@webiny/cli": 0.0.0 + "@webiny/error": 0.0.0 + "@webiny/handler": 0.0.0 + "@webiny/handler-aws": 0.0.0 + "@webiny/handler-graphql": 0.0.0 + "@webiny/plugins": 0.0.0 + "@webiny/project-utils": 0.0.0 + "@webiny/wcp": 0.0.0 + graphql: ^15.8.0 + ttypescript: ^1.5.13 + typescript: ^4.7.4 + languageName: unknown + linkType: soft + "@webiny/api-headless-cms-ddb-es@0.0.0, @webiny/api-headless-cms-ddb-es@workspace:packages/api-headless-cms-ddb-es": version: 0.0.0-use.local resolution: "@webiny/api-headless-cms-ddb-es@workspace:packages/api-headless-cms-ddb-es" @@ -17954,6 +17985,7 @@ __metadata: "@webiny/api-form-builder": 0.0.0 "@webiny/api-form-builder-so-ddb": 0.0.0 "@webiny/api-headless-cms": 0.0.0 + "@webiny/api-headless-cms-aco": 0.0.0 "@webiny/api-headless-cms-ddb": 0.0.0 "@webiny/api-i18n": 0.0.0 "@webiny/api-i18n-content": 0.0.0