diff --git a/cypress-tests/package.json b/cypress-tests/package.json index 37387e915e6..094e4cfae92 100644 --- a/cypress-tests/package.json +++ b/cypress-tests/package.json @@ -20,7 +20,7 @@ "nanoid": "^3.3.7", "node-fetch": "^2.6.1", "typescript": "4.9.5", - "uniqid": "^5.2.0" + "uniqid": "^5.4.0" }, "scripts": { "cypress:open": "yarn cypress open", diff --git a/packages/api-aco/src/record/record.types.ts b/packages/api-aco/src/record/record.types.ts index 28cf17cea5e..56773a14716 100644 --- a/packages/api-aco/src/record/record.types.ts +++ b/packages/api-aco/src/record/record.types.ts @@ -1,6 +1,6 @@ import { AcoBaseFields, ListMeta } from "~/types"; import { Topic } from "@webiny/pubsub/types"; -import { CmsModel } from "@webiny/api-headless-cms/types"; +import { CmsEntryListSort, CmsModel } from "@webiny/api-headless-cms/types"; export type GenericSearchData = { [key: string]: any; @@ -39,7 +39,7 @@ export interface ListSearchRecordsWhere { +export const createListSort = (sort?: ListSort): CmsEntryListSort | undefined => { if (!sort) { return; } - return Object.keys(sort).map(key => `${key}_${sort[key]}`); + return Object.keys(sort).map(key => { + return `${key}_${sort[key]}` as CmsEntryListSortAsc | CmsEntryListSortDesc; + }); }; diff --git a/packages/api-apw/src/types.ts b/packages/api-apw/src/types.ts index 9c569468dd5..61da1cc1a46 100644 --- a/packages/api-apw/src/types.ts +++ b/packages/api-apw/src/types.ts @@ -2,7 +2,8 @@ import { CmsEntry as BaseCmsEntry, OnEntryBeforePublishTopicParams, OnEntryAfterPublishTopicParams, - OnEntryAfterUnpublishTopicParams + OnEntryAfterUnpublishTopicParams, + CmsEntryListSort } from "@webiny/api-headless-cms/types"; import { Page, @@ -62,7 +63,7 @@ export interface ListWhere { export interface ListParams { where?: ListWhere; - sort?: string[]; + sort?: CmsEntryListSort; limit?: number; after?: string | null; } diff --git a/packages/api-file-manager/src/createFileManager/files.crud.ts b/packages/api-file-manager/src/createFileManager/files.crud.ts index 825204501d5..fa80ffb8853 100644 --- a/packages/api-file-manager/src/createFileManager/files.crud.ts +++ b/packages/api-file-manager/src/createFileManager/files.crud.ts @@ -13,6 +13,7 @@ import { ROOT_FOLDER } from "~/contants"; import { NotAuthorizedError } from "@webiny/api-security"; import { getDate } from "@webiny/api-headless-cms/utils/date"; import { getIdentity as utilsGetIdentity } from "@webiny/api-headless-cms/utils/identity"; +import { CmsEntryListSort } from "@webiny/api-headless-cms/types"; export const createFilesCrud = (config: FileManagerConfig): FilesCRUD => { const { @@ -301,7 +302,7 @@ export const createFilesCrud = (config: FileManagerConfig): FilesCRUD => { where.createdBy = identity.id; } - const sort = + const sort: CmsEntryListSort = Array.isArray(initialSort) && initialSort.length > 0 ? initialSort : ["id_DESC"]; try { return await storageOperations.files.list({ diff --git a/packages/api-file-manager/src/types.ts b/packages/api-file-manager/src/types.ts index 444f68634ed..6c805348ea3 100644 --- a/packages/api-file-manager/src/types.ts +++ b/packages/api-file-manager/src/types.ts @@ -6,7 +6,7 @@ import { Context } from "@webiny/api/types"; import { FileLifecycleEvents } from "./types/file.lifecycle"; import { CreatedBy, File } from "./types/file"; import { Topic } from "@webiny/pubsub/types"; -import { CmsContext } from "@webiny/api-headless-cms/types"; +import { CmsContext, CmsEntryListSort } from "@webiny/api-headless-cms/types"; import { Context as TasksContext } from "@webiny/tasks/types"; export * from "./types/file.lifecycle"; @@ -68,7 +68,7 @@ export interface FilesListOpts { limit?: number; after?: string; where?: FileListWhereParams; - sort?: string[]; + sort?: CmsEntryListSort; } export interface FileListMeta { @@ -325,7 +325,7 @@ export interface FileManagerFilesStorageOperationsListParamsWhere { */ export interface FileManagerFilesStorageOperationsListParams { where: FileManagerFilesStorageOperationsListParamsWhere; - sort: string[]; + sort: CmsEntryListSort; limit: number; after: string | null; search?: string; diff --git a/packages/api-file-manager/src/types/file.ts b/packages/api-file-manager/src/types/file.ts index 6587b8fbf06..393105c2006 100644 --- a/packages/api-file-manager/src/types/file.ts +++ b/packages/api-file-manager/src/types/file.ts @@ -26,6 +26,7 @@ export interface File { createdBy: CreatedBy; modifiedBy: CreatedBy | null; savedBy: CreatedBy; + extensions?: Record; /** * Added with new storage operations refactoring. diff --git a/packages/api-headless-cms-bulk-actions/tsconfig.build.json b/packages/api-headless-cms-bulk-actions/tsconfig.build.json index a666b40ec94..7e3885a3cc9 100644 --- a/packages/api-headless-cms-bulk-actions/tsconfig.build.json +++ b/packages/api-headless-cms-bulk-actions/tsconfig.build.json @@ -3,18 +3,18 @@ "include": ["src"], "references": [ { "path": "../api-headless-cms/tsconfig.build.json" }, + { "path": "../handler/tsconfig.build.json" }, + { "path": "../handler-aws/tsconfig.build.json" }, + { "path": "../tasks/tsconfig.build.json" }, + { "path": "../utils/tsconfig.build.json" }, { "path": "../api/tsconfig.build.json" }, { "path": "../api-admin-users/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/tsconfig.build.json" }, - { "path": "../handler-aws/tsconfig.build.json" }, { "path": "../handler-graphql/tsconfig.build.json" }, { "path": "../plugins/tsconfig.build.json" }, - { "path": "../tasks/tsconfig.build.json" }, - { "path": "../utils/tsconfig.build.json" }, { "path": "../wcp/tsconfig.build.json" } ], "compilerOptions": { diff --git a/packages/api-headless-cms-bulk-actions/tsconfig.json b/packages/api-headless-cms-bulk-actions/tsconfig.json index ff0656762cd..0c13532bb1a 100644 --- a/packages/api-headless-cms-bulk-actions/tsconfig.json +++ b/packages/api-headless-cms-bulk-actions/tsconfig.json @@ -2,19 +2,19 @@ "extends": "../../tsconfig.json", "include": ["src", "__tests__"], "references": [ + { "path": "../api-headless-cms" }, + { "path": "../handler" }, + { "path": "../handler-aws" }, + { "path": "../tasks" }, + { "path": "../utils" }, { "path": "../api" }, { "path": "../api-admin-users" }, - { "path": "../api-headless-cms" }, { "path": "../api-i18n" }, { "path": "../api-security" }, { "path": "../api-tenancy" }, { "path": "../api-wcp" }, - { "path": "../handler" }, - { "path": "../handler-aws" }, { "path": "../handler-graphql" }, { "path": "../plugins" }, - { "path": "../tasks" }, - { "path": "../utils" }, { "path": "../wcp" } ], "compilerOptions": { @@ -24,12 +24,20 @@ "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], + "@webiny/api-headless-cms/*": ["../api-headless-cms/src/*"], + "@webiny/api-headless-cms": ["../api-headless-cms/src"], + "@webiny/handler/*": ["../handler/src/*"], + "@webiny/handler": ["../handler/src"], + "@webiny/handler-aws/*": ["../handler-aws/src/*"], + "@webiny/handler-aws": ["../handler-aws/src"], + "@webiny/tasks/*": ["../tasks/src/*"], + "@webiny/tasks": ["../tasks/src"], + "@webiny/utils/*": ["../utils/src/*"], + "@webiny/utils": ["../utils/src"], "@webiny/api/*": ["../api/src/*"], "@webiny/api": ["../api/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/*"], @@ -38,18 +46,10 @@ "@webiny/api-tenancy": ["../api-tenancy/src"], "@webiny/api-wcp/*": ["../api-wcp/src/*"], "@webiny/api-wcp": ["../api-wcp/src"], - "@webiny/handler/*": ["../handler/src/*"], - "@webiny/handler": ["../handler/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/tasks/*": ["../tasks/src/*"], - "@webiny/tasks": ["../tasks/src"], - "@webiny/utils": ["../utils/src"], - "@webiny/utils/*": ["../utils/src/*"], "@webiny/wcp/*": ["../wcp/src/*"], "@webiny/wcp": ["../wcp/src"] }, 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 0543df02b76..f2686e2cc23 100644 --- a/packages/api-headless-cms-ddb/src/operations/entry/index.ts +++ b/packages/api-headless-cms-ddb/src/operations/entry/index.ts @@ -84,7 +84,7 @@ const convertFromStorageEntry = (params: ConvertStorageEntryParams): CmsStorageE }; }; -const MAX_LIST_LIMIT = 10000; +const MAX_LIST_LIMIT = 1000000; export interface CreateEntriesStorageOperationsParams { entity: Entity; diff --git a/packages/api-headless-cms-es-tasks/package.json b/packages/api-headless-cms-es-tasks/package.json index 1a6cf58e20f..e6e3853d4b4 100644 --- a/packages/api-headless-cms-es-tasks/package.json +++ b/packages/api-headless-cms-es-tasks/package.json @@ -22,9 +22,9 @@ "@webiny/utils": "0.0.0" }, "devDependencies": { - "@babel/cli": "^7.22.6", - "@babel/core": "^7.22.8", - "@babel/preset-env": "^7.22.7", + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", "@faker-js/faker": "^8.4.1", "@webiny/api": "0.0.0", "@webiny/api-i18n": "0.0.0", diff --git a/packages/api-headless-cms-import-export/.babelrc.js b/packages/api-headless-cms-import-export/.babelrc.js new file mode 100644 index 00000000000..9da7674cb52 --- /dev/null +++ b/packages/api-headless-cms-import-export/.babelrc.js @@ -0,0 +1 @@ +module.exports = require("@webiny/project-utils").createBabelConfigForNode({ path: __dirname }); diff --git a/packages/api-headless-cms-import-export/LICENSE b/packages/api-headless-cms-import-export/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/api-headless-cms-import-export/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-import-export/README.md b/packages/api-headless-cms-import-export/README.md new file mode 100644 index 00000000000..8e94fcfed57 --- /dev/null +++ b/packages/api-headless-cms-import-export/README.md @@ -0,0 +1,15 @@ +# @webiny/api-headless-cms-import-export +[![](https://img.shields.io/npm/dw/@webiny/api-headless-cms-import-export.svg)](https://www.npmjs.com/package/@webiny/api-headless-cms-import-export) +[![](https://img.shields.io/npm/v/@webiny/api-headless-cms-import-export.svg)](https://www.npmjs.com/package/@webiny/api-headless-cms-import-export) +[![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-import-export +``` + +Or if you prefer yarn: +``` +yarn add @webiny/api-headless-cms-import-export +``` diff --git a/packages/api-headless-cms-import-export/__tests__/crud/useCases/validateImportFromUrl.test.ts b/packages/api-headless-cms-import-export/__tests__/crud/useCases/validateImportFromUrl.test.ts new file mode 100644 index 00000000000..a1fcda874c0 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/crud/useCases/validateImportFromUrl.test.ts @@ -0,0 +1,323 @@ +import { ValidateImportFromUrlUseCase } from "~/crud/useCases/validateImportFromUrl"; +import { categoryModel } from "~tests/helpers/models"; +import { Context } from "~/types"; +import { useHandler } from "~tests/helpers/useHandler"; +import { NotFoundError } from "@webiny/handler-graphql"; +import { CmsModel } from "@webiny/api-headless-cms/types"; + +describe("validateImportFromUrl", () => { + const { createContext } = useHandler(); + let context: Context; + const getModel = (modelId: string) => { + return context.cms.getModel(modelId); + }; + const getModelToAstConverter = () => { + return context.cms.getModelToAstConverter(); + }; + beforeEach(async () => { + context = await createContext(); + }); + + it("should fail on invalid data", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel + }); + + const params = { + data: "data" + }; + + try { + await useCase.execute(params); + } catch (ex) { + expect(ex.message).toBe("Invalid input data provided."); + } + }); + + it("should fail on no files found", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel + }); + + try { + await useCase.execute({ + data: JSON.stringify({ + model: categoryModel, + files: [] + }) + }); + } catch (ex) { + expect(ex.message).toBe("No files found in the provided data."); + } + }); + + it("should fail on invalid file", async () => { + expect.assertions(2); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel + }); + try { + await useCase.execute({ + data: JSON.stringify({ + model: categoryModel, + files: [ + { + get: "", + head: "", + checksum: "", + key: "", + type: "assets" + } + ] + }) + }); + } catch (ex) { + expect(ex.message).toBe("Validation failed."); + expect(ex.data).toEqual({ + invalidFields: { + "files.0.get": { + code: "invalid_string", + data: { + fatal: undefined, + path: ["files", 0, "get"] + }, + message: "Invalid url" + }, + "files.0.head": { + code: "invalid_string", + data: { + fatal: undefined, + path: ["files", 0, "head"] + }, + message: "Invalid url" + } + } + }); + } + }); + + it("should fail if no entries file", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel + }); + + try { + await useCase.execute({ + data: JSON.stringify({ + model: categoryModel, + files: [ + { + get: "https://some-url.com/file.zip", + head: "https://some-url.com/file.zip", + type: "assets", + checksum: "checksum", + key: "key" + } + ] + }) + }); + } catch (ex) { + expect(ex.message).toBe("No entries file found in the provided data."); + } + }); + + it("should fail if model not found", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel: () => { + throw new NotFoundError("Model not found."); + } + }); + + try { + await useCase.execute({ + data: JSON.stringify({ + model: categoryModel, + files: [ + { + get: "https://some-url.com/entries.zip", + head: "https://some-url.com/entries.zip", + type: "entries", + checksum: "checksum", + key: "key" + } + ] + }) + }); + } catch (ex) { + expect(ex.message).toEqual(`Model provided in the JSON data, "category", not found.`); + } + }); + + it("should fail if model getter fails for some reason", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel: () => { + throw new Error("Unspecified."); + } + }); + + try { + await useCase.execute({ + data: JSON.stringify({ + model: categoryModel, + files: [ + { + get: "https://some-url.com/entries.zip", + head: "https://some-url.com/entries.zip", + type: "entries", + checksum: "checksum", + key: "key" + } + ] + }) + }); + } catch (ex) { + expect(ex.message).toEqual(`Unspecified.`); + } + }); + + it("should fail to match models - database model missing fields", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel: async () => { + return { + ...categoryModel, + fields: categoryModel.fields.slice(1) + } as unknown as CmsModel; + } + }); + + try { + await useCase.execute({ + data: JSON.stringify({ + model: categoryModel, + files: [ + { + get: "https://some-url.com/entries.we.zip", + head: "https://some-url.com/entries.we.zip", + type: "entries", + checksum: "checksum", + key: "key" + } + ] + }) + }); + } catch (ex) { + expect(ex.message).toEqual(`Field "title" not found in the model from the database.`); + } + }); + + it("should fail to match models - exported model missing fields", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel: async () => { + return { + ...categoryModel + } as unknown as CmsModel; + } + }); + + try { + await useCase.execute({ + data: JSON.stringify({ + model: { + ...categoryModel, + fields: categoryModel.fields.slice(0, 1) + }, + files: [ + { + get: "https://some-url.com/entries.we.zip", + head: "https://some-url.com/entries.we.zip", + type: "entries", + checksum: "checksum", + key: "key" + } + ] + }) + }); + } catch (ex) { + expect(ex.message).toEqual( + `Field "slug" not found in the model provided via the JSON data.` + ); + } + }); + + it("should validate files properly", async () => { + expect.assertions(1); + const useCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter, + getModel + }); + + const result = await useCase.execute({ + data: JSON.stringify({ + model: categoryModel, + files: [ + { + get: "https://some-url.com/entries.zip", + head: "https://some-url.com/entries.zip", + type: "entries", + checksum: "checksum", + key: "key" + }, + { + get: "https://some-url.com/assets.zip", + head: "https://some-url.com/assets.zip", + type: "assets", + checksum: "checksum", + key: "key" + } + ] + }) + }); + expect(result).toEqual({ + model: expect.objectContaining({ + modelId: categoryModel.modelId + }), + files: [ + { + get: "https://some-url.com/entries.zip", + head: "https://some-url.com/entries.zip", + type: "entries", + checksum: "checksum", + key: "key", + error: { + data: { + pathname: "/entries.zip", + type: "zip" + }, + code: "FILE_TYPE_NOT_SUPPORTED", + message: "File type not supported." + } + }, + { + get: "https://some-url.com/assets.zip", + head: "https://some-url.com/assets.zip", + type: "assets", + checksum: "checksum", + key: "key", + error: { + data: { + pathname: "/assets.zip", + type: "zip" + }, + code: "FILE_TYPE_NOT_SUPPORTED", + message: "File type not supported." + } + } + ] + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/crud/utils/parseImportUrlData.ts b/packages/api-headless-cms-import-export/__tests__/crud/utils/parseImportUrlData.ts new file mode 100644 index 00000000000..e08f5c023a8 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/crud/utils/parseImportUrlData.ts @@ -0,0 +1,266 @@ +import { parseImportUrlData } from "~/crud/utils/parseImportUrlData"; + +describe("parseImportUrlData", () => { + it("should properly parse string data", async () => { + const result = parseImportUrlData( + JSON.stringify({ + model: { + modelId: "authors", + name: "Authors", + group: "659dacad78fc4c0008fb82fe", + icon: "fab/redhat", + description: "author", + singularApiName: "Author", + pluralApiName: "Authors", + fields: [ + { + multipleValues: false, + listValidation: [], + settings: {}, + renderer: { name: "text-input", settings: {} }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Name", + type: "text", + tags: [], + placeholderText: null, + id: "gb6mroog", + validation: [ + { + name: "required", + message: "Title is a required field.", + settings: {} + } + ], + storageId: "text@gb6mroog", + fieldId: "title" + }, + { + multipleValues: false, + listValidation: [], + settings: {}, + renderer: { name: "long-text-text-area", settings: {} }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Description", + type: "long-text", + tags: [], + placeholderText: null, + id: "od1cn25t", + validation: [], + storageId: "long-text@od1cn25t", + fieldId: "description" + }, + { + multipleValues: false, + listValidation: [], + settings: { imagesOnly: true }, + renderer: { name: "file-input", settings: {} }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Image", + type: "file", + tags: [], + placeholderText: null, + id: "wr4z5wa9", + validation: [], + storageId: "file@wr4z5wa9", + fieldId: "image" + }, + { + multipleValues: true, + listValidation: [], + settings: {}, + renderer: { name: "file-inputs" }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Files", + type: "file", + tags: [], + placeholderText: null, + id: "qjftjbsf", + validation: [], + storageId: "file@qjftjbsf", + fieldId: "files" + } + ], + layout: [["gb6mroog"], ["od1cn25t", "wr4z5wa9"], ["qjftjbsf"]], + titleFieldId: "title", + descriptionFieldId: "description", + imageFieldId: "image", + tags: ["type:model"] + }, + files: [ + { + get: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/entries.we.zip?&x-id=GetObject", + head: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/entries.we.zip?", + key: "authors/66d853af79ff7500086231748m0nub0sj/entries.we.zip", + checksum: "351908052acc58ced0761cbbdffc5b64", + type: "entries" + }, + { + get: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/assets..wa.zip?&x-id=GetObject", + head: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/assets..wa.zip?", + key: "authors/66d853af79ff7500086231748m0nub0sj/assets..wa.zip", + checksum: "b7ef1426390086e7050d98bf9d4a6937-48", + type: "assets" + } + ] + }) + ); + + expect(result).toEqual({ + model: expect.any(Object), + files: expect.any(Array) + }); + }); + + it("should properly parse object data", async () => { + const result = parseImportUrlData({ + model: { + modelId: "authors", + name: "Authors", + group: "659dacad78fc4c0008fb82fe", + icon: "fab/redhat", + description: "author", + singularApiName: "Author", + pluralApiName: "Authors", + fields: [ + { + multipleValues: false, + listValidation: [], + settings: {}, + renderer: { name: "text-input", settings: {} }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Name", + type: "text", + tags: [], + placeholderText: null, + id: "gb6mroog", + validation: [ + { + name: "required", + message: "Title is a required field.", + settings: {} + } + ], + storageId: "text@gb6mroog", + fieldId: "title" + }, + { + multipleValues: false, + listValidation: [], + settings: {}, + renderer: { name: "long-text-text-area", settings: {} }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Description", + type: "long-text", + tags: [], + placeholderText: null, + id: "od1cn25t", + validation: [], + storageId: "long-text@od1cn25t", + fieldId: "description" + }, + { + multipleValues: false, + listValidation: [], + settings: { imagesOnly: true }, + renderer: { name: "file-input", settings: {} }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Image", + type: "file", + tags: [], + placeholderText: null, + id: "wr4z5wa9", + validation: [], + storageId: "file@wr4z5wa9", + fieldId: "image" + }, + { + multipleValues: true, + listValidation: [], + settings: {}, + renderer: { name: "file-inputs" }, + helpText: null, + predefinedValues: { enabled: false, values: [] }, + label: "Files", + type: "file", + tags: [], + placeholderText: null, + id: "qjftjbsf", + validation: [], + storageId: "file@qjftjbsf", + fieldId: "files" + } + ], + layout: [["gb6mroog"], ["od1cn25t", "wr4z5wa9"], ["qjftjbsf"]], + titleFieldId: "title", + descriptionFieldId: "description", + imageFieldId: "image", + tags: ["type:model"] + }, + files: [ + { + get: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/entries.we.zip?&x-id=GetObject", + head: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/entries.we.zip?", + key: "authors/66d853af79ff7500086231748m0nub0sj/entries.we.zip", + checksum: "351908052acc58ced0761cbbdffc5b64", + type: "entries" + }, + { + get: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/assets..wa.zip?&x-id=GetObject", + head: "https://wby-fm-bucket-99ce7db.s3.eu-central-1.amazonaws.com/cms-export/authors/66d853af79ff7500086231748m0nub0sj/assets..wa.zip?", + key: "authors/66d853af79ff7500086231748m0nub0sj/assets..wa.zip", + checksum: "b7ef1426390086e7050d98bf9d4a6937-48", + type: "assets" + } + ] + }); + expect(result).toEqual({ + model: expect.any(Object), + files: expect.any(Array) + }); + }); + + it("should fail to parse invalid string data", async () => { + expect.assertions(1); + try { + parseImportUrlData("some wrong data"); + } catch (ex) { + expect(ex.message).toEqual("Invalid input data provided."); + } + }); + + it("should fail to parse invalid object data", async () => { + expect.assertions(2); + try { + parseImportUrlData({ model: {}, files: [] }); + } catch (ex) { + expect(ex.message).toEqual("Validation failed."); + expect(ex.data).toEqual({ + invalidFields: { + "model.fields": { + code: "invalid_type", + data: { + fatal: undefined, + path: ["model", "fields"] + }, + message: "Required" + }, + "model.modelId": { + code: "invalid_type", + data: { + fatal: undefined, + path: ["model", "modelId"] + }, + message: "Required" + } + } + }); + } + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/graphql/exportContentEntries.test.ts b/packages/api-headless-cms-import-export/__tests__/graphql/exportContentEntries.test.ts new file mode 100644 index 00000000000..449b8f91036 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/graphql/exportContentEntries.test.ts @@ -0,0 +1,85 @@ +import { useHandler } from "~tests/helpers/useHandler"; +import { AUTHOR_MODEL_ID } from "~tests/mocks/model"; + +describe("get export content entries", () => { + it("should return an error because the export task is not found", async () => { + const { getExportContentEntries } = useHandler(); + + const [response] = await getExportContentEntries({ + id: "non-existing-id" + }); + + expect(response).toEqual({ + data: { + getExportContentEntries: { + data: null, + error: { + message: `Export content entries task with id "non-existing-id" not found.`, + code: "NOT_FOUND", + data: null + } + } + } + }); + }); + + it("should properly trigger and get the export task", async () => { + const { createContext, getExportContentEntries } = useHandler(); + + const context = await createContext(); + + const task = await context.cmsImportExport.exportContentEntries({ + modelId: AUTHOR_MODEL_ID, + exportAssets: false + }); + + const [response] = await getExportContentEntries({ + id: task.id + }); + expect(response).toMatchObject({ + data: { + getExportContentEntries: { + data: { + id: task.id, + modelId: AUTHOR_MODEL_ID, + status: "pending", + files: null + }, + error: null + } + } + }); + }); + + it("should abort the export task", async () => { + const { createContext, abortExportContentEntries } = useHandler(); + + const context = await createContext(); + + const task = await context.cmsImportExport.exportContentEntries({ + modelId: AUTHOR_MODEL_ID, + exportAssets: false + }); + + const [response] = await abortExportContentEntries({ + id: task.id + }); + + expect(response).toEqual({ + data: { + abortExportContentEntries: { + data: { + id: task.id, + createdOn: task.createdOn, + createdBy: task.createdBy, + finishedOn: task.finishedOn, + modelId: task.modelId, + files: null, + status: "aborted" + }, + error: null + } + } + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/graphql/importFromUrl.test.ts b/packages/api-headless-cms-import-export/__tests__/graphql/importFromUrl.test.ts new file mode 100644 index 00000000000..a8a464be128 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/graphql/importFromUrl.test.ts @@ -0,0 +1,205 @@ +import { useHandler } from "~tests/helpers/useHandler"; +import { createValidateImportFromUrlTask } from "~/tasks"; +import { TaskDataStatus } from "@webiny/tasks"; + +describe("import from url - graphql", () => { + it("should fail to import from URL because of invalid ID", async () => { + const { importFromUrl } = useHandler(); + + const [result] = await importFromUrl({ + id: "unknownId" + }); + expect(result).toEqual({ + data: { + importFromUrl: { + data: null, + error: { + code: "NOT_FOUND", + data: null, + message: `Import from URL task with id "unknownId" not found.` + } + } + } + }); + }); + + it("should fail to import from URL because of integrity check not done yet", async () => { + const { importFromUrl, createContext } = useHandler(); + + const context = await createContext(); + + const definition = createValidateImportFromUrlTask(); + + const task = await context.tasks.createTask({ + name: "Test Task", + definitionId: definition.id, + input: { + files: [], + model: { + modelId: "aModelId" + } + } + }); + + const [result] = await importFromUrl({ + id: task.id + }); + expect(result).toEqual({ + data: { + importFromUrl: { + data: null, + error: { + code: "INTEGRITY_CHECK_FAILED", + data: { + status: "pending" + }, + message: "Integrity check failed." + } + } + } + }); + }); + + it("should fail to import from URL because of no files in the integrity check output", async () => { + const { importFromUrl, createContext } = useHandler(); + + const context = await createContext(); + + const definition = createValidateImportFromUrlTask(); + + const task = await context.tasks.createTask({ + name: "Test Task", + definitionId: definition.id, + input: { + files: [], + model: { + modelId: "aModelId" + } + } + }); + + await context.tasks.updateTask(task.id, { + taskStatus: TaskDataStatus.SUCCESS + }); + + const [result] = await importFromUrl({ + id: task.id + }); + expect(result).toEqual({ + data: { + importFromUrl: { + data: null, + error: { + code: "NO_FILES_FOUND", + data: null, + message: "No files found in the provided data." + } + } + } + }); + }); + + it("should fail to import from URL because import was already started", async () => { + const { importFromUrl, createContext } = useHandler(); + + const context = await createContext(); + + const definition = createValidateImportFromUrlTask(); + + const task = await context.tasks.createTask({ + name: "Test Task", + definitionId: definition.id, + input: { + files: [], + model: { + modelId: "aModelId" + } + } + }); + + await context.tasks.updateTask(task.id, { + taskStatus: TaskDataStatus.SUCCESS, + output: { + importTaskId: "notImportant", + files: [ + { + url: "somethingNotImportant" + } + ] + } + }); + + const [result] = await importFromUrl({ + id: task.id + }); + expect(result).toEqual({ + data: { + importFromUrl: { + data: null, + error: { + code: "IMPORT_TASK_EXISTS", + data: { + id: "notImportant" + }, + message: "Import was already started. You cannot start it again." + } + } + } + }); + }); + + it("should properly trigger the import from URL task", async () => { + const { importFromUrl, createContext } = useHandler(); + + const context = await createContext(); + + const definition = createValidateImportFromUrlTask(); + + const task = await context.tasks.createTask({ + name: "Test Task", + definitionId: definition.id, + input: { + files: [], + model: { + modelId: "aModelId" + } + } + }); + + await context.tasks.updateTask(task.id, { + taskStatus: TaskDataStatus.SUCCESS, + output: { + files: [ + { + url: "somethingNotImportant" + } + ] + } + }); + + const [result] = await importFromUrl({ + id: task.id + }); + + expect(result).toEqual({ + data: { + importFromUrl: { + data: { + id: expect.any(String), + status: "pending", + files: null + }, + error: null + } + } + }); + + const parentTask = await context.tasks.getTask(task.id); + + expect(parentTask).toMatchObject({ + output: { + importTaskId: result.data.importFromUrl.data.id + } + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/graphql/validateImportFromUrl.test.ts b/packages/api-headless-cms-import-export/__tests__/graphql/validateImportFromUrl.test.ts new file mode 100644 index 00000000000..98b6eada9f9 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/graphql/validateImportFromUrl.test.ts @@ -0,0 +1,330 @@ +import { useHandler } from "~tests/helpers/useHandler"; +import { TaskDataStatus } from "@webiny/tasks"; +import { categoryModel } from "~tests/helpers/models"; + +describe("validate import from url - graphql", () => { + it("should run validation and fail - parse string as json", async () => { + const { validateImportFromUrl } = useHandler(); + + const [result1] = await validateImportFromUrl({ + data: "" + }); + + expect(result1).toEqual({ + data: { + validateImportFromUrl: { + data: null, + error: { + message: "Invalid input data provided.", + code: "INVALID_INPUT_DATA", + data: null + } + } + } + }); + + const [result2] = await validateImportFromUrl({ + data: "{bla}" + }); + + expect(result2).toEqual({ + data: { + validateImportFromUrl: { + data: null, + error: { + message: "Invalid input data provided.", + code: "INVALID_INPUT_DATA", + data: null + } + } + } + }); + + const [result3] = await validateImportFromUrl({ + data: "{[]}" + }); + + expect(result3).toEqual({ + data: { + validateImportFromUrl: { + data: null, + error: { + message: "Invalid input data provided.", + code: "INVALID_INPUT_DATA", + data: null + } + } + } + }); + + const [result4] = await validateImportFromUrl({ + data: "invalid data" + }); + + expect(result4).toEqual({ + data: { + validateImportFromUrl: { + data: null, + error: { + message: "Invalid input data provided.", + code: "INVALID_INPUT_DATA", + data: null + } + } + } + }); + }); + + it("should run validation and fail - no files found", async () => { + const { validateImportFromUrl } = useHandler(); + + const [result] = await validateImportFromUrl({ + data: JSON.stringify({ + model: { + ...categoryModel + }, + files: [] + }) + }); + + expect(result).toEqual({ + data: { + validateImportFromUrl: { + data: null, + error: { + message: "No files found in the provided data.", + code: "NO_FILES_FOUND", + data: null + } + } + } + }); + }); + + it("should run validation and fail - invalid files", async () => { + const { validateImportFromUrl } = useHandler(); + + const [result] = await validateImportFromUrl({ + data: JSON.stringify({ + model: { + ...categoryModel + }, + files: [ + { + get: "invalid-url", + head: "invalid-url", + type: "invalid-type", + checksum: "checksum", + key: "key" + } + ] + }) + }); + + expect(result).toEqual({ + data: { + validateImportFromUrl: { + data: null, + error: { + code: "VALIDATION_FAILED_INVALID_FIELDS", + data: { + invalidFields: { + "files.0.get": { + code: "invalid_string", + data: { + path: ["files", 0, "get"] + }, + message: "Invalid url" + }, + "files.0.head": { + code: "invalid_string", + data: { + path: ["files", 0, "head"] + }, + message: "Invalid url" + }, + "files.0.type": { + code: "invalid_enum_value", + data: { + path: ["files", 0, "type"] + }, + message: + "Invalid enum value. Expected 'entries' | 'assets', received 'invalid-type'" + } + } + }, + message: "Validation failed." + } + } + } + }); + }); + + it("should run validation and fail - invalid file type", async () => { + const { validateImportFromUrl } = useHandler(); + + const [result] = await validateImportFromUrl({ + data: JSON.stringify({ + model: { + ...categoryModel + }, + files: [ + { + get: "https://get-url.com", + head: "https://head-url.com", + type: "entries", + checksum: "checksum", + key: "key" + } + ] + }) + }); + + expect(result).toEqual({ + data: { + validateImportFromUrl: { + data: { + id: expect.any(String), + files: [ + { + get: "https://get-url.com", + head: "https://head-url.com", + type: "entries", + size: null, + error: { + message: "File type not supported.", + data: { + pathname: "/" + } + } + } + ], + status: TaskDataStatus.PENDING + }, + error: null + } + } + }); + }); + + it("should run validation and fail - missing entries field", async () => { + const { validateImportFromUrl } = useHandler(); + + const [result] = await validateImportFromUrl({ + data: JSON.stringify({ + model: { + ...categoryModel + }, + files: [ + { + get: "https://get-url.com", + head: "https://head-url.com", + type: "assets", + checksum: "checksum", + key: "key" + } + ] + }) + }); + + expect(result).toEqual({ + data: { + validateImportFromUrl: { + data: null, + error: { + message: "No entries file found in the provided data.", + code: "NO_ENTRIES_FILE", + data: null + } + } + } + }); + }); + + it("should run the validation and pass", async () => { + const { validateImportFromUrl, getValidateImportFromUrl } = useHandler(); + + const { get, head } = { + get: "https://get-url.com/entries.we.zip?someExtraUrlParams=true", + head: "https://get-url.com/entries.we.zip?someExtraUrlParams=true" + }; + + const [result] = await validateImportFromUrl({ + data: JSON.stringify({ + model: { + ...categoryModel + }, + files: [ + { + get, + head, + type: "entries", + checksum: "checksum", + key: "key" + } + ] + }) + }); + + expect(result).toEqual({ + data: { + validateImportFromUrl: { + data: { + id: expect.any(String), + files: [ + { + get, + head, + type: "entries", + size: null, + error: null + } + ], + status: TaskDataStatus.PENDING + }, + error: null + } + } + }); + + const [getNoResult] = await getValidateImportFromUrl({ + id: "unknownid" + }); + expect(getNoResult).toEqual({ + data: { + getValidateImportFromUrl: { + data: null, + error: { + code: "NOT_FOUND", + data: null, + message: 'Validate import from URL task with id "unknownid" not found.' + } + } + } + }); + + const [getResult] = await getValidateImportFromUrl({ + id: result.data.validateImportFromUrl.data.id + }); + expect(getResult).toEqual({ + data: { + getValidateImportFromUrl: { + data: { + id: expect.any(String), + files: [ + { + get, + head, + size: null, + type: "entries", + error: null + } + ], + status: TaskDataStatus.PENDING + }, + error: null + } + } + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/graphql/abortExportContentEntries.ts b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/abortExportContentEntries.ts new file mode 100644 index 00000000000..26848ab3691 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/abortExportContentEntries.ts @@ -0,0 +1,28 @@ +import type { IInvokeCb } from "../types"; +import { createErrorFields, createExportFields } from "~tests/helpers/graphql/fields"; + +export interface IAbortExportContentEntriesVariables { + id: string; +} + +const query = /* GraphQL */ ` + mutation AbortExportContentEntries($id: ID!) { + abortExportContentEntries(id: $id) { + data { + ${createExportFields()} + } + ${createErrorFields()} + } + } +`; + +export const createAbortExportContentEntries = (invoke: IInvokeCb) => { + return async (variables: IAbortExportContentEntriesVariables) => { + return invoke({ + body: { + query, + variables + } + }); + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/graphql/exportContentEntries.ts b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/exportContentEntries.ts new file mode 100644 index 00000000000..17b55f3202e --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/exportContentEntries.ts @@ -0,0 +1,37 @@ +import type { IInvokeCb } from "../types"; +import { createErrorFields, createExportFields } from "./fields"; +import { CmsModel } from "@webiny/api-headless-cms/types"; +import { getModel } from "~tests/mocks/model"; + +export interface IExportContentEntriesVariables { + modelId: string; + limit?: number; +} + +const createQuery = (model: Pick): string => { + return /* GraphQL */ ` + mutation Export${model.pluralApiName}ContentEntries($limit: Int) { + expor${model.pluralApiName}tContentEntries(limit: $limit) { + data { + ${createExportFields()} + } + ${createErrorFields()} + } + } + `; +}; + +export const createExportContentEntries = (invoke: IInvokeCb) => { + return async (variables: IExportContentEntriesVariables) => { + const modelId = variables.modelId; + // @ts-expect-error + delete variables.modelId; + + return invoke({ + body: { + query: createQuery(getModel(modelId)), + variables + } + }); + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/graphql/fields.ts b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/fields.ts new file mode 100644 index 00000000000..eb0d85a56a0 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/fields.ts @@ -0,0 +1,47 @@ +export const createErrorFields = () => { + return /* GraphQL */ ` + error { + code + message + data + } + `; +}; + +export const createExportFields = () => { + return /* GraphQL */ ` + id + createdOn + createdBy { + id + displayName + type + } + finishedOn + modelId + files { + get + head + key + type + } + status + `; +}; + +export const createValidateImportFromUrlFields = () => { + return /* GraphQL */ ` + id + status + files { + get + head + type + size + error { + message + data + } + } + `; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/graphql/getExportContentEntries.ts b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/getExportContentEntries.ts new file mode 100644 index 00000000000..81a44902782 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/getExportContentEntries.ts @@ -0,0 +1,28 @@ +import type { IInvokeCb } from "../types"; +import { createErrorFields, createExportFields } from "./fields"; + +const query = /* GraphQL */ ` + query GetExportContentEntries($id: ID!) { + getExportContentEntries(id: $id) { + data { + ${createExportFields()} + } + ${createErrorFields()} + } + } +`; + +export interface IGetExportContentEntriesVariables { + id: string; +} + +export const createGetExportContentEntries = (invoke: IInvokeCb) => { + return async (variables: IGetExportContentEntriesVariables) => { + return invoke({ + body: { + query, + variables: variables + } + }); + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/graphql/getValidateImportFromUrl.ts b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/getValidateImportFromUrl.ts new file mode 100644 index 00000000000..d65ac2ca0c9 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/getValidateImportFromUrl.ts @@ -0,0 +1,28 @@ +import { createErrorFields, createValidateImportFromUrlFields } from "./fields"; +import type { IInvokeCb } from "~tests/helpers/types"; + +const query = /* GraphQL */ ` + query GetValidateImportFromUrl($id: ID!) { + getValidateImportFromUrl(id: $id) { + data { + ${createValidateImportFromUrlFields()} + } + ${createErrorFields()} + } + } +`; + +export interface IGetValidateImportFromUrlVariables { + id: string; +} + +export const createGetValidateImportFromUrl = (invoke: IInvokeCb) => { + return async (variables: IGetValidateImportFromUrlVariables) => { + return invoke({ + body: { + query, + variables + } + }); + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/graphql/importFromUrl.ts b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/importFromUrl.ts new file mode 100644 index 00000000000..6a2cd4047ea --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/importFromUrl.ts @@ -0,0 +1,28 @@ +import { createErrorFields, createValidateImportFromUrlFields } from "./fields"; +import type { IInvokeCb } from "~tests/helpers/types"; + +const mutation = /* GraphQL */ ` + mutation ImportFromUrl($id: ID!) { + importFromUrl(id: $id) { + data { + ${createValidateImportFromUrlFields()} + } + ${createErrorFields()} + } + } +`; + +export interface IImportFromUrlVariables { + id: string; +} + +export const createImportFromUrl = (invoke: IInvokeCb) => { + return async (variables: IImportFromUrlVariables) => { + return invoke({ + body: { + query: mutation, + variables: variables + } + }); + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/graphql/validateImportFromUrl.ts b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/validateImportFromUrl.ts new file mode 100644 index 00000000000..1a265a55aa7 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/graphql/validateImportFromUrl.ts @@ -0,0 +1,28 @@ +import { createErrorFields, createValidateImportFromUrlFields } from "./fields"; +import type { IInvokeCb } from "~tests/helpers/types"; + +const mutation = /* GraphQL */ ` + mutation ValidateImportFromUrl($data: JSON!) { + validateImportFromUrl(data: $data) { + data { + ${createValidateImportFromUrlFields()} + } + ${createErrorFields()} + } + } +`; + +export interface IValidateImportFromUrlVariables { + data: string; +} + +export const createValidateImportFromUrl = (invoke: IInvokeCb) => { + return async (variables: IValidateImportFromUrlVariables) => { + return invoke({ + body: { + query: mutation, + variables: variables + } + }); + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/helpers.ts b/packages/api-headless-cms-import-export/__tests__/helpers/helpers.ts new file mode 100644 index 00000000000..e4ad8a1f012 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/helpers.ts @@ -0,0 +1,72 @@ +import type { SecurityIdentity } from "@webiny/api-security/types"; +import { ContextPlugin } from "@webiny/api"; +import type { Context } from "~/types"; + +export interface PermissionsArg { + name: string; + locales?: string[]; + rwd?: string; + pw?: string; + own?: boolean; +} + +export const identity = { + id: "id-12345678", + displayName: "John Doe", + type: "admin" +}; + +const getSecurityIdentity = () => { + return identity; +}; + +export const createPermissions = (permissions?: PermissionsArg[]): PermissionsArg[] => { + if (permissions) { + return permissions; + } + return [ + { + name: "task.entry", + rwd: "rwd" + }, + { + name: "content.i18n", + locales: ["en-US", "de-DE"] + }, + { + name: "*" + } + ]; +}; + +export const createIdentity = (identity?: SecurityIdentity) => { + if (!identity) { + return getSecurityIdentity(); + } + return identity; +}; + +export const createDummyLocales = () => { + return new ContextPlugin(async context => { + const { i18n, security } = context; + + await security.authenticate(""); + + await security.withoutAuthorization(async () => { + const [items] = await i18n.locales.listLocales({ + where: {} + }); + if (items.length > 0) { + return; + } + await i18n.locales.createLocale({ + code: "en-US", + default: true + }); + await i18n.locales.createLocale({ + code: "de-DE", + default: true + }); + }); + }); +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/models.ts b/packages/api-headless-cms-import-export/__tests__/helpers/models.ts new file mode 100644 index 00000000000..c1fae81f6f1 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/models.ts @@ -0,0 +1,119 @@ +import { + CmsGroup, + CmsModelInput, + createCmsGroupPlugin, + createCmsModelPlugin +} from "@webiny/api-headless-cms"; + +export const group: CmsGroup = { + id: "5e7c96c46adcbe0007268295", + name: "A sample content model group", + slug: "a-sample-content-model-group", + description: "This is a simple content model group example.", + icon: "fas/star" +}; + +export const categoryModel: CmsModelInput = { + titleFieldId: "title", + name: "Category", + description: "Product category", + modelId: "category", + singularApiName: "CategoryApiNameWhichIsABitDifferentThanModelId", + pluralApiName: "CategoriesApiModel", + group: { + id: group.id, + name: group.name + }, + layout: [["titleFieldIdAbcdef"], ["slugFieldIdAbc"], ["parentCategory"], ["tags"]], + fields: [ + { + id: "titleFieldIdAbcdef", + multipleValues: false, + helpText: "", + label: "Title", + type: "text", + storageId: "text@titleStorageId", + fieldId: "title", + validation: [ + { + name: "required", + message: "This field is required" + }, + { + name: "minLength", + message: "Enter at least 3 characters", + settings: { + min: 3.0 + } + } + ], + listValidation: [], + placeholderText: "placeholder text", + predefinedValues: { + enabled: false, + values: [] + }, + renderer: { + name: "renderer" + } + }, + { + id: "slugFieldIdAbc", + multipleValues: false, + helpText: "", + label: "Slug", + type: "text", + storageId: "text@slugStorageId", + fieldId: "slug", + validation: [ + { + name: "required", + message: "This field is required" + } + ], + listValidation: [], + placeholderText: "placeholder text", + predefinedValues: { + enabled: false, + values: [] + }, + renderer: { + name: "renderer" + } + }, + { + id: "parentCategory", + multipleValues: false, + helpText: "", + label: "Self - reference", + type: "ref", + fieldId: "parent", + settings: { + models: [ + { + modelId: "category", + name: "Category" + } + ] + } + }, + { + id: "tags", + multipleValues: true, + helpText: "", + label: "Tags", + type: "text", + fieldId: "tags" + } + ] +}; +export const models: CmsModelInput[] = [categoryModel]; + +export const createCmsPlugins = () => { + return [ + createCmsGroupPlugin(group), + ...models.map(model => { + return createCmsModelPlugin(model); + }) + ]; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/tenancySecurity.ts b/packages/api-headless-cms-import-export/__tests__/helpers/tenancySecurity.ts new file mode 100644 index 00000000000..ce58ad2f694 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/tenancySecurity.ts @@ -0,0 +1,69 @@ +import { Plugin } from "@webiny/plugins/Plugin"; +import { createTenancyContext, createTenancyGraphQL } from "@webiny/api-tenancy"; +import { createSecurityContext, createSecurityGraphQL } from "@webiny/api-security"; +import { + SecurityIdentity, + SecurityPermission, + SecurityStorageOperations +} from "@webiny/api-security/types"; +import { ContextPlugin } from "@webiny/api"; +import { BeforeHandlerPlugin } from "@webiny/handler"; +import { Context } from "~/types"; +import { getStorageOps } from "@webiny/project-utils/testing/environment"; +import { TenancyStorageOperations, Tenant } from "@webiny/api-tenancy/types"; + +interface Config { + setupGraphQL?: boolean; + permissions: SecurityPermission[]; + identity?: SecurityIdentity | null; +} + +export const defaultIdentity: SecurityIdentity = { + id: "id-12345678", + type: "admin", + displayName: "John Doe" +}; + +export const createTenancyAndSecurity = ({ + setupGraphQL, + permissions, + identity +}: Config): Plugin[] => { + const tenancyStorage = getStorageOps("tenancy"); + const securityStorage = getStorageOps("security"); + + return [ + createTenancyContext({ storageOperations: tenancyStorage.storageOperations }), + setupGraphQL ? createTenancyGraphQL() : null, + createSecurityContext({ storageOperations: securityStorage.storageOperations }), + setupGraphQL ? createSecurityGraphQL() : null, + new ContextPlugin(context => { + context.tenancy.setCurrentTenant({ + id: "root", + name: "Root", + webinyVersion: context.WEBINY_VERSION + } as unknown as Tenant); + + context.security.addAuthenticator(async () => { + return identity || defaultIdentity; + }); + + context.security.addAuthorizer(async () => { + const { headers = {} } = context.request || {}; + if (headers["authorization"]) { + return null; + } + + return permissions || [{ name: "*" }]; + }); + }), + new BeforeHandlerPlugin(context => { + const { headers = {} } = context.request || {}; + if (headers["authorization"]) { + return context.security.authenticate(headers["authorization"]); + } + + return context.security.authenticate(""); + }) + ].filter(Boolean) as Plugin[]; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/types.ts b/packages/api-headless-cms-import-export/__tests__/helpers/types.ts new file mode 100644 index 00000000000..49d96037aa3 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/types.ts @@ -0,0 +1,14 @@ +import { GenericRecord } from "@webiny/api/types"; + +export interface InvokeParams { + httpMethod?: "POST" | "GET" | "OPTIONS"; + body?: { + query: string; + variables?: GenericRecord; + }; + headers?: GenericRecord; +} + +export interface IInvokeCb { + (params: InvokeParams): Promise<[T, any]>; +} diff --git a/packages/api-headless-cms-import-export/__tests__/helpers/useHandler.ts b/packages/api-headless-cms-import-export/__tests__/helpers/useHandler.ts new file mode 100644 index 00000000000..9fff87025d3 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/helpers/useHandler.ts @@ -0,0 +1,125 @@ +import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; +import graphQLHandlerPlugins from "@webiny/handler-graphql"; +import { getStorageOps } from "@webiny/project-utils/testing/environment"; +import { HeadlessCmsStorageOperations } from "@webiny/api-headless-cms/types"; +import { createWcpContext } from "@webiny/api-wcp"; +import { createTenancyAndSecurity } from "./tenancySecurity"; +import { createDummyLocales, createIdentity, createPermissions } from "./helpers"; +import { mockLocalesPlugins } from "@webiny/api-i18n/graphql/testing"; +import i18nContext from "@webiny/api-i18n/graphql/context"; +import { + createApiGatewayHandler, + createRawEventHandler, + createRawHandler +} from "@webiny/handler-aws"; +import { APIGatewayEvent, LambdaContext } from "@webiny/handler-aws/types"; +import { PluginCollection } from "@webiny/plugins/types"; +import { createBackgroundTaskContext } from "@webiny/tasks"; +import { Context } from "~/types"; +import { createModelPlugin } from "~tests/mocks/model"; +import { createFileManagerContext } from "@webiny/api-file-manager"; +import { FileManagerStorageOperations } from "@webiny/api-file-manager/types"; +import { InvokeParams } from "./types"; +import { createHeadlessCmsImportExport } from "~/index"; +import { createGetExportContentEntries } from "./graphql/getExportContentEntries"; +import { createExportContentEntries } from "./graphql/exportContentEntries"; +import { createAbortExportContentEntries } from "./graphql/abortExportContentEntries"; +import { createMockTaskTriggerTransportPlugin } from "@webiny/project-utils/testing/tasks"; +import { createValidateImportFromUrl } from "./graphql/validateImportFromUrl"; +import { createGetValidateImportFromUrl } from "./graphql/getValidateImportFromUrl"; +import { createCmsPlugins } from "~tests/helpers/models"; +import { createImportFromUrl } from "~tests/helpers/graphql/importFromUrl"; + +export interface UseHandlerParams { + plugins?: PluginCollection; +} + +export const useHandler = (params?: UseHandlerParams) => { + const { plugins: inputPlugins = [] } = params || {}; + const cmsStorage = getStorageOps("cms"); + const i18nStorage = getStorageOps("i18n"); + + process.env.S3_BUCKET = "a-mock-s3-bucket"; + + const fileManagerStorage = getStorageOps("fileManager"); + + const plugins = [ + createModelPlugin(), + createWcpContext(), + ...cmsStorage.plugins, + ...fileManagerStorage.plugins, + ...createTenancyAndSecurity({ + setupGraphQL: false, + permissions: createPermissions(), + identity: createIdentity() + }), + i18nContext(), + i18nStorage.storageOperations, + createDummyLocales(), + mockLocalesPlugins(), + createHeadlessCmsContext({ + storageOperations: cmsStorage.storageOperations + }), + createHeadlessCmsGraphQL(), + createBackgroundTaskContext(), + createHeadlessCmsImportExport(), + createFileManagerContext({ + storageOperations: fileManagerStorage.storageOperations + }), + graphQLHandlerPlugins(), + createRawEventHandler(async ({ context }) => { + return context; + }), + createMockTaskTriggerTransportPlugin(), + ...createCmsPlugins(), + ...inputPlugins + ]; + + const rawHandler = createRawHandler({ + plugins + }); + + const graphQLHandler = createApiGatewayHandler({ + plugins + }); + + const invoke = async ({ + httpMethod = "POST", + body, + headers = {}, + ...rest + }: InvokeParams): Promise<[T, any]> => { + const response = await graphQLHandler( + { + /** + * If no path defined, use /graphql as we want to make request to main api + */ + path: `/cms/manage/en-US`, + 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]; + }; + + return { + invoke, + getExportContentEntries: createGetExportContentEntries(invoke), + exportContentEntries: createExportContentEntries(invoke), + abortExportContentEntries: createAbortExportContentEntries(invoke), + validateImportFromUrl: createValidateImportFromUrl(invoke), + getValidateImportFromUrl: createGetValidateImportFromUrl(invoke), + importFromUrl: createImportFromUrl(invoke), + createContext: async () => { + return await rawHandler({}, {} as LambdaContext); + } + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/assets.ts b/packages/api-headless-cms-import-export/__tests__/mocks/assets.ts new file mode 100644 index 00000000000..3e400d0550d --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/assets.ts @@ -0,0 +1,3071 @@ +export interface IMockAsset { + key: string; + url: string; +} + +export const assets: IMockAsset[] = [ + { + key: "668bed60544f8800080d2abd/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bed60544f8800080d2abd/Archivecopy.zip" + }, + { + key: "668bed3e544f8800080d2ab9/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bed3e544f8800080d2ab9/Archivecopy9.zip" + }, + { + key: "668bed19544f8800080d2ab5/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bed19544f8800080d2ab5/Archivecopy8.zip" + }, + { + key: "668becf5544f8800080d2ab1/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668becf5544f8800080d2ab1/Archivecopy7.zip" + }, + { + key: "668becce544f8800080d2aad/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668becce544f8800080d2aad/Archivecopy6.zip" + }, + { + key: "668beca9544f8800080d2aa9/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beca9544f8800080d2aa9/Archivecopy5.zip" + }, + { + key: "668bec82544f8800080d2aa5/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bec82544f8800080d2aa5/Archivecopy4.zip" + }, + { + key: "668bec59544f8800080d2aa1/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bec59544f8800080d2aa1/Archivecopy3.zip" + }, + { + key: "668bec2c544f8800080d2a9d/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bec2c544f8800080d2a9d/Archivecopy2.zip" + }, + { + key: "668bec04544f8800080d2a99/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bec04544f8800080d2a99/Archive.zip" + }, + { + key: "668bebff544f8800080d2a95/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebff544f8800080d2a95/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668bebfb544f8800080d2a91/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebfb544f8800080d2a91/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668bebf9544f8800080d2a8d/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebf9544f8800080d2a8d/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668bebf6544f8800080d2a89/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebf6544f8800080d2a89/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668bebed544f8800080d2a85/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebed544f8800080d2a85/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668bebea544f8800080d2a81/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebea544f8800080d2a81/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668bebe5544f8800080d2a7d/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebe5544f8800080d2a7d/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668bebd7544f8800080d2a79/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebd7544f8800080d2a79/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668bebd5544f8800080d2a75/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebd5544f8800080d2a75/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668bebd0544f8800080d2a71/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebd0544f8800080d2a71/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668bebce544f8800080d2a6d/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebce544f8800080d2a6d/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668bebcd544f8800080d2a69/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebcd544f8800080d2a69/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668bebca544f8800080d2a65/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebca544f8800080d2a65/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668bebc9544f8800080d2a61/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebc9544f8800080d2a61/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668bebc6544f8800080d2a5d/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebc6544f8800080d2a5d/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668bebc2544f8800080d2a59/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebc2544f8800080d2a59/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668bebbb544f8800080d2a55/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebbb544f8800080d2a55/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668bebb8544f8800080d2a51/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebb8544f8800080d2a51/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668bebb6544f8800080d2a4d/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bebb6544f8800080d2a4d/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668beba9544f8800080d2a49/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beba9544f8800080d2a49/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668beba7544f8800080d2a45/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beba7544f8800080d2a45/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668beba3544f8800080d2a41/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beba3544f8800080d2a41/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668beba1544f8800080d2a3d/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beba1544f8800080d2a3d/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668beba0544f8800080d2a39/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beba0544f8800080d2a39/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668beb9d544f8800080d2a35/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb9d544f8800080d2a35/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668beb9b544f8800080d2a31/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb9b544f8800080d2a31/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668beb9a544f8800080d2a2d/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb9a544f8800080d2a2d/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668beb98544f8800080d2a29/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb98544f8800080d2a29/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668beb94544f8800080d2a25/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb94544f8800080d2a25/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668beb8d544f8800080d2a21/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb8d544f8800080d2a21/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668beb8c544f8800080d2a1d/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb8c544f8800080d2a1d/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668beb89544f8800080d2a19/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb89544f8800080d2a19/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668beb87544f8800080d2a15/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb87544f8800080d2a15/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668beb81544f8800080d2a11/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb81544f8800080d2a11/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668beb7c544f8800080d2a0d/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb7c544f8800080d2a0d/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668beb78544f8800080d2a09/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb78544f8800080d2a09/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668beb76544f8800080d2a05/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb76544f8800080d2a05/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668beb74544f8800080d2a01/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb74544f8800080d2a01/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668beb6d544f8800080d29fd/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb6d544f8800080d29fd/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668beb6b544f8800080d29f9/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb6b544f8800080d29f9/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668beb67544f8800080d29f5/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb67544f8800080d29f5/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668beb5d544f8800080d29f1/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb5d544f8800080d29f1/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668beb5a544f8800080d29ed/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb5a544f8800080d29ed/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668beb56544f8800080d29e9/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb56544f8800080d29e9/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668beb54544f8800080d29e5/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb54544f8800080d29e5/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668beb53544f8800080d29e1/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb53544f8800080d29e1/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668beb51544f8800080d29dd/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb51544f8800080d29dd/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668beb50544f8800080d29d9/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb50544f8800080d29d9/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668beb4c544f8800080d29d5/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb4c544f8800080d29d5/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668beb49544f8800080d29d1/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb49544f8800080d29d1/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668beb43544f8800080d29cd/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb43544f8800080d29cd/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668beb41544f8800080d29c9/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb41544f8800080d29c9/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668beb3f544f8800080d29c5/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb3f544f8800080d29c5/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668beb34544f8800080d29c1/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb34544f8800080d29c1/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668beb32544f8800080d29bd/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb32544f8800080d29bd/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668beb2e544f8800080d29b9/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb2e544f8800080d29b9/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668beb2c544f8800080d29b5/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb2c544f8800080d29b5/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668beb2b544f8800080d29b1/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb2b544f8800080d29b1/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668beb28544f8800080d29ad/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb28544f8800080d29ad/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668beb26544f8800080d29a9/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb26544f8800080d29a9/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668beb25544f8800080d29a5/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb25544f8800080d29a5/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668beb23544f8800080d29a1/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb23544f8800080d29a1/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668beb1f544f8800080d299d/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb1f544f8800080d299d/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668beb17544f8800080d2999/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb17544f8800080d2999/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668beb15544f8800080d2995/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb15544f8800080d2995/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668beb13544f8800080d2991/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb13544f8800080d2991/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668beb10544f8800080d298d/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb10544f8800080d298d/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668beb0b544f8800080d2989/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb0b544f8800080d2989/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668beb05544f8800080d2985/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb05544f8800080d2985/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668beb01544f8800080d2981/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beb01544f8800080d2981/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668beaff544f8800080d297d/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beaff544f8800080d297d/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668beafd544f8800080d2979/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beafd544f8800080d2979/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668beaf4544f8800080d2975/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beaf4544f8800080d2975/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668beaf1544f8800080d2971/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beaf1544f8800080d2971/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668beaed544f8800080d296d/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beaed544f8800080d296d/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668beade544f8800080d2969/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beade544f8800080d2969/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668beada544f8800080d2965/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beada544f8800080d2965/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668bead3544f8800080d2961/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bead3544f8800080d2961/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668bead1544f8800080d295d/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bead1544f8800080d295d/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668beacf544f8800080d2959/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beacf544f8800080d2959/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668beacc544f8800080d2955/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beacc544f8800080d2955/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668beacb544f8800080d2951/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beacb544f8800080d2951/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668beac7544f8800080d294d/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beac7544f8800080d294d/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668beac2544f8800080d2949/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beac2544f8800080d2949/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668beab8544f8800080d2945/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beab8544f8800080d2945/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668beab4544f8800080d2941/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beab4544f8800080d2941/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668beab2544f8800080d293d/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beab2544f8800080d293d/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668beaa0544f8800080d2939/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668beaa0544f8800080d2939/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668bea9e544f8800080d2935/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea9e544f8800080d2935/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668bea9a544f8800080d2931/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea9a544f8800080d2931/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668bea98544f8800080d292d/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea98544f8800080d292d/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668bea97544f8800080d2929/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea97544f8800080d2929/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668bea94544f8800080d2925/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea94544f8800080d2925/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668bea92544f8800080d2921/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea92544f8800080d2921/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668bea91544f8800080d291d/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea91544f8800080d291d/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668bea8f544f8800080d2919/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea8f544f8800080d2919/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668bea8c544f8800080d2915/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea8c544f8800080d2915/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668bea84544f8800080d2911/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea84544f8800080d2911/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668bea82544f8800080d290d/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea82544f8800080d290d/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668bea80544f8800080d2909/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea80544f8800080d2909/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668bea7d544f8800080d2905/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea7d544f8800080d2905/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668bea78544f8800080d2901/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea78544f8800080d2901/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668bea73544f8800080d28fd/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea73544f8800080d28fd/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668bea6f544f8800080d28f9/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea6f544f8800080d28f9/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668bea6d544f8800080d28f5/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea6d544f8800080d28f5/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668bea6a544f8800080d28f1/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea6a544f8800080d28f1/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668bea61544f8800080d28ed/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea61544f8800080d28ed/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668bea5e544f8800080d28e9/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea5e544f8800080d28e9/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668bea5a544f8800080d28e5/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea5a544f8800080d28e5/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668bea4f544f8800080d28e1/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea4f544f8800080d28e1/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668bea4b544f8800080d28dd/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea4b544f8800080d28dd/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668bea46544f8800080d28d9/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea46544f8800080d28d9/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668bea44544f8800080d28d5/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea44544f8800080d28d5/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668bea42544f8800080d28d1/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea42544f8800080d28d1/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668bea3f544f8800080d28cd/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea3f544f8800080d28cd/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668bea3f544f8800080d28c9/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea3f544f8800080d28c9/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668bea3b544f8800080d28c5/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea3b544f8800080d28c5/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668bea37544f8800080d28c1/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea37544f8800080d28c1/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668bea2f544f8800080d28bd/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea2f544f8800080d28bd/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668bea2b544f8800080d28b9/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea2b544f8800080d28b9/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668bea29544f8800080d28b5/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea29544f8800080d28b5/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668bea1d544f8800080d28b1/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea1d544f8800080d28b1/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668bea1a544f8800080d28ad/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea1a544f8800080d28ad/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668bea15544f8800080d28a9/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea15544f8800080d28a9/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668bea12544f8800080d28a5/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea12544f8800080d28a5/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668bea0f544f8800080d28a1/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea0f544f8800080d28a1/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668bea0b544f8800080d289d/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea0b544f8800080d289d/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668bea08544f8800080d2899/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea08544f8800080d2899/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668bea06544f8800080d2895/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea06544f8800080d2895/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668bea03544f8800080d2891/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bea03544f8800080d2891/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668be9ff544f8800080d288d/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668be9ff544f8800080d288d/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668be9f8544f8800080d2889/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668be9f8544f8800080d2889/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668be9f5544f8800080d2885/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668be9f5544f8800080d2885/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668be9f3544f8800080d2881/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668be9f3544f8800080d2881/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668be9f1544f8800080d287d/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668be9f1544f8800080d287d/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668be9ec544f8800080d2879/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668be9ec544f8800080d2879/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668bd15ba481f70008cd499b/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bd15ba481f70008cd499b/zip-of-zips.zip" + }, + { + key: "668bd134a481f70008cd4997/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bd134a481f70008cd4997/zip-of-zips.zip" + }, + { + key: "668bd113a481f70008cd4993/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bd113a481f70008cd4993/zip-of-zips.zip" + }, + { + key: "668bd0eea481f70008cd498f/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bd0eea481f70008cd498f/zip-of-zips.zip" + }, + { + key: "668bd0caa481f70008cd498b/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bd0caa481f70008cd498b/zip-of-zips.zip" + }, + { + key: "668bd0a8a481f70008cd4987/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bd0a8a481f70008cd4987/zip-of-zips.zip" + }, + { + key: "668bd088a481f70008cd4983/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bd088a481f70008cd4983/zip-of-zips.zip" + }, + { + key: "668bcb3a55287900082429b6/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcb3a55287900082429b6/Archivecopy.zip" + }, + { + key: "668bcb2855287900082429b2/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcb2855287900082429b2/Archivecopy9.zip" + }, + { + key: "668bcb1555287900082429ae/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcb1555287900082429ae/Archivecopy8.zip" + }, + { + key: "668bcaff55287900082429aa/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcaff55287900082429aa/Archivecopy7.zip" + }, + { + key: "668bcaf255287900082429a6/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcaf255287900082429a6/Archivecopy6.zip" + }, + { + key: "668bcae555287900082429a2/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcae555287900082429a2/Archivecopy5.zip" + }, + { + key: "668bcad9552879000824299e/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcad9552879000824299e/Archivecopy4.zip" + }, + { + key: "668bcacd552879000824299a/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcacd552879000824299a/Archivecopy3.zip" + }, + { + key: "668bcabe5528790008242996/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bcabe5528790008242996/Archivecopy2.zip" + }, + { + key: "668bca995528790008242992/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca995528790008242992/zip-of-zips.zip" + }, + { + key: "668bca8b552879000824298e/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca8b552879000824298e/Archive.zip" + }, + { + key: "668bca6b552879000824298a/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca6b552879000824298a/zip-of-zips.zip" + }, + { + key: "668bca5c5528790008242986/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca5c5528790008242986/Archivecopy.zip" + }, + { + key: "668bca4f5528790008242982/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca4f5528790008242982/Archivecopy9.zip" + }, + { + key: "668bca41552879000824297e/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca41552879000824297e/Archivecopy8.zip" + }, + { + key: "668bca36552879000824297a/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca36552879000824297a/Archivecopy7.zip" + }, + { + key: "668bca2a5528790008242976/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca2a5528790008242976/Archivecopy6.zip" + }, + { + key: "668bca1e5528790008242972/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca1e5528790008242972/Archivecopy5.zip" + }, + { + key: "668bca11552879000824296e/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca11552879000824296e/Archivecopy4.zip" + }, + { + key: "668bca05552879000824296a/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bca05552879000824296a/Archivecopy3.zip" + }, + { + key: "668bc9f95528790008242966/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9f95528790008242966/Archivecopy2.zip" + }, + { + key: "668bc9ea5528790008242962/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9ea5528790008242962/Archive.zip" + }, + { + key: "668bc924858df900083c6f39/assets.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc924858df900083c6f39/assets.zip" + }, + { + key: "668bc900858df900083c6f35/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc900858df900083c6f35/zip-of-zips.zip" + }, + { + key: "668bc8f0858df900083c6f31/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc8f0858df900083c6f31/Archivecopy.zip" + }, + { + key: "668bc8e2858df900083c6f2d/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc8e2858df900083c6f2d/Archivecopy9.zip" + }, + { + key: "668bc8d5858df900083c6f29/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc8d5858df900083c6f29/Archivecopy8.zip" + }, + { + key: "668bc8c7858df900083c6f25/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc8c7858df900083c6f25/Archivecopy7.zip" + }, + { + key: "668bc8b8858df900083c6f21/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc8b8858df900083c6f21/Archivecopy6.zip" + }, + { + key: "668bc8aa858df900083c6f1d/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc8aa858df900083c6f1d/Archivecopy5.zip" + }, + { + key: "668bc89a858df900083c6f19/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc89a858df900083c6f19/Archivecopy4.zip" + }, + { + key: "668bc889858df900083c6f15/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc889858df900083c6f15/Archivecopy3.zip" + }, + { + key: "668bc87e858df900083c6f11/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc87e858df900083c6f11/Archivecopy2.zip" + }, + { + key: "668bc84a858df900083c6f0d/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc84a858df900083c6f0d/Archive.zip" + }, + { + key: "668bc7d9209dac000807dc7e/assets.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc7d9209dac000807dc7e/assets.zip" + }, + { + key: "668bc765209dac000807dc7a/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc765209dac000807dc7a/zip-of-zips.zip" + }, + { + key: "668bc756209dac000807dc76/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc756209dac000807dc76/Archivecopy.zip" + }, + { + key: "668bc748209dac000807dc72/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc748209dac000807dc72/Archivecopy9.zip" + }, + { + key: "668bc73b209dac000807dc6e/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc73b209dac000807dc6e/Archivecopy8.zip" + }, + { + key: "668bc72c209dac000807dc6a/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc72c209dac000807dc6a/Archivecopy7.zip" + }, + { + key: "668bc720209dac000807dc66/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc720209dac000807dc66/Archivecopy6.zip" + }, + { + key: "668bc712209dac000807dc62/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc712209dac000807dc62/Archivecopy5.zip" + }, + { + key: "668bc705209dac000807dc5e/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc705209dac000807dc5e/Archivecopy4.zip" + }, + { + key: "668bc6fb209dac000807dc5a/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc6fb209dac000807dc5a/Archivecopy3.zip" + }, + { + key: "668bc6ee209dac000807dc56/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc6ee209dac000807dc56/Archivecopy2.zip" + }, + { + key: "668bc6e3209dac000807dc52/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc6e3209dac000807dc52/Archive.zip" + }, + { + key: "668bc29eb51cc50008706641/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc29eb51cc50008706641/Archivecopy.zip" + }, + { + key: "668bc294b51cc5000870663d/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc294b51cc5000870663d/Archivecopy9.zip" + }, + { + key: "668bc288b51cc50008706639/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc288b51cc50008706639/Archivecopy8.zip" + }, + { + key: "668bc27eb51cc50008706635/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc27eb51cc50008706635/Archivecopy7.zip" + }, + { + key: "668bc273b51cc50008706631/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc273b51cc50008706631/Archivecopy6.zip" + }, + { + key: "668bc269b51cc5000870662d/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc269b51cc5000870662d/Archivecopy5.zip" + }, + { + key: "668bc25db51cc50008706629/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc25db51cc50008706629/Archivecopy4.zip" + }, + { + key: "668bc253b51cc50008706625/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc253b51cc50008706625/Archivecopy3.zip" + }, + { + key: "668bc245b51cc50008706621/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc245b51cc50008706621/Archivecopy2.zip" + }, + { + key: "668bc237b51cc5000870661d/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc237b51cc5000870661d/Archive.zip" + }, + { + key: "668bc228b51cc50008706619/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc228b51cc50008706619/Archivecopy.zip" + }, + { + key: "668bc21db51cc50008706615/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc21db51cc50008706615/Archivecopy9.zip" + }, + { + key: "668bc212b51cc50008706611/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc212b51cc50008706611/Archivecopy8.zip" + }, + { + key: "668bc206b51cc5000870660d/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc206b51cc5000870660d/Archivecopy7.zip" + }, + { + key: "668bc1fbb51cc50008706609/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc1fbb51cc50008706609/Archivecopy6.zip" + }, + { + key: "668bc1f0b51cc50008706605/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc1f0b51cc50008706605/Archivecopy5.zip" + }, + { + key: "668bc1ccb51cc50008706601/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc1ccb51cc50008706601/Archivecopy4.zip" + }, + { + key: "668bc1a7b51cc500087065fd/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc1a7b51cc500087065fd/Archivecopy3.zip" + }, + { + key: "668bc183b51cc500087065f9/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc183b51cc500087065f9/Archivecopy2.zip" + }, + { + key: "668bc161b51cc500087065f5/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc161b51cc500087065f5/Archive.zip" + }, + { + key: "668bc13fb51cc500087065f1/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc13fb51cc500087065f1/Archivecopy.zip" + }, + { + key: "668bc116b51cc500087065ed/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc116b51cc500087065ed/Archivecopy9.zip" + }, + { + key: "668bc0f4b51cc500087065e9/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0f4b51cc500087065e9/Archivecopy8.zip" + }, + { + key: "668bc0e7b51cc500087065e5/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0e7b51cc500087065e5/Archivecopy7.zip" + }, + { + key: "668bc0d9b51cc500087065e1/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0d9b51cc500087065e1/Archivecopy6.zip" + }, + { + key: "668bc0cd5b0c400008abbf61/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0cd5b0c400008abbf61/Archivecopy5.zip" + }, + { + key: "668bc0bd5b0c400008abbf5d/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0bd5b0c400008abbf5d/Archivecopy4.zip" + }, + { + key: "668bc0ae5b0c400008abbf59/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0ae5b0c400008abbf59/Archivecopy3.zip" + }, + { + key: "668bc0a15b0c400008abbf55/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0a15b0c400008abbf55/Archivecopy2.zip" + }, + { + key: "668bc0935b0c400008abbf51/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0935b0c400008abbf51/Archive.zip" + }, + { + key: "668bc0825b0c400008abbf4d/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0825b0c400008abbf4d/Archivecopy.zip" + }, + { + key: "668bc0755b0c400008abbf49/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0755b0c400008abbf49/Archivecopy9.zip" + }, + { + key: "668bc0675b0c400008abbf45/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0675b0c400008abbf45/Archivecopy8.zip" + }, + { + key: "668bc0585b0c400008abbf41/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0585b0c400008abbf41/Archivecopy7.zip" + }, + { + key: "668bc04a5b0c400008abbf3d/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc04a5b0c400008abbf3d/Archivecopy6.zip" + }, + { + key: "668bc03d5b0c400008abbf39/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc03d5b0c400008abbf39/Archivecopy5.zip" + }, + { + key: "668bc02d5b0c400008abbf35/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc02d5b0c400008abbf35/Archivecopy4.zip" + }, + { + key: "668bc01d5b0c400008abbf31/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc01d5b0c400008abbf31/Archivecopy3.zip" + }, + { + key: "668bc00f5b0c400008abbf2d/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc00f5b0c400008abbf2d/Archivecopy2.zip" + }, + { + key: "668bc0015b0c400008abbf29/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc0015b0c400008abbf29/Archive.zip" + }, + { + key: "668bbb49ebd0de0008bb2d10/assets.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bbb49ebd0de0008bb2d10/assets.zip" + }, + { + key: "668bbb03ebd0de0008bb2d0c/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bbb03ebd0de0008bb2d0c/zip-of-zips.zip" + }, + { + key: "668bbab52813790008174d6c/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bbab52813790008174d6c/zip-of-zips.zip" + }, + { + key: "668bba6c2813790008174d68/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bba6c2813790008174d68/zip-of-zips.zip" + }, + { + key: "668bba202813790008174d64/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bba202813790008174d64/zip-of-zips.zip" + }, + { + key: "668bb0f1a5464800081d93fa/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb0f1a5464800081d93fa/Archive.zip" + }, + { + key: "668bb0e2a5464800081d93f6/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb0e2a5464800081d93f6/Archivecopy.zip" + }, + { + key: "668bb0daa5464800081d93f2/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb0daa5464800081d93f2/Archivecopy9.zip" + }, + { + key: "668bb0cda5464800081d93ee/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb0cda5464800081d93ee/Archivecopy8.zip" + }, + { + key: "668bb0c1a5464800081d93ea/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb0c1a5464800081d93ea/Archivecopy7.zip" + }, + { + key: "668bb0b6a5464800081d93e6/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb0b6a5464800081d93e6/Archivecopy6.zip" + }, + { + key: "668bb0a9a5464800081d93e2/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb0a9a5464800081d93e2/Archivecopy5.zip" + }, + { + key: "668bb09aa5464800081d93de/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb09aa5464800081d93de/Archivecopy4.zip" + }, + { + key: "668bb090a5464800081d93da/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb090a5464800081d93da/Archivecopy3.zip" + }, + { + key: "668bb085a5464800081d93d6/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb085a5464800081d93d6/Archivecopy2.zip" + }, + { + key: "668bb061a5464800081d93d2/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb061a5464800081d93d2/zip-of-zips.zip" + }, + { + key: "668bb055a5464800081d93ce/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb055a5464800081d93ce/Archive.zip" + }, + { + key: "668bb049a5464800081d93ca/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb049a5464800081d93ca/Archivecopy.zip" + }, + { + key: "668bb03da5464800081d93c6/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb03da5464800081d93c6/Archivecopy9.zip" + }, + { + key: "668bb031a5464800081d93c2/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb031a5464800081d93c2/Archivecopy8.zip" + }, + { + key: "668bb027a5464800081d93be/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb027a5464800081d93be/Archivecopy7.zip" + }, + { + key: "668bb01aa5464800081d93ba/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb01aa5464800081d93ba/Archivecopy6.zip" + }, + { + key: "668bb00da5464800081d93b6/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb00da5464800081d93b6/Archivecopy5.zip" + }, + { + key: "668bb002a5464800081d93b2/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bb002a5464800081d93b2/Archivecopy4.zip" + }, + { + key: "668baff4a5464800081d93ae/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668baff4a5464800081d93ae/Archivecopy3.zip" + }, + { + key: "668bafe7a5464800081d93aa/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bafe7a5464800081d93aa/Archivecopy2.zip" + }, + { + key: "668ba93be262ee000898664e/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba93be262ee000898664e/Archivecopy.zip" + }, + { + key: "668ba92de262ee000898664a/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba92de262ee000898664a/Archivecopy9.zip" + }, + { + key: "668ba922e262ee0008986646/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba922e262ee0008986646/Archivecopy8.zip" + }, + { + key: "668ba917e262ee0008986642/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba917e262ee0008986642/Archivecopy7.zip" + }, + { + key: "668ba909e262ee000898663e/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba909e262ee000898663e/Archivecopy6.zip" + }, + { + key: "668ba8fce262ee000898663a/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba8fce262ee000898663a/Archivecopy5.zip" + }, + { + key: "668ba8efe262ee0008986636/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba8efe262ee0008986636/Archivecopy4.zip" + }, + { + key: "668ba8e4e262ee0008986632/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba8e4e262ee0008986632/Archivecopy3.zip" + }, + { + key: "668ba8d9e262ee000898662e/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba8d9e262ee000898662e/Archivecopy2.zip" + }, + { + key: "668ba8a5e262ee0008986628/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba8a5e262ee0008986628/Archive.zip" + }, + { + key: "668ba897e262ee0008986624/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba897e262ee0008986624/Archivecopy.zip" + }, + { + key: "668ba886e262ee0008986620/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba886e262ee0008986620/Archivecopy9.zip" + }, + { + key: "668ba876e262ee000898661c/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba876e262ee000898661c/Archivecopy8.zip" + }, + { + key: "668ba867e262ee0008986618/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba867e262ee0008986618/Archivecopy7.zip" + }, + { + key: "668ba856e262ee0008986614/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba856e262ee0008986614/Archivecopy6.zip" + }, + { + key: "668ba848e262ee0008986610/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba848e262ee0008986610/Archivecopy5.zip" + }, + { + key: "668ba83be262ee000898660c/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba83be262ee000898660c/Archivecopy4.zip" + }, + { + key: "668ba82fe262ee0008986608/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba82fe262ee0008986608/Archivecopy3.zip" + }, + { + key: "668ba81ee262ee0008986604/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba81ee262ee0008986604/Archivecopy2.zip" + }, + { + key: "668ba7f9e262ee0008986600/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba7f9e262ee0008986600/zip-of-zips.zip" + }, + { + key: "668ba58ef599020008a75841/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba58ef599020008a75841/Archive.zip" + }, + { + key: "668ba57ff599020008a7583d/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba57ff599020008a7583d/Archivecopy.zip" + }, + { + key: "668ba56ff599020008a75839/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba56ff599020008a75839/Archivecopy9.zip" + }, + { + key: "668ba560f599020008a75835/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba560f599020008a75835/Archivecopy8.zip" + }, + { + key: "668ba552f599020008a75831/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba552f599020008a75831/Archivecopy7.zip" + }, + { + key: "668ba542f599020008a7582d/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba542f599020008a7582d/Archivecopy6.zip" + }, + { + key: "668ba536f599020008a75829/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba536f599020008a75829/Archivecopy5.zip" + }, + { + key: "668ba528f599020008a75825/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba528f599020008a75825/Archivecopy4.zip" + }, + { + key: "668ba517f599020008a75821/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba517f599020008a75821/Archivecopy3.zip" + }, + { + key: "668ba505f599020008a7581d/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba505f599020008a7581d/Archivecopy2.zip" + }, + { + key: "668ba4e7f599020008a75819/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba4e7f599020008a75819/zip-of-zips.zip" + }, + { + key: "668b9ce7f599020008a752b8/8lycoyxp1-WEBINY_PAGE_EXPORT.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9ce7f599020008a752b8/8lycoyxp1-WEBINY_PAGE_EXPORT.zip" + }, + { + key: "668b9be609c3440008356b17/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9be609c3440008356b17/Archive.zip" + }, + { + key: "668b9bd709c3440008356b13/Archivecopy.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9bd709c3440008356b13/Archivecopy.zip" + }, + { + key: "668b9bc909c3440008356b0f/Archivecopy9.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9bc909c3440008356b0f/Archivecopy9.zip" + }, + { + key: "668b9bbb09c3440008356b0b/Archivecopy8.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9bbb09c3440008356b0b/Archivecopy8.zip" + }, + { + key: "668b9bad09c3440008356b07/Archivecopy7.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9bad09c3440008356b07/Archivecopy7.zip" + }, + { + key: "668b9ba209c3440008356b03/Archivecopy6.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9ba209c3440008356b03/Archivecopy6.zip" + }, + { + key: "668b9b9809c3440008356aff/Archivecopy5.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9b9809c3440008356aff/Archivecopy5.zip" + }, + { + key: "668b9b8d09c3440008356afb/Archivecopy4.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9b8d09c3440008356afb/Archivecopy4.zip" + }, + { + key: "668b9b8109c3440008356af7/Archivecopy3.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9b8109c3440008356af7/Archivecopy3.zip" + }, + { + key: "668b9b7109c3440008356af3/Archivecopy2.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9b7109c3440008356af3/Archivecopy2.zip" + }, + { + key: "668b9b4d09c3440008356aef/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9b4d09c3440008356aef/zip-of-zips.zip" + }, + { + key: "668b9a51f599020008a752b2/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9a51f599020008a752b2/zip-of-zips.zip" + }, + { + key: "668b9997bdc3320008e9c0f2/Archive.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9997bdc3320008e9c0f2/Archive.zip" + }, + { + key: "668b9978bdc3320008e9c0ee/zip-of-zips.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9978bdc3320008e9c0ee/zip-of-zips.zip" + }, + { + key: "666a9347a4fc6d0008a7b5ae/8lxcni93y-WEBINY_PAGE_EXPORT.zip", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/666a9347a4fc6d0008a7b5ae/8lxcni93y-WEBINY_PAGE_EXPORT.zip" + }, + { + key: "668bc9c1858df900083c6fc1/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9c1858df900083c6fc1/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668bc9be858df900083c6fbd/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9be858df900083c6fbd/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668bc9bb858df900083c6fb9/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9bb858df900083c6fb9/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668bc9b8858df900083c6fb5/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9b8858df900083c6fb5/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668bc9b3858df900083c6fb1/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9b3858df900083c6fb1/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668bc9b0858df900083c6fad/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9b0858df900083c6fad/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668bc9ad858df900083c6fa9/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9ad858df900083c6fa9/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668bc9a8858df900083c6fa5/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9a8858df900083c6fa5/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668bc9a5858df900083c6fa1/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9a5858df900083c6fa1/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668bc9a2858df900083c6f9d/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9a2858df900083c6f9d/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668bc9a0858df900083c6f99/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc9a0858df900083c6f99/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668bc99e858df900083c6f95/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc99e858df900083c6f95/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668bc99c858df900083c6f91/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc99c858df900083c6f91/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668bc99a858df900083c6f8d/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc99a858df900083c6f8d/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668bc998858df900083c6f89/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc998858df900083c6f89/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668bc995858df900083c6f85/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc995858df900083c6f85/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668bc991858df900083c6f81/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc991858df900083c6f81/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668bc98e858df900083c6f7d/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc98e858df900083c6f7d/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668bc98c858df900083c6f79/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc98c858df900083c6f79/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668bc987858df900083c6f75/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc987858df900083c6f75/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668bc985858df900083c6f71/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc985858df900083c6f71/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668bc983858df900083c6f6d/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc983858df900083c6f6d/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668bc981858df900083c6f69/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc981858df900083c6f69/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668bc97f858df900083c6f65/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc97f858df900083c6f65/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668bc97d858df900083c6f61/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc97d858df900083c6f61/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668bc97b858df900083c6f5d/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc97b858df900083c6f5d/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668bc979858df900083c6f59/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc979858df900083c6f59/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668bc977858df900083c6f55/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc977858df900083c6f55/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668bc974858df900083c6f51/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc974858df900083c6f51/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668bc970858df900083c6f4d/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc970858df900083c6f4d/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668bc96e858df900083c6f49/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc96e858df900083c6f49/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668bc96b858df900083c6f45/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc96b858df900083c6f45/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668bc969858df900083c6f41/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc969858df900083c6f41/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668bc966858df900083c6f3d/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668bc966858df900083c6f3d/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba277f599020008a75814/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba277f599020008a75814/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba275f599020008a75810/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba275f599020008a75810/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba274f599020008a7580c/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba274f599020008a7580c/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba272f599020008a75808/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba272f599020008a75808/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba270f599020008a75804/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba270f599020008a75804/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba26ef599020008a75800/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba26ef599020008a75800/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba26df599020008a757fc/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba26df599020008a757fc/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba269f599020008a757f8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba269f599020008a757f8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba268f599020008a757f4/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba268f599020008a757f4/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba266f599020008a757f0/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba266f599020008a757f0/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba265f599020008a757ec/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba265f599020008a757ec/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba264f599020008a757e8/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba264f599020008a757e8/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba263f599020008a757e4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba263f599020008a757e4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba262f599020008a757e0/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba262f599020008a757e0/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba260f599020008a757dc/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba260f599020008a757dc/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba25ff599020008a757d8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba25ff599020008a757d8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba25cf599020008a757d4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba25cf599020008a757d4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba25bf599020008a757d0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba25bf599020008a757d0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668ba25af599020008a757cc/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba25af599020008a757cc/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba256f599020008a757c8/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba256f599020008a757c8/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba255f599020008a757c4/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba255f599020008a757c4/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba254f599020008a757c0/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba254f599020008a757c0/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba253f599020008a757bc/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba253f599020008a757bc/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba252f599020008a757b8/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba252f599020008a757b8/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba251f599020008a757b4/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba251f599020008a757b4/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668ba250f599020008a757b0/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba250f599020008a757b0/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba250f599020008a757ac/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba250f599020008a757ac/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba24ff599020008a757a8/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba24ff599020008a757a8/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba24df599020008a757a4/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba24df599020008a757a4/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba24af599020008a757a0/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba24af599020008a757a0/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba249f599020008a7579c/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba249f599020008a7579c/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba248f599020008a75798/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba248f599020008a75798/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba247f599020008a75794/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba247f599020008a75794/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba246f599020008a75790/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba246f599020008a75790/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba245f599020008a7578c/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba245f599020008a7578c/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba244f599020008a75788/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba244f599020008a75788/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba244f599020008a75784/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba244f599020008a75784/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba243f599020008a75780/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba243f599020008a75780/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba241f599020008a7577c/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba241f599020008a7577c/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba240f599020008a75778/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba240f599020008a75778/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba23ef599020008a75774/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba23ef599020008a75774/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba23bf599020008a75770/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba23bf599020008a75770/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba23af599020008a7576c/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba23af599020008a7576c/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba238f599020008a75768/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba238f599020008a75768/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba237f599020008a75764/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba237f599020008a75764/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba236f599020008a75760/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba236f599020008a75760/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba235f599020008a7575c/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba235f599020008a7575c/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba234f599020008a75758/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba234f599020008a75758/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba233f599020008a75754/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba233f599020008a75754/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba232f599020008a75750/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba232f599020008a75750/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba22ff599020008a7574c/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba22ff599020008a7574c/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba22df599020008a75748/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba22df599020008a75748/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668ba22cf599020008a75744/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba22cf599020008a75744/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba228f599020008a75740/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba228f599020008a75740/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba227f599020008a7573c/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba227f599020008a7573c/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba225f599020008a75738/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba225f599020008a75738/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba225f599020008a75734/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba225f599020008a75734/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba224f599020008a75730/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba224f599020008a75730/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba223f599020008a7572c/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba223f599020008a7572c/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668ba221f599020008a75728/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba221f599020008a75728/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba220f599020008a75724/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba220f599020008a75724/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba220f599020008a75720/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba220f599020008a75720/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba21ef599020008a7571c/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba21ef599020008a7571c/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba21bf599020008a75718/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba21bf599020008a75718/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba21af599020008a75714/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba21af599020008a75714/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba21af599020008a75710/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba21af599020008a75710/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba219f599020008a7570c/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba219f599020008a7570c/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba217f599020008a75708/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba217f599020008a75708/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba214f599020008a75704/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba214f599020008a75704/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba213f599020008a75700/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba213f599020008a75700/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba212f599020008a756fc/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba212f599020008a756fc/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba211f599020008a756f8/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba211f599020008a756f8/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba20ff599020008a756f4/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba20ff599020008a756f4/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba20df599020008a756f0/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba20df599020008a756f0/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba20cf599020008a756ec/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba20cf599020008a756ec/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba207f599020008a756e8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba207f599020008a756e8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba206f599020008a756e4/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba206f599020008a756e4/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba204f599020008a756e0/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba204f599020008a756e0/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba203f599020008a756dc/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba203f599020008a756dc/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba202f599020008a756d8/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba202f599020008a756d8/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba201f599020008a756d4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba201f599020008a756d4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba200f599020008a756d0/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba200f599020008a756d0/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba1fff599020008a756cc/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1fff599020008a756cc/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba1fef599020008a756c8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1fef599020008a756c8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba1fbf599020008a756c4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1fbf599020008a756c4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba1faf599020008a756c0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1faf599020008a756c0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668ba1f9f599020008a756bc/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f9f599020008a756bc/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba1f6f599020008a756b8/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f6f599020008a756b8/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba1f5f599020008a756b4/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f5f599020008a756b4/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba1f4f599020008a756b0/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f4f599020008a756b0/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba1f3f599020008a756ac/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f3f599020008a756ac/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba1f3f599020008a756a8/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f3f599020008a756a8/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba1f2f599020008a756a4/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f2f599020008a756a4/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "666bfc2abacd2d0008acbfbf/rozi-golub.jpeg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/666bfc2abacd2d0008acbfbf/rozi-golub.jpeg" + }, + { + key: "668ba1f1f599020008a756a0/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f1f599020008a756a0/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba1f0f599020008a7569c/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1f0f599020008a7569c/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba1eaf599020008a75688/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1eaf599020008a75688/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba1ebf599020008a7568c/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1ebf599020008a7568c/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba1ecf599020008a75690/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1ecf599020008a75690/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba1eef599020008a75694/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1eef599020008a75694/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba1eff599020008a75698/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1eff599020008a75698/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba156f599020008a754e0/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba156f599020008a754e0/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba156f599020008a754dc/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba156f599020008a754dc/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba155f599020008a754d8/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba155f599020008a754d8/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba153f599020008a754d4/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba153f599020008a754d4/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba152f599020008a754d0/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba152f599020008a754d0/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba148f599020008a754bc/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba148f599020008a754bc/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba149f599020008a754c0/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba149f599020008a754c0/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba14bf599020008a754c4/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba14bf599020008a754c4/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba14cf599020008a754c8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba14cf599020008a754c8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba150f599020008a754cc/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba150f599020008a754cc/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba147f599020008a754b8/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba147f599020008a754b8/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba146f599020008a754b4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba146f599020008a754b4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba146f599020008a754b0/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba146f599020008a754b0/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba145f599020008a754ac/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba145f599020008a754ac/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba143f599020008a754a8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba143f599020008a754a8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba13bf599020008a75494/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba13bf599020008a75494/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba13bf599020008a75498/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba13bf599020008a75498/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba13ff599020008a7549c/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba13ff599020008a7549c/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba140f599020008a754a0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba140f599020008a754a0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668ba141f599020008a754a4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba141f599020008a754a4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba13af599020008a75490/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba13af599020008a75490/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba139f599020008a7548c/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba139f599020008a7548c/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba138f599020008a75488/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba138f599020008a75488/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba137f599020008a75484/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba137f599020008a75484/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668ba136f599020008a75480/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba136f599020008a75480/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba130f599020008a7546c/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba130f599020008a7546c/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba131f599020008a75470/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba131f599020008a75470/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba133f599020008a75474/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba133f599020008a75474/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba135f599020008a75478/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba135f599020008a75478/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba135f599020008a7547c/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba135f599020008a7547c/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba130f599020008a75468/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba130f599020008a75468/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba12ff599020008a75464/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba12ff599020008a75464/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba12ef599020008a75460/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba12ef599020008a75460/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba0d4f599020008a7545b/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0d4f599020008a7545b/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba0d0f599020008a75457/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0d0f599020008a75457/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba0c0f599020008a75443/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0c0f599020008a75443/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba0c3f599020008a75447/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0c3f599020008a75447/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba0c5f599020008a7544b/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0c5f599020008a7544b/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba0cdf599020008a7544f/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0cdf599020008a7544f/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba0cff599020008a75453/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0cff599020008a75453/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba0b8f599020008a7543f/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0b8f599020008a7543f/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba0b5f599020008a7543b/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0b5f599020008a7543b/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba0b2f599020008a75437/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0b2f599020008a75437/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba0b1f599020008a75433/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0b1f599020008a75433/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba0aff599020008a7542f/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0aff599020008a7542f/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba0a2f599020008a7541b/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0a2f599020008a7541b/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba0a8f599020008a7541f/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0a8f599020008a7541f/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba0abf599020008a75423/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0abf599020008a75423/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba0adf599020008a75427/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0adf599020008a75427/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba0aef599020008a7542b/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0aef599020008a7542b/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba0a0f599020008a75417/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba0a0f599020008a75417/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668ba09ff599020008a75413/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba09ff599020008a75413/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba095f599020008a7540f/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba095f599020008a7540f/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba093f599020008a7540b/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba093f599020008a7540b/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba090f599020008a75407/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba090f599020008a75407/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba08af599020008a753f3/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba08af599020008a753f3/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba08af599020008a753f7/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba08af599020008a753f7/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba08cf599020008a753fb/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba08cf599020008a753fb/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668ba08ef599020008a753ff/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba08ef599020008a753ff/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba08ff599020008a75403/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba08ff599020008a75403/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba089f599020008a753ef/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba089f599020008a753ef/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba087f599020008a753eb/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba087f599020008a753eb/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba080f599020008a753e7/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba080f599020008a753e7/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba07ff599020008a753e3/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba07ff599020008a753e3/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba07df599020008a753df/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba07df599020008a753df/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba06cf599020008a753cb/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba06cf599020008a753cb/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba06df599020008a753cf/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba06df599020008a753cf/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba06ef599020008a753d3/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba06ef599020008a753d3/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba077f599020008a753d7/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba077f599020008a753d7/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba07bf599020008a753db/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba07bf599020008a753db/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba06cf599020008a753c7/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba06cf599020008a753c7/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba06af599020008a753c3/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba06af599020008a753c3/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba069f599020008a753bf/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba069f599020008a753bf/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba068f599020008a753bb/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba068f599020008a753bb/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba063f599020008a753b7/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba063f599020008a753b7/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba05ff599020008a753a3/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba05ff599020008a753a3/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba060f599020008a753a7/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba060f599020008a753a7/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba060f599020008a753ab/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba060f599020008a753ab/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba061f599020008a753af/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba061f599020008a753af/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba062f599020008a753b3/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba062f599020008a753b3/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba05ef599020008a7539f/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba05ef599020008a7539f/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba05df599020008a7539b/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba05df599020008a7539b/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba05cf599020008a75397/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba05cf599020008a75397/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba05af599020008a75393/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba05af599020008a75393/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba058f599020008a7538f/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba058f599020008a7538f/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668ba051f599020008a7537b/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba051f599020008a7537b/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba051f599020008a7537f/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba051f599020008a7537f/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba053f599020008a75383/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba053f599020008a75383/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba053f599020008a75387/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba053f599020008a75387/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba057f599020008a7538b/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba057f599020008a7538b/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba050f599020008a75377/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba050f599020008a75377/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba04ff599020008a75373/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba04ff599020008a75373/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668ba04ef599020008a7536f/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba04ef599020008a7536f/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba04ef599020008a7536b/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba04ef599020008a7536b/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba04df599020008a75367/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba04df599020008a75367/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba046f599020008a75353/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba046f599020008a75353/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba047f599020008a75357/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba047f599020008a75357/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba047f599020008a7535b/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba047f599020008a7535b/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba049f599020008a7535f/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba049f599020008a7535f/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba04bf599020008a75363/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba04bf599020008a75363/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "666ff7feb5d8ac0008dee653/prijesanacije.jpeg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/666ff7feb5d8ac0008dee653/prijesanacije.jpeg" + }, + { + key: "668b9f57f599020008a752e2/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f57f599020008a752e2/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668b9f54f599020008a752de/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f54f599020008a752de/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668b9f53f599020008a752da/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f53f599020008a752da/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668b9f58f599020008a752e6/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f58f599020008a752e6/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668b9f5af599020008a752ea/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f5af599020008a752ea/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668b9f60f599020008a752fe/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f60f599020008a752fe/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668b9f5ef599020008a752fa/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f5ef599020008a752fa/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668b9f5df599020008a752f6/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f5df599020008a752f6/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668b9f5cf599020008a752f2/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f5cf599020008a752f2/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668b9f5bf599020008a752ee/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f5bf599020008a752ee/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668b9f61f599020008a75302/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f61f599020008a75302/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668b9f64f599020008a75306/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f64f599020008a75306/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668b9f65f599020008a7530a/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f65f599020008a7530a/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668b9f67f599020008a7530e/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f67f599020008a7530e/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668b9f6af599020008a75312/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f6af599020008a75312/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668b9fd7f599020008a75326/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fd7f599020008a75326/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668b9fd6f599020008a75322/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fd6f599020008a75322/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668b9f6df599020008a7531e/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f6df599020008a7531e/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668b9f6cf599020008a7531a/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f6cf599020008a7531a/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668b9f6bf599020008a75316/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f6bf599020008a75316/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668b9fdaf599020008a7532a/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fdaf599020008a7532a/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668b9fdbf599020008a7532e/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fdbf599020008a7532e/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668b9fddf599020008a75332/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fddf599020008a75332/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668b9fdef599020008a75336/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fdef599020008a75336/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668b9fe1f599020008a7533a/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fe1f599020008a7533a/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba045f599020008a7534f/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba045f599020008a7534f/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668b9fe5f599020008a7534a/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fe5f599020008a7534a/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668b9fe4f599020008a75346/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fe4f599020008a75346/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668b9fe3f599020008a75342/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fe3f599020008a75342/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668b9fe3f599020008a7533e/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9fe3f599020008a7533e/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668b9875f599020008a752ab/Screenshot2024-07-08at09.42.37.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9875f599020008a752ab/Screenshot2024-07-08at09.42.37.png" + }, + { + key: "668b9838f599020008a7529f/Screenshot2024-07-08at09.40.52.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9838f599020008a7529f/Screenshot2024-07-08at09.40.52.png" + }, + { + key: "668b97ffbdc3320008e9c0e0/Screenshot2024-07-08at09.40.37.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b97ffbdc3320008e9c0e0/Screenshot2024-07-08at09.40.37.png" + }, + { + key: "668b9f4ff599020008a752ce/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f4ff599020008a752ce/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668b9f50f599020008a752d2/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f50f599020008a752d2/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668b9f52f599020008a752d6/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f52f599020008a752d6/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668b9f4ef599020008a752ca/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f4ef599020008a752ca/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668b9f4ef599020008a752c6/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9f4ef599020008a752c6/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668b9d49c3af0d00087efc5a/8lewsfjej-window-vuejs-8ff82bc1d9e0dddb3b935f3c648eebf7.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d49c3af0d00087efc5a/8lewsfjej-window-vuejs-8ff82bc1d9e0dddb3b935f3c648eebf7.svg" + }, + { + key: "668b9d49c3af0d00087efc59/8lewsj4p2-cover(11).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d49c3af0d00087efc59/8lewsj4p2-cover(11).png" + }, + { + key: "668b9d49c3af0d00087efc58/8lewsjhd6-chrissy-61e1496694bf224c6e071f7a9729b448.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d49c3af0d00087efc58/8lewsjhd6-chrissy-61e1496694bf224c6e071f7a9729b448.png" + }, + { + key: "668ba1e1f599020008a75674/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1e1f599020008a75674/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba1e2f599020008a75678/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1e2f599020008a75678/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba1e4f599020008a7567c/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1e4f599020008a7567c/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba1e6f599020008a75680/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1e6f599020008a75680/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba1e9f599020008a75684/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1e9f599020008a75684/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba1e0f599020008a75670/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1e0f599020008a75670/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba1ddf599020008a7566c/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1ddf599020008a7566c/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba1dcf599020008a75668/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1dcf599020008a75668/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba1dbf599020008a75664/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1dbf599020008a75664/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba1d7f599020008a75660/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1d7f599020008a75660/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba1d1f599020008a7564c/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1d1f599020008a7564c/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba1d2f599020008a75650/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1d2f599020008a75650/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba1d3f599020008a75654/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1d3f599020008a75654/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba1d4f599020008a75658/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1d4f599020008a75658/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba1d6f599020008a7565c/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1d6f599020008a7565c/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba1d0f599020008a75648/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1d0f599020008a75648/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba1cff599020008a75644/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1cff599020008a75644/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba1cdf599020008a75640/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1cdf599020008a75640/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba1cbf599020008a7563c/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1cbf599020008a7563c/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba1c9f599020008a75638/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1c9f599020008a75638/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668b9d48d2c092000925956c/9lb2efs17-8l9zs5x6m-9l9ilstvx-Group867.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c092000925956c/9lb2efs17-8l9zs5x6m-9l9ilstvx-Group867.png" + }, + { + key: "668b9d49c3af0d00087efc56/8lewsm9au-cover(13).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d49c3af0d00087efc56/8lewsm9au-cover(13).png" + }, + { + key: "668b9d49c3af0d00087efc57/8lewskov6-cover(12).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d49c3af0d00087efc57/8lewskov6-cover(12).png" + }, + { + key: "668b9d48d2c092000925956b/8lcq9ajzq-icon.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c092000925956b/8lcq9ajzq-icon.svg" + }, + { + key: "668b9d48d2c092000925956a/8lcq9dyld-Frame868.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c092000925956a/8lcq9dyld-Frame868.svg" + }, + { + key: "668b9d48d2c0920009259569/8lcq9mv3i-Frame867.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c0920009259569/8lcq9mv3i-Frame867.svg" + }, + { + key: "668b9d48d2c0920009259568/8lcqa5mh5-Frame866.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c0920009259568/8lcqa5mh5-Frame866.svg" + }, + { + key: "668b9d48d2c0920009259567/8lesfgf02-Group850.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c0920009259567/8lesfgf02-Group850.svg" + }, + { + key: "668b9d3d89b4a600083b1aad/8ldn8cu3o-icon(2).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aad/8ldn8cu3o-icon(2).svg" + }, + { + key: "668b9d3d89b4a600083b1aac/8ldn8d633-Frame287(5).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aac/8ldn8d633-Frame287(5).svg" + }, + { + key: "668b9d3d89b4a600083b1aab/8ldn8efor-Frame287(6).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aab/8ldn8efor-Frame287(6).svg" + }, + { + key: "668b9d3d89b4a600083b1aaa/8ldn8eo7k-icon7(8).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aaa/8ldn8eo7k-icon7(8).svg" + }, + { + key: "668b9d3d89b4a600083b1aa9/8ldn90s8x-Frame287(7).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aa9/8ldn90s8x-Frame287(7).svg" + }, + { + key: "668b9d3d89b4a600083b1aa4/8ldn96u5a-Frame287(9).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aa4/8ldn96u5a-Frame287(9).svg" + }, + { + key: "668b9d3d89b4a600083b1aa5/8ldn96lkn-icon7(11).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aa5/8ldn96lkn-icon7(11).svg" + }, + { + key: "668b9d3d89b4a600083b1aa6/8ldn93cue-icon8(1).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aa6/8ldn93cue-icon8(1).svg" + }, + { + key: "668b9d3d89b4a600083b1aa7/8ldn935ct-Frame287(8).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aa7/8ldn935ct-Frame287(8).svg" + }, + { + key: "668b9d3d89b4a600083b1aa8/8ldn90yqv-icon7(9).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aa8/8ldn90yqv-icon7(9).svg" + }, + { + key: "668b9d3d89b4a600083b1aa3/8ldx5i1m7-icon7(12).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aa3/8ldx5i1m7-icon7(12).svg" + }, + { + key: "668b9d3b66d4760008eeb444/8lfcjipul-A4-49(2).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb444/8lfcjipul-A4-49(2).png" + }, + { + key: "668b9d3b66d4760008eeb443/8lfck9s5e-Frame287(4).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb443/8lfck9s5e-Frame287(4).svg" + }, + { + key: "668b9d3b66d4760008eeb442/8lfckvky2-featureImage.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb442/8lfckvky2-featureImage.png" + }, + { + key: "668b9d3b66d4760008eeb441/8lfckw39p-featureImage(1).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb441/8lfckw39p-featureImage(1).png" + }, + { + key: "668b9d3b66d4760008eeb43c/8lfgrkcmu-Frame287(7).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb43c/8lfgrkcmu-Frame287(7).svg" + }, + { + key: "668b9d3b66d4760008eeb43d/8lfgrjrje-case.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb43d/8lfgrjrje-case.png" + }, + { + key: "668b9d3b66d4760008eeb43e/8lfgrgo5d-Frame287(6).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb43e/8lfgrgo5d-Frame287(6).svg" + }, + { + key: "668b9d3b66d4760008eeb43f/8lfgrfdqt-Header(1).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb43f/8lfgrfdqt-Header(1).png" + }, + { + key: "668b9d3b66d4760008eeb440/8lfckxks5-Frame287(5).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb440/8lfckxks5-Frame287(5).svg" + }, + { + key: "668b9d3b66d4760008eeb43b/8lfgrt7pp-case2.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b66d4760008eeb43b/8lfgrt7pp-case2.png" + }, + { + key: "668b9d3b44ce8c00087bf5d6/8lfju5zdr-smarkt-reha-logo-fe0f2829b6c19c36f72d32147044890b.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b44ce8c00087bf5d6/8lfju5zdr-smarkt-reha-logo-fe0f2829b6c19c36f72d32147044890b.png" + }, + { + key: "668b9d3b44ce8c00087bf5d5/8lfju7633-oev-case-study-screenshot-60c495f07bd974d34fb99643346ce119.jpeg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b44ce8c00087bf5d5/8lfju7633-oev-case-study-screenshot-60c495f07bd974d34fb99643346ce119.jpeg" + }, + { + key: "668b9d3b44ce8c00087bf5d4/8lfjuc4jv-smarkt-logo-87c46e6c99b090d68157f7886e4f956b.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b44ce8c00087bf5d4/8lfjuc4jv-smarkt-logo-87c46e6c99b090d68157f7886e4f956b.png" + }, + { + key: "668b9d3b44ce8c00087bf5d3/8lfjuc4iv-reha-logo-83faf4e1b18dafc352740c76b4efc394.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3b44ce8c00087bf5d3/8lfjuc4iv-reha-logo-83faf4e1b18dafc352740c76b4efc394.png" + }, + { + key: "668b9d3ad30bf3000805daca/8lews3snh-cover(6).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805daca/8lews3snh-cover(6).png" + }, + { + key: "668b9d3ad30bf3000805dacb/8lews3kpd-taminoturoko-briggs-7ee02cec520076220b2967c6866715d5.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dacb/8lews3kpd-taminoturoko-briggs-7ee02cec520076220b2967c6866715d5.png" + }, + { + key: "668b9d3ad30bf3000805dacc/8lews1ltm-menard-13d3af0ef3cc8e7c77c2497d5c38fd34.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dacc/8lews1ltm-menard-13d3af0ef3cc8e7c77c2497d5c38fd34.png" + }, + { + key: "668b9d3ad30bf3000805dacd/8lews16zv-cover(5).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dacd/8lews16zv-cover(5).png" + }, + { + key: "668b9d3ad30bf3000805dace/8lewry2z5-window-reactjs-a5f8b8ecf26a094184e647d695dcb437.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dace/8lewry2z5-window-reactjs-a5f8b8ecf26a094184e647d695dcb437.svg" + }, + { + key: "668b9d3ad30bf3000805dac9/8lews5bjq-cover(7).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dac9/8lews5bjq-cover(7).png" + }, + { + key: "668b9d3ad30bf3000805dac8/8lews5kse-victory-tuduo-178e7a289aad2d99026596b549824b51.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dac8/8lews5kse-victory-tuduo-178e7a289aad2d99026596b549824b51.png" + }, + { + key: "668b9d3ad30bf3000805dac7/8lews7xkt-cover(8).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dac7/8lews7xkt-cover(8).png" + }, + { + key: "668b9d3ad30bf3000805dac6/8lews8i1x-chris-okoro-a9d990a4815df26f9816b391bd5f858e.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dac6/8lews8i1x-chris-okoro-a9d990a4815df26f9816b391bd5f858e.png" + }, + { + key: "668b9d3ad30bf3000805dac5/8lews9uti-cover(9).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dac5/8lews9uti-cover(9).png" + }, + { + key: "668b9d3a9ea537000987f3fa/8lewqhc67-cover(1).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3fa/8lewqhc67-cover(1).png" + }, + { + key: "668b9d3a9ea537000987f3fb/8lewq3s4q-caleb.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3fb/8lewq3s4q-caleb.png" + }, + { + key: "668b9d3a9ea537000987f3fc/8lewq14x3-cover.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3fc/8lewq14x3-cover.png" + }, + { + key: "668b9d3a9ea537000987f3fd/8lewpciwy-window-nextjs-c4f7f621853dcb3d02e6debb88e8479f.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3fd/8lewpciwy-window-nextjs-c4f7f621853dcb3d02e6debb88e8479f.svg" + }, + { + key: "668b9d3ad30bf3000805dac4/8lewsb5db-cover(10).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3ad30bf3000805dac4/8lewsb5db-cover(10).png" + }, + { + key: "668b9d3a9ea537000987f3f9/8lewqhnyk-fredrick-emmanuel-c3a570bf9dfa7cab90b696268b08336d.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3f9/8lewqhnyk-fredrick-emmanuel-c3a570bf9dfa7cab90b696268b08336d.png" + }, + { + key: "668b9d3a9ea537000987f3f8/8lewqk8ed-cover(2).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3f8/8lewqk8ed-cover(2).png" + }, + { + key: "668b9d3a9ea537000987f3f7/8lewqkjfl-samarpit-shrivastava-b90c1772381fe0545d6cce0c1f662f45.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3f7/8lewqkjfl-samarpit-shrivastava-b90c1772381fe0545d6cce0c1f662f45.png" + }, + { + key: "668b9d3a9ea537000987f3f6/8lewqn2x7-cover(3).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3a9ea537000987f3f6/8lewqn2x7-cover(3).png" + }, + { + key: "668b9d39d703cd00095f494b/8lfguxzyv-Ellipse128(1).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f494b/8lfguxzyv-Ellipse128(1).svg" + }, + { + key: "668b9d39d703cd00095f4946/8lfgzmy8u-ape-factory-logo-d9106f9529b37875a6d0c317256a8612.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4946/8lfgzmy8u-ape-factory-logo-d9106f9529b37875a6d0c317256a8612.png" + }, + { + key: "668b9d39d703cd00095f4947/8lfgzkq9x-antstack-logo-829e1deedd60b9d8e8ebd3b71eb47418.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4947/8lfgzkq9x-antstack-logo-829e1deedd60b9d8e8ebd3b71eb47418.svg" + }, + { + key: "668b9d39d703cd00095f4948/8lfgzdni7-anthill-logo-6d8774dfcb34329022a966c63f070f99.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4948/8lfgzdni7-anthill-logo-6d8774dfcb34329022a966c63f070f99.png" + }, + { + key: "668b9d39d703cd00095f4949/8lfgyz7gw-Line9.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4949/8lfgyz7gw-Line9.svg" + }, + { + key: "668b9d39d703cd00095f494a/8lfgv8d1t-alpine-logo-960c6748d2c546d4fc635a7be0afa026.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f494a/8lfgv8d1t-alpine-logo-960c6748d2c546d4fc635a7be0afa026.png" + }, + { + key: "668b9d39d703cd00095f4945/8lfgzon53-download.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4945/8lfgzon53-download.png" + }, + { + key: "668b9d39d703cd00095f4944/8lfgzqu7l-casasoft-logo-88d6289fe6d76c6db11d32394c94c21a.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4944/8lfgzqu7l-casasoft-logo-88d6289fe6d76c6db11d32394c94c21a.svg" + }, + { + key: "668b9d39d703cd00095f4943/8lfgzsmxb-coding-sans-logo-f90e81e16e5fd9975a340d03cb847eeb.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4943/8lfgzsmxb-coding-sans-logo-f90e81e16e5fd9975a340d03cb847eeb.png" + }, + { + key: "668b9d39d703cd00095f4942/8lfgzuqjj-focusreactive.jpeg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4942/8lfgzuqjj-focusreactive.jpeg" + }, + { + key: "668b9d39d703cd00095f4941/8lfgzx0ri-groundfog-e16a13f2e887ae153159cc53a84a2482.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d39d703cd00095f4941/8lfgzx0ri-groundfog-e16a13f2e887ae153159cc53a84a2482.png" + }, + { + key: "668b9d47bc3a98000853f25d/8lf8ftr2q-img.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d47bc3a98000853f25d/8lf8ftr2q-img.svg" + }, + { + key: "668b9d47bc3a98000853f25e/8lf8frd89-sectiontitle(3).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d47bc3a98000853f25e/8lf8frd89-sectiontitle(3).svg" + }, + { + key: "668b9d48d2c0920009259565/8lesh6uvg-Frame869(2).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c0920009259565/8lesh6uvg-Frame869(2).svg" + }, + { + key: "668b9d48d2c0920009259566/8lesh6a03-Frame870.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d48d2c0920009259566/8lesh6a03-Frame870.svg" + }, + { + key: "668b9d47bc3a98000853f25c/8lf8fvgxi-icon7(1).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d47bc3a98000853f25c/8lf8fvgxi-icon7(1).svg" + }, + { + key: "668b9d47bc3a98000853f25b/8lfjmk33c-Group1012.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d47bc3a98000853f25b/8lfjmk33c-Group1012.svg" + }, + { + key: "668b9d4366d4760008eeb456/8lf8eglnd-sectiontitle(4).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d4366d4760008eeb456/8lf8eglnd-sectiontitle(4).svg" + }, + { + key: "668b9d4366d4760008eeb455/8lf8ejc89-img(1).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d4366d4760008eeb455/8lf8ejc89-img(1).svg" + }, + { + key: "668b9d4366d4760008eeb454/8lf8eklda-Group1027.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d4366d4760008eeb454/8lf8eklda-Group1027.svg" + }, + { + key: "668b9d3d44ce8c00087bf5e5/9lb2efs12-9l9zt6sj9-img(1).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d44ce8c00087bf5e5/9lb2efs12-9l9zt6sj9-img(1).png" + }, + { + key: "668b9d400330df0008016f20/8lewstd5f-joseph-chege-7c5476e8cb25baa929bccf7006df33b1.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d400330df0008016f20/8lewstd5f-joseph-chege-7c5476e8cb25baa929bccf7006df33b1.png" + }, + { + key: "668b9d400330df0008016f21/8lewssrjm-cover(14).png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d400330df0008016f21/8lewssrjm-cover(14).png" + }, + { + key: "668b9d400330df0008016f22/8lewsqsxx-window-flutter-d70f1fd789d58461b90f6aa4411c9d86.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d400330df0008016f22/8lewsqsxx-window-flutter-d70f1fd789d58461b90f6aa4411c9d86.svg" + }, + { + key: "668b9d4366d4760008eeb453/8lf8fhzqs-icon8(4).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d4366d4760008eeb453/8lf8fhzqs-icon8(4).svg" + }, + { + key: "668b9d3d44ce8c00087bf5e4/8ldn93cue-icon8(1).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d44ce8c00087bf5e4/8ldn93cue-icon8(1).svg" + }, + { + key: "668b9d3d44ce8c00087bf5e3/8lepkerbf-sectiontitle(5).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d44ce8c00087bf5e3/8lepkerbf-sectiontitle(5).svg" + }, + { + key: "668b9d3d44ce8c00087bf5e2/8lepkpiei-Frame287(6).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d44ce8c00087bf5e2/8lepkpiei-Frame287(6).svg" + }, + { + key: "668b9d3d44ce8c00087bf5e1/8lepkps4o-icon7(6).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d44ce8c00087bf5e1/8lepkps4o-icon7(6).svg" + }, + { + key: "668b9d3d44ce8c00087bf5e0/8lepkrdvh-Frame287(7).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d44ce8c00087bf5e0/8lepkrdvh-Frame287(7).svg" + }, + { + key: "668b9d3d89b4a600083b1ab3/8ldn2n509-Frame287.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1ab3/8ldn2n509-Frame287.svg" + }, + { + key: "668b9d3d89b4a600083b1ab4/8ldn1x7zl-icon7(5).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1ab4/8ldn1x7zl-icon7(5).svg" + }, + { + key: "668b9d3d89b4a600083b1ab5/8ldmzzynz-Group896.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1ab5/8ldmzzynz-Group896.svg" + }, + { + key: "668b9d3d89b4a600083b1ab6/8ldmzw2fb-8lbgdgcuh-63129206-0-home-hero-bg-dc2fa67.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1ab6/8ldmzw2fb-8lbgdgcuh-63129206-0-home-hero-bg-dc2fa67.svg" + }, + { + key: "668b9d3d44ce8c00087bf5df/8lfjw7veq-03-Serverless.png", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d44ce8c00087bf5df/8lfjw7veq-03-Serverless.png" + }, + { + key: "668b9d3d89b4a600083b1ab2/8ldn6bygx-icon8.svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1ab2/8ldn6bygx-icon8.svg" + }, + { + key: "668b9d3d89b4a600083b1ab1/8ldn6d87m-Frame287(1).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1ab1/8ldn6d87m-Frame287(1).svg" + }, + { + key: "668b9d3d89b4a600083b1ab0/8ldn6k0ua-icon7(6).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1ab0/8ldn6k0ua-icon7(6).svg" + }, + { + key: "668b9d3d89b4a600083b1aaf/8ldn6lxdn-Frame287(3).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aaf/8ldn6lxdn-Frame287(3).svg" + }, + { + key: "668b9d3d89b4a600083b1aae/8ldn7asit-Frame287(4).svg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668b9d3d89b4a600083b1aae/8ldn7asit-Frame287(4).svg" + }, + { + key: "668ba1c0f599020008a75624/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1c0f599020008a75624/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba1c1f599020008a75628/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1c1f599020008a75628/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba1c3f599020008a7562c/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1c3f599020008a7562c/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba1c4f599020008a75630/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1c4f599020008a75630/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba1c9f599020008a75634/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1c9f599020008a75634/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba1bff599020008a75620/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1bff599020008a75620/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba1bef599020008a7561c/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1bef599020008a7561c/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668ba1bdf599020008a75618/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1bdf599020008a75618/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba1bcf599020008a75614/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1bcf599020008a75614/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba1bcf599020008a75610/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1bcf599020008a75610/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba1b4f599020008a755fc/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1b4f599020008a755fc/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba1b5f599020008a75600/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1b5f599020008a75600/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba1b6f599020008a75604/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1b6f599020008a75604/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba1b7f599020008a75608/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1b7f599020008a75608/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba1baf599020008a7560c/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1baf599020008a7560c/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba1b3f599020008a755f8/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1b3f599020008a755f8/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba1b1f599020008a755f4/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1b1f599020008a755f4/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba1aff599020008a755f0/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1aff599020008a755f0/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba1aef599020008a755ec/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1aef599020008a755ec/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba1adf599020008a755e8/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1adf599020008a755e8/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba1a4f599020008a755d4/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a4f599020008a755d4/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba1a5f599020008a755d8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a5f599020008a755d8/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba1a8f599020008a755dc/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a8f599020008a755dc/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba1a9f599020008a755e0/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a9f599020008a755e0/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba1aaf599020008a755e4/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1aaf599020008a755e4/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba1a2f599020008a755d0/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a2f599020008a755d0/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba1a2f599020008a755cc/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a2f599020008a755cc/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + }, + { + key: "668ba1a1f599020008a755c8/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a1f599020008a755c8/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba1a0f599020008a755c4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a0f599020008a755c4/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba1a0f599020008a755c0/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba1a0f599020008a755c0/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba19af599020008a755ac/maria-lupan-45mHOwW6AqY-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba19af599020008a755ac/maria-lupan-45mHOwW6AqY-unsplash.jpg" + }, + { + key: "668ba19af599020008a755b0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba19af599020008a755b0/andrea-tapia-GZ6hTsWkPBM-unsplash.jpg" + }, + { + key: "668ba19bf599020008a755b4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba19bf599020008a755b4/jennie-razumnaya-FNOsfYdhzeQ-unsplash.jpg" + }, + { + key: "668ba19ef599020008a755b8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba19ef599020008a755b8/salah-ait-mokhtar-pW6O__wg_GQ-unsplash.jpg" + }, + { + key: "668ba19ff599020008a755bc/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba19ff599020008a755bc/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba196f599020008a755a8/mediamodifier-ZA1l0CfRqqU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba196f599020008a755a8/mediamodifier-ZA1l0CfRqqU-unsplash.jpg" + }, + { + key: "668ba195f599020008a755a4/shanthi-raja-GU9cEfg0dvk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba195f599020008a755a4/shanthi-raja-GU9cEfg0dvk-unsplash.jpg" + }, + { + key: "668ba193f599020008a755a0/asep-rendi-IaYFX0QITgk-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba193f599020008a755a0/asep-rendi-IaYFX0QITgk-unsplash.jpg" + }, + { + key: "668ba192f599020008a7559c/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba192f599020008a7559c/luca-laurence-ZrqrP9Xs2vI-unsplash.jpg" + }, + { + key: "668ba192f599020008a75598/2h-media-ShGClLlvQbA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba192f599020008a75598/2h-media-ShGClLlvQbA-unsplash.jpg" + }, + { + key: "668ba18df599020008a75584/mediamodifier-m6Hw3FybWPA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba18df599020008a75584/mediamodifier-m6Hw3FybWPA-unsplash.jpg" + }, + { + key: "668ba18ef599020008a75588/mockup-free-DNMwRtoOz5g-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba18ef599020008a75588/mockup-free-DNMwRtoOz5g-unsplash.jpg" + }, + { + key: "668ba18ff599020008a7558c/allison-saeng-xnANlVZMViA-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba18ff599020008a7558c/allison-saeng-xnANlVZMViA-unsplash.jpg" + }, + { + key: "668ba190f599020008a75590/pmv-chamara-KLU0scqbKQ0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba190f599020008a75590/pmv-chamara-KLU0scqbKQ0-unsplash.jpg" + }, + { + key: "668ba191f599020008a75594/cardmapr-nl-NFCou1VhdjE-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba191f599020008a75594/cardmapr-nl-NFCou1VhdjE-unsplash.jpg" + }, + { + key: "668ba18af599020008a75580/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba18af599020008a75580/andrew-dunstan-vmtoLazDg_Y-unsplash.jpg" + }, + { + key: "668ba18af599020008a7557c/matt-wojtas--baMCm2CLKM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba18af599020008a7557c/matt-wojtas--baMCm2CLKM-unsplash.jpg" + }, + { + key: "668ba189f599020008a75578/marnie-rochester-kAwAIDqdih8-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba189f599020008a75578/marnie-rochester-kAwAIDqdih8-unsplash.jpg" + }, + { + key: "668ba188f599020008a75574/allison-saeng-1ikODAZ_MOs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba188f599020008a75574/allison-saeng-1ikODAZ_MOs-unsplash.jpg" + }, + { + key: "668ba186f599020008a75570/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba186f599020008a75570/kelly-sikkema-Yie2C8Un_Oc-unsplash.jpg" + }, + { + key: "668ba17ff599020008a7555c/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba17ff599020008a7555c/tran-mau-tri-tam-3xFwO_wTrkg-unsplash.jpg" + }, + { + key: "668ba181f599020008a75560/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba181f599020008a75560/charlesdeluvio-cZr2sgaxy3Q-unsplash.jpg" + }, + { + key: "668ba182f599020008a75564/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba182f599020008a75564/kelly-sikkema-4JxV3Gs42Ks-unsplash.jpg" + }, + { + key: "668ba183f599020008a75568/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba183f599020008a75568/kaizen-nguy-n-8Js2kEeiirs-unsplash.jpg" + }, + { + key: "668ba184f599020008a7556c/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba184f599020008a7556c/kevin-bhagat-ms-QnzmKGVM-unsplash.jpg" + }, + { + key: "668ba17ef599020008a75558/keagan-henman-XYtuOYfIg_M-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba17ef599020008a75558/keagan-henman-XYtuOYfIg_M-unsplash.jpg" + }, + { + key: "668ba17cf599020008a75554/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba17cf599020008a75554/kelly-sikkema-ia1p6fqftnQ-unsplash.jpg" + }, + { + key: "668ba178f599020008a75550/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba178f599020008a75550/sergey-kotenev-GE_K6RgKBfU-unsplash.jpg" + }, + { + key: "668ba177f599020008a7554c/taru-goyal-fwhcnlBEw7s-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba177f599020008a7554c/taru-goyal-fwhcnlBEw7s-unsplash.jpg" + }, + { + key: "668ba175f599020008a75548/d-l-samuels-TGis_XXj7UM-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba175f599020008a75548/d-l-samuels-TGis_XXj7UM-unsplash.jpg" + }, + { + key: "668ba172f599020008a75534/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba172f599020008a75534/jennie-razumnaya-Qjew_Tnmcgs-unsplash.jpg" + }, + { + key: "668ba173f599020008a75538/point-normal-GxJ2vyVZZGI-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba173f599020008a75538/point-normal-GxJ2vyVZZGI-unsplash.jpg" + }, + { + key: "668ba173f599020008a7553c/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba173f599020008a7553c/cardmapr-nl-9JJ8Zu9vPak-unsplash.jpg" + }, + { + key: "668ba174f599020008a75540/christian-werther-W2FAELrIaxc-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba174f599020008a75540/christian-werther-W2FAELrIaxc-unsplash.jpg" + }, + { + key: "668ba175f599020008a75544/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg", + url: "https://d1zqvydzhnfn89.cloudfront.net/files/668ba175f599020008a75544/jakub-zerdzicki-MUDaGFpimN0-unsplash.jpg" + } +]; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createCmsAssetsZipper.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createCmsAssetsZipper.ts new file mode 100644 index 00000000000..300ec2eb0c3 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createCmsAssetsZipper.ts @@ -0,0 +1,112 @@ +import { + CreateMultipartUploadCommand, + createS3Client, + GetObjectCommand, + S3Client, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import { createPassThrough } from "~tests/mocks/createPassThrough"; +import type { PassThrough } from "stream"; +import { mockClient } from "aws-sdk-client-mock"; +import { CmsAssetsZipper } from "~/tasks/utils/cmsAssetsZipper"; +import { Upload } from "~/tasks/utils/upload"; +import { Zipper } from "~/tasks/utils/zipper"; +import { createArchiver } from "~/tasks/utils/archiver"; +import type { ICmsEntryFetcher } from "~/tasks/utils/cmsEntryFetcher"; +import type { IEntryAssets, IEntryAssetsResolver } from "~/tasks/utils/entryAssets"; +import type { IFileFetcher } from "~/tasks/utils/fileFetcher"; +import { createFileFetcher } from "~tests/mocks/createFileFetcher"; +import { createEntryAssetsResolver } from "~tests/mocks/createEntryAssetsResolver"; +import { WEBINY_EXPORT_ASSETS_EXTENSION } from "~/tasks/constants"; + +interface ICreateCmsAssetsZipperParams { + entryFetcher?: ICmsEntryFetcher; + createEntryAssets: () => IEntryAssets; + createEntryAssetsResolver?: () => IEntryAssetsResolver; + fileFetcher?: IFileFetcher; + region?: string; + filename?: string; + stream?: PassThrough; + bucket?: string; +} + +export const createCmsAssetsZipper = (params: ICreateCmsAssetsZipperParams) => { + const stream = params.stream || createPassThrough(); + + const mockedClient = mockClient(S3Client); + mockedClient.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + mockedClient.on(GetObjectCommand).resolves({}); + mockedClient.on(UploadPartCommand).resolves({ ETag: "1" }); + + const region = params.region || "eu-central-1"; + const bucket = params.bucket || "my-test-bucket"; + const filename = params.filename || `test.${WEBINY_EXPORT_ASSETS_EXTENSION}`; + + const client = createS3Client({ + region + }); + + const upload = new Upload({ + client, + bucket, + stream, + filename + }); + + let buffers: Buffer[] | undefined = undefined; + + stream.on("data", chunk => { + if (!buffers) { + buffers = []; + } + buffers.push(chunk); + }); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true + } + }); + + const zipper = new Zipper({ + upload, + archiver + }); + + const cmsAssetsZipper = new CmsAssetsZipper({ + fileFetcher: createFileFetcher(), + entryFetcher: async () => { + return { + items: [], + meta: { + totalCount: 0, + cursor: null, + hasMoreItems: false + } + }; + }, + createEntryAssetsResolver: () => { + return createEntryAssetsResolver(); + }, + zipper, + ...params + }); + + return { + s3Url: `https://${bucket}.s3.${region}.amazonaws.com/${filename}`, + region, + bucket, + filename, + getBuffer: () => { + if (!buffers) { + throw new Error("No buffers found. Please write some data to the stream first."); + } + return Buffer.concat(buffers); + }, + upload, + archiver, + zipper, + cmsAssetsZipper + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createCmsEntryZipper.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createCmsEntryZipper.ts new file mode 100644 index 00000000000..0206b3f6dc4 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createCmsEntryZipper.ts @@ -0,0 +1,97 @@ +import { + CreateMultipartUploadCommand, + createS3Client, + GetObjectCommand, + S3Client, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import { createPassThrough } from "~tests/mocks/createPassThrough"; +import { PassThrough } from "stream"; +import { mockClient } from "aws-sdk-client-mock"; +import { CmsEntryZipper } from "~/tasks/utils/cmsEntryZipper"; +import { Upload } from "~/tasks/utils/upload"; +import { Zipper } from "~/tasks/utils/zipper"; +import { createArchiver } from "~/tasks/utils/archiver"; +import type { ICmsEntryFetcher } from "~/tasks/utils/cmsEntryFetcher"; +import type { IAsset, IEntryAssets } from "~/tasks/utils/entryAssets"; +import type { IUniqueResolver } from "~/tasks/utils/uniqueResolver/abstractions/UniqueResolver"; +import { WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; + +interface ICreateCmsEntryZipperParams { + fetcher: ICmsEntryFetcher; + region?: string; + filename?: string; + stream?: PassThrough; + bucket?: string; + entryAssets: IEntryAssets; + uniqueAssetsResolver: IUniqueResolver; +} + +export const createCmsEntryZipper = (params: ICreateCmsEntryZipperParams) => { + const stream = params.stream || createPassThrough(); + + const mockedClient = mockClient(S3Client); + mockedClient.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + mockedClient.on(GetObjectCommand).resolves({}); + mockedClient.on(UploadPartCommand).resolves({ ETag: "1" }); + + const region = params.region || "eu-central-1"; + const bucket = params.bucket || "my-test-bucket"; + const filename = params.filename || `test.${WEBINY_EXPORT_ENTRIES_EXTENSION}`; + + const client = createS3Client({ + region + }); + + const upload = new Upload({ + client, + bucket, + stream, + filename + }); + + let buffers: Buffer[] | undefined = undefined; + + stream.on("data", chunk => { + if (!buffers) { + buffers = []; + } + buffers.push(chunk); + }); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true + } + }); + + const zipper = new Zipper({ + upload, + archiver + }); + + const cmsEntryZipper = new CmsEntryZipper({ + zipper, + fetcher: params.fetcher, + entryAssets: params.entryAssets, + uniqueAssetsResolver: params.uniqueAssetsResolver + }); + + return { + s3Url: `https://${bucket}.s3.${region}.amazonaws.com/${filename}`, + region, + bucket, + filename, + getBuffer: () => { + if (!buffers) { + throw new Error("No buffers found. Please write some data to the stream first."); + } + return Buffer.concat(buffers); + }, + upload, + archiver, + zipper, + cmsEntryZipper + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createEntryAssets.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createEntryAssets.ts new file mode 100644 index 00000000000..d7a2e1befc3 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createEntryAssets.ts @@ -0,0 +1,19 @@ +import type { IContentEntryTraverser } from "@webiny/api-headless-cms"; +import type { IEntryAssets, IEntryAssetsParams } from "~/tasks/utils/entryAssets"; +import { EntryAssets } from "~/tasks/utils/entryAssets"; +import { createUniqueResolver } from "~tests/mocks/createUniqueResolver"; + +const defaultTraverser: IContentEntryTraverser = { + async traverse() { + return; + } +}; + +type Params = IEntryAssetsParams; + +export const createEntryAssets = (params?: Params): IEntryAssets => { + return new EntryAssets({ + traverser: params?.traverser || defaultTraverser, + uniqueResolver: createUniqueResolver() + }); +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createEntryAssetsResolver.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createEntryAssetsResolver.ts new file mode 100644 index 00000000000..d3d2bcf7bb6 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createEntryAssetsResolver.ts @@ -0,0 +1,18 @@ +import type { IEntryAssetsResolverParams } from "~/tasks/utils/entryAssets"; +import { EntryAssetsResolver } from "~/tasks/utils/entryAssets"; + +export const createEntryAssetsResolver = (params?: IEntryAssetsResolverParams) => { + return new EntryAssetsResolver({ + fetchFiles: async () => { + return { + items: [], + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 0 + } + }; + }, + ...params + }); +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createFileFetcher.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createFileFetcher.ts new file mode 100644 index 00000000000..4156222a615 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createFileFetcher.ts @@ -0,0 +1,38 @@ +import { + IFileFetcher, + IFileFetcherListCallable, + IFileFetcherStreamCallable +} from "~/tasks/utils/fileFetcher"; + +export interface ICreateFileFetcherParams { + list?: IFileFetcherListCallable; + fetch?: IFileFetcherStreamCallable; +} + +export const createFileFetcher = (params?: ICreateFileFetcherParams): IFileFetcher => { + return { + list: async () => { + return []; + }, + stream: async () => { + return null; + }, + delete: async () => { + return {} as any; + }, + exists: async () => { + return false; + }, + read: async () => { + return null; + }, + // @ts-expect-error + fetch: async () => { + return null; + }, + head: async () => { + return null; + }, + ...params + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createPassThrough.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createPassThrough.ts new file mode 100644 index 00000000000..c9061c26193 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createPassThrough.ts @@ -0,0 +1,7 @@ +import { PassThrough } from "stream"; + +export const createPassThrough = (): PassThrough => { + return new PassThrough({ + autoDestroy: true + }); +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createUniqueResolver.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createUniqueResolver.ts new file mode 100644 index 00000000000..b08d16f0982 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createUniqueResolver.ts @@ -0,0 +1,6 @@ +import type { GenericRecord } from "@webiny/api/types"; +import { UniqueResolver } from "~/tasks/utils/uniqueResolver/UniqueResolver"; + +export const createUniqueResolver = () => { + return new UniqueResolver(); +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createUpload.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createUpload.ts new file mode 100644 index 00000000000..c00427bacc4 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createUpload.ts @@ -0,0 +1,31 @@ +import type { IAwsUpload, IUpload, IUploadDoneResult } from "~/tasks/utils/upload"; +import type { Options as BaseUploadOptions } from "@webiny/aws-sdk/lib-storage"; +import type { PassThrough } from "stream"; + +export interface ICreateUploadParams { + stream: PassThrough; + filename: string; + factory?(params: BaseUploadOptions): IAwsUpload; + queueSize?: number; +} + +export interface IExtendedUpload extends IUpload { + filename: string; +} + +export const createUpload = (params: ICreateUploadParams): IExtendedUpload => { + return { + ...params, + client: {} as any, + upload: {} as any, + async done() { + return {} as IUploadDoneResult; + }, + async abort() { + return; + }, + onProgress() { + return; + } + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createUrlSigner.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createUrlSigner.ts new file mode 100644 index 00000000000..d2ae08ea732 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createUrlSigner.ts @@ -0,0 +1,24 @@ +import type { IUrlSigner } from "~/tasks/utils/urlSigner"; + +export const createUrlSigner = (): IUrlSigner => { + return { + async get(params) { + const timeout = params.timeout || 604800; // 1 week default + return { + url: `signed://${params.key}`, + bucket: "bucket", + key: params.key, + expiresOn: new Date(new Date().getTime() + timeout) + }; + }, + async head(params) { + const timeout = params.timeout || 604800; // 1 week default + return { + url: `signed://${params.key}`, + bucket: "bucket", + key: params.key, + expiresOn: new Date(new Date().getTime() + timeout) + }; + } + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/createZipper.ts b/packages/api-headless-cms-import-export/__tests__/mocks/createZipper.ts new file mode 100644 index 00000000000..199108c01ed --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/createZipper.ts @@ -0,0 +1,56 @@ +import { Zipper } from "~/tasks/utils/zipper"; +import { Upload } from "~/tasks/utils/upload"; +import { createPassThrough } from "~tests/mocks/createPassThrough"; +import { + CreateMultipartUploadCommand, + createS3Client, + GetObjectCommand, + S3Client, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import { mockClient } from "aws-sdk-client-mock"; +import type { PassThrough } from "stream"; +import { createArchiver } from "~/tasks/utils/archiver"; +import { WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; + +interface ICreateZipperParams { + region?: string; + filename?: string; + stream?: PassThrough; + bucket?: string; +} + +export const createZipper = (params: ICreateZipperParams = {}) => { + const stream = params.stream || createPassThrough(); + + const mockedClient = mockClient(S3Client); + mockedClient.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + mockedClient.on(GetObjectCommand).resolves({}); + mockedClient.on(UploadPartCommand).resolves({ ETag: "1" }); + + const region = params.region || "eu-central-1"; + const bucket = params.bucket || "my-test-bucket"; + const filename = params.filename || `test.${WEBINY_EXPORT_ENTRIES_EXTENSION}`; + const client = createS3Client({ + region + }); + + const upload = new Upload({ + client, + bucket, + stream, + filename + }); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true + } + }); + + return new Zipper({ + upload, + archiver + }); +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/fetch.ts b/packages/api-headless-cms-import-export/__tests__/mocks/fetch.ts new file mode 100644 index 00000000000..0bbcd57ae8c --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/fetch.ts @@ -0,0 +1,67 @@ +export interface IResponse { + toJSON?(): Promise; + toText?(): Promise; + toBlob?(): Promise; + toArrayBuffer?(): Promise; + stream: ReadableStream; +} + +export interface ICreateMockFetchParams { + (url: string): Promise | IResponse; +} + +export const createMockFetch = (response: ICreateMockFetchParams): typeof fetch => { + return async url => { + if (typeof url !== "string") { + throw new Error("Expected a string as the first argument."); + } + const headers = new Headers(); + + const res = await response(url); + return { + url, + headers, + body: { + getReader() { + return new ReadableStreamDefaultReader(res.stream); + } + } as ReadableStream, + type: "default", + redirected: false, + status: 200, + ok: true, + statusText: "OK", + json: async () => { + if (res.toJSON) { + return res.toJSON(); + } + return {}; + }, + text: async () => { + if (res.toText) { + return res.toText(); + } + return ""; + }, + bodyUsed: false, + blob: async () => { + if (res.toBlob) { + return res.toBlob(); + } + return new Blob(); + }, + clone: () => { + return structuredClone(this) as any; + }, + formData: async () => { + return new FormData(); + }, + arrayBuffer: async () => { + if (res.toArrayBuffer) { + return res.toArrayBuffer(); + } + return new ArrayBuffer(0); + } + }; + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/images.ts b/packages/api-headless-cms-import-export/__tests__/mocks/images.ts new file mode 100644 index 00000000000..725fa1ef56f --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/images.ts @@ -0,0 +1,120 @@ +import type { GenericRecord } from "@webiny/api/types"; +import { generateAlphaNumericId } from "@webiny/utils"; + +interface IImageData { + id: string; + url: string; + size: number; + aliases: string[]; + key: string; + name: string; + type: string; + extensions: GenericRecord; + location: { + folderId: string; + }; + meta: GenericRecord; + tags: string[]; +} + +interface IImageConfig { + url: string; + size?: number; + aliases?: string[]; + tags?: string[]; +} + +class ImageData { + public readonly data: IImageData; + + public get id(): string { + return this.data.id; + } + + public get url(): string { + return this.data.url; + } + + public get size(): number { + return this.data.size; + } + + public get aliases(): string[] { + return this.data.aliases; + } + + public get key(): string { + return this.data.key; + } + + public get name(): string { + return this.data.name; + } + + public get type(): string { + return this.data.type; + } + + public get extensions(): GenericRecord { + return this.data.extensions; + } + + public get location(): { folderId: string } { + return this.data.location; + } + + public get meta(): GenericRecord { + return this.data.meta; + } + + public get tags(): string[] { + return this.data.tags; + } + + public constructor(config: IImageConfig) { + const url = new URL(config.url); + const { pathname } = url; + this.data = { + id: generateAlphaNumericId(), + url: config.url, + size: config.size || 100, + aliases: config.aliases || [], + key: pathname, + type: "image/jpeg", + tags: config.tags || [], + name: pathname.split("/").pop() as string, + extensions: {}, + location: { + folderId: "root" + }, + meta: {} + }; + } +} + +export const createImages = (): ImageData[] => { + return [ + new ImageData({ + url: "https://aCloundfrontDistributionId.cloudfront.net/files/fileId1234/image-1-in-its-own-directory.jpg", + aliases: ["alias-image-1-in-its-own-directory.jpg"] + }), + new ImageData({ + url: "https://aCloundfrontDistributionId.cloudfront.net/files/fileId2345/image-2-in-its-own-directory.jpg" + }), + new ImageData({ + url: "https://aCloundfrontDistributionId.cloudfront.net/files/image-3-no-directory.jpg", + aliases: ["alias-image-3-no-directory.jpg"] + }), + new ImageData({ + url: "https://aCloundfrontDistributionId.cloudfront.net/files/fileId4567/image-4-in-its-own-directory.jpg", + aliases: ["alias-image-4-in-its-own-directory.jpg"] + }), + new ImageData({ + url: "https://aCloundfrontDistributionId.cloudfront.net/files/image-5-no-directory.jpg" + }), + new ImageData({ + url: "https://aCloundfrontDistributionId.cloudfront.net/files/fileId6789/image-6-in-its-own-directory.jpg", + aliases: ["alias-image-6-in-its-own-directory.jpg"] + }) + ]; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/mockMultipartUpload.ts b/packages/api-headless-cms-import-export/__tests__/mocks/mockMultipartUpload.ts new file mode 100644 index 00000000000..2e007515c2a --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/mockMultipartUpload.ts @@ -0,0 +1,38 @@ +import { + IMultipartUploadHandler, + IMultipartUploadHandlerAbortResult, + IMultipartUploadHandlerAddResult, + IMultipartUploadHandlerCompleteResult, + IMultipartUploadHandlerGetBufferResult, + ITag +} from "~/tasks/utils/upload"; +import { NonEmptyArray } from "@webiny/api/types"; + +export const createMockMultipartUpload = async ( + params?: Partial +): Promise => { + return { + getUploadId(): string { + return "mockUploadId"; + }, + async add(): Promise { + return {} as any; + }, + async complete(): Promise { + return {} as any; + }, + async abort(): Promise { + return {} as any; + }, + getBuffer(): IMultipartUploadHandlerGetBufferResult { + return {} as any; + }, + getNextPart(): number { + return 1; + }, + getTags(): NonEmptyArray { + return ["tag1"]; + }, + ...params + }; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/model.ts b/packages/api-headless-cms-import-export/__tests__/mocks/model.ts new file mode 100644 index 00000000000..f9b17990538 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/mocks/model.ts @@ -0,0 +1,106 @@ +import { createCmsModelPlugin } from "@webiny/api-headless-cms"; + +export const AUTHOR_MODEL_ID = "author"; + +export const getModel = (id: string) => { + if (id === AUTHOR_MODEL_ID) { + return model; + } + throw new Error(`Cannot get model "${id}"!`); +}; + +export const model = { + createdOn: new Date().toISOString(), + savedOn: new Date().toISOString(), + locale: "en-US", + titleFieldId: "fullName", + lockedFields: [], + name: "Author", + description: "Author", + modelId: AUTHOR_MODEL_ID, + singularApiName: "AuthorApiModel", + pluralApiName: "AuthorsApiModel", + group: { + id: "test", + name: "test" + }, + layout: [["fullName", "image", "images", "wrapper", "wrappers"]], + fields: [ + { + id: "fullName", + multipleValues: false, + label: "Full name", + type: "text", + fieldId: "fullName" + }, + { + id: "image", + multipleValues: false, + label: "Image", + fieldId: "image", + type: "file" + }, + { + id: "images", + multipleValues: true, + label: "Image", + fieldId: "images", + type: "file" + }, + { + id: "wrapper", + multipleValues: false, + label: "Wrapper", + type: "object", + fieldId: "wrapper", + settings: { + fields: [ + { + id: "image", + multipleValues: false, + label: "Image", + fieldId: "image", + type: "file" + }, + { + id: "images", + multipleValues: true, + label: "Images", + fieldId: "images", + type: "file" + } + ] + } + }, + { + id: "wrappers", + multipleValues: true, + label: "Wrappers", + type: "object", + fieldId: "wrappers", + settings: { + fields: [ + { + id: "image", + multipleValues: false, + label: "Image", + fieldId: "image", + type: "file" + }, + { + id: "images", + multipleValues: true, + label: "Images", + fieldId: "images", + type: "file" + } + ] + } + } + ], + tenant: "root" +}; + +export const createModelPlugin = () => { + return createCmsModelPlugin(model); +}; diff --git a/packages/api-headless-cms-import-export/__tests__/mocks/testing.we.zip b/packages/api-headless-cms-import-export/__tests__/mocks/testing.we.zip new file mode 100644 index 00000000000..b742f3118a6 Binary files /dev/null and b/packages/api-headless-cms-import-export/__tests__/mocks/testing.we.zip differ diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/exportContentEntries.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/exportContentEntries.test.ts new file mode 100644 index 00000000000..1fccf1e8958 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/exportContentEntries.test.ts @@ -0,0 +1,92 @@ +import { createRunner } from "@webiny/project-utils/testing/tasks"; +import { useHandler } from "~tests/helpers/useHandler"; +import { createExportContentEntriesTask } from "~/tasks"; +import type { ITaskRunParams } from "@webiny/tasks"; + +jest.mock("~/tasks/domain/createExportContentEntries", () => { + return { + createExportContentEntries: () => { + return { + run: async ({ input }: ITaskRunParams) => { + if (input.kill) { + throw new Error("An error happened!"); + } + return { + executed: true + }; + } + }; + } + }; +}); + +describe("export content entries task", () => { + it("should run the task and return a done response", async () => { + const { createContext } = useHandler(); + const context = await createContext(); + + const definition = createExportContentEntriesTask(); + + const task = await context.tasks.createTask({ + name: "Create mock export content entries task", + definitionId: definition.id, + input: {} + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id + }); + expect(result).toEqual({ + locale: "en-US", + message: undefined, + status: "done", + tenant: "root", + webinyTaskDefinitionId: "exportContentEntries", + webinyTaskId: task.id + }); + }); + + it("should run the task and return an error", async () => { + const { createContext } = useHandler(); + const context = await createContext(); + + const definition = createExportContentEntriesTask(); + + const task = await context.tasks.createTask({ + name: "Create mock export content entries task", + definitionId: definition.id, + input: { + kill: true + } + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id + }); + expect(result).toEqual({ + error: { + data: { + input: { + kill: true + } + }, + message: "An error happened!" + }, + locale: "en-US", + status: "error", + tenant: "root", + webinyTaskDefinitionId: "exportContentEntries", + webinyTaskId: task.id + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/importFromUrlContentEntries/importFromUrlContentEntriesCombined.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/importFromUrlContentEntries/importFromUrlContentEntriesCombined.test.ts new file mode 100644 index 00000000000..7ee9bdca888 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/importFromUrlContentEntries/importFromUrlContentEntriesCombined.test.ts @@ -0,0 +1,98 @@ +import { createDownloadFileFromUrl } from "~/tasks/domain/downloadFileFromUrl/index"; +import { + CompleteMultipartUploadCommand, + CreateMultipartUploadCommand, + GetObjectCommand, + S3Client, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import { mockClient } from "aws-sdk-client-mock"; +import { ICmsImportExportValidatedContentEntriesFile } from "~/types"; +import { createMockFetch } from "~tests/mocks/fetch"; +import fs from "fs"; +import path from "path"; +import { createMockMultipartUpload } from "~tests/mocks/mockMultipartUpload"; + +describe("import from url content entries combined", () => { + beforeEach(async () => { + process.env.S3_BUCKET = "a-mock-s3-bucket"; + }); + + it("should construct class properly", async () => { + const file: Pick = { + get: "https://webiny.com/asdgkdhsbg3iu2bfd/file-1.we.zip", + size: 12345 + }; + + const { pathname: filename } = new URL(file.get); + + const combined = createDownloadFileFromUrl({ + fetch: createMockFetch(async () => { + return { + stream: new ReadableStream() + }; + }), + file: { + url: file.get, + size: file.size, + key: filename + }, + upload: await createMockMultipartUpload() + }); + + expect(combined.isDone()).toBe(false); + expect(combined.getNextRange()).toEqual(0); + }); + + it("should start processing and finish properly", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + mockedClient.on(CompleteMultipartUploadCommand).resolves({}); + mockedClient.on(GetObjectCommand).resolves({}); + mockedClient.on(UploadPartCommand).resolves({ ETag: "1" }); + + const filename = "testing.we.zip"; + const file: Pick = { + get: path.resolve(__dirname, `../../mocks/${filename}`), + size: 4642 + }; + + const combined = createDownloadFileFromUrl({ + fetch: createMockFetch(async url => { + const file = fs.readFileSync(url); + return { + toArrayBuffer: async () => { + return file.buffer; + }, + toJSON: async () => { + return file.toJSON(); + }, + toText: async () => { + return file.toString(); + }, + stream: new ReadableStream({ + pull: controller => { + controller.enqueue(file); + controller.close(); + } + }) + }; + }), + file: { + url: file.get, + size: file.size, + key: filename + }, + upload: await createMockMultipartUpload() + }); + + const result = await combined.process(async () => { + return; + }); + + expect(result).toEqual("done"); + + expect(combined.isDone()).toBe(true); + expect(combined.getNextRange()).toEqual(1); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/importFromUrlController.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/importFromUrlController.test.ts new file mode 100644 index 00000000000..e6c94127c3c --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/importFromUrlController.test.ts @@ -0,0 +1,295 @@ +import { createImportFromUrlControllerTask } from "~/tasks"; +import { createRunner } from "@webiny/project-utils/testing/tasks"; +import { CmsImportExportFileType, Context, ICmsImportExportValidatedFile } from "~/types"; +import { useHandler } from "~tests/helpers/useHandler"; +import { + ResponseDoneResult, + ResponseErrorResult, + TaskDataStatus, + TaskResponseStatus +} from "@webiny/tasks"; +import { categoryModel } from "~tests/helpers/models"; +import { NonEmptyArray } from "@webiny/api/types"; + +describe("import from url controller", () => { + let context: Context; + + beforeEach(async () => { + const { createContext } = useHandler(); + context = await createContext(); + }); + + it("should run the task and fail because of missing model", async () => { + const definition = createImportFromUrlControllerTask(); + + const task = await context.tasks.createTask({ + definitionId: definition.id, + input: {}, + name: "Import from URL Controller" + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id, + ...task + }); + + expect(result).toBeInstanceOf(ResponseErrorResult); + expect(result).toEqual({ + error: { + code: "MISSING_MODEL_ID", + message: `Missing "modelId" in the input.`, + data: { + input: {} + } + }, + status: TaskResponseStatus.ERROR, + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id + }); + }); + + it("should run the task and fail because of missing files", async () => { + const definition = createImportFromUrlControllerTask(); + + const task = await context.tasks.createTask({ + definitionId: definition.id, + input: { + modelId: categoryModel.modelId + }, + name: "Import from URL Controller" + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id, + ...task + }); + + expect(result).toBeInstanceOf(ResponseErrorResult); + expect(result).toEqual({ + error: { + code: "NO_FILES_FOUND", + message: `No files found in the provided data.`, + data: { + input: { + modelId: categoryModel.modelId + } + } + }, + status: TaskResponseStatus.ERROR, + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id + }); + }); + + it("should run the task and fail because of non-existing model", async () => { + const definition = createImportFromUrlControllerTask(); + + const modelId = "nonExistingModelId"; + + const files: NonEmptyArray = [ + { + get: "https://some-url.com/file-1.we.zip", + head: "https://some-url.com/file-1.we.zip", + size: 1000, + error: undefined, + type: CmsImportExportFileType.ENTRIES, + checksum: "checksum", + checked: true, + key: "file-1.we.zip" + } + ]; + + const task = await context.tasks.createTask({ + definitionId: definition.id, + input: { + modelId, + files + }, + name: "Import from URL Controller" + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id, + ...task + }); + + expect(result).toBeInstanceOf(ResponseErrorResult); + expect(result).toEqual({ + error: { + code: "MODEL_NOT_FOUND", + message: `Model "${modelId}" not found.`, + data: { + input: { + modelId, + files + } + } + }, + status: TaskResponseStatus.ERROR, + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id + }); + }); + + it("should run the task, trigger child tasks and return a continue response", async () => { + expect.assertions(3); + const definition = createImportFromUrlControllerTask(); + + const files: NonEmptyArray = [ + { + get: "https://some-url.com/file-1.we.zip", + head: "https://some-url.com/file-1.we.zip", + size: 1000, + error: undefined, + type: CmsImportExportFileType.ENTRIES, + checksum: "checksum", + checked: true, + key: "file-1.we.zip" + }, + { + get: "https://some-url.com/file-2.wa.zip", + head: "https://some-url.com/file-2.wa.zip", + size: 1250, + error: undefined, + type: CmsImportExportFileType.ASSETS, + checksum: "checksum", + checked: true, + key: "file-2.wa.zip" + }, + { + get: "https://some-url.com/file-3.unknown.zip", + head: "https://some-url.com/file-3.unknown.zip", + size: 2000, + error: undefined, + type: "unknown" as CmsImportExportFileType.ENTRIES, + checksum: "checksum", + checked: true, + key: "something-unknown.zip" + } + ]; + + const task = await context.tasks.createTask({ + definitionId: definition.id, + input: { + modelId: categoryModel.modelId, + files + }, + name: "Import from URL Controller" + }); + + console.warn = jest.fn(); + + const runner = createRunner({ + context, + task: definition, + onContinue: async ({ taskId, iteration, result }) => { + if (iteration === 1) { + return; + } + const children = await context.tasks.listTasks({ + where: { + parentId: taskId + }, + limit: 1000000 + }); + for (const child of children.items) { + await context.tasks.updateTask(child.id, { + taskStatus: TaskDataStatus.SUCCESS + }); + } + /** + * This is a strange expect, but we can do it as we know that it will happen due to the + * continue result on the first iteration of the runner. + */ + // assertion #1 + expect(result).toMatchObject({ + status: TaskResponseStatus.CONTINUE, + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id, + input: { + modelId: categoryModel.modelId, + files: files.map(file => { + const output = { + ...file + }; + delete output.error; + return output; + }), + steps: { + download: { + triggered: true + } + } + }, + message: undefined, + delay: -1 + }); + } + }); + + const result = await runner({ + webinyTaskId: task.id, + tenant: "root", + locale: "en-US" + }); + + expect(result).toEqual({ + message: undefined, + output: { + aborted: [], + done: [], + failed: [], + files: [], + invalid: [] + }, + locale: "en-US", + status: "done", + tenant: "root", + webinyTaskDefinitionId: "importFromUrlController", + webinyTaskId: expect.any(String) + }); + + // assertion #2 + expect(result).toEqual({ + status: TaskResponseStatus.DONE, + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id, + output: { + aborted: [], + done: [], + failed: [], + invalid: [], + files: [] + }, + message: undefined + }); + + // assertion #3 + expect(result).toBeInstanceOf(ResponseDoneResult); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/archiver.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/archiver.test.ts new file mode 100644 index 00000000000..068cca5f67a --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/archiver.test.ts @@ -0,0 +1,17 @@ +// @ts-expect-error +import BaseArchiver from "archiver/lib/core"; +import { createArchiver } from "~/tasks/utils/archiver"; + +describe("archiver", () => { + it("should properly create an instance of Archiver", async () => { + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true + } + }); + + expect(archiver.archiver).not.toBeNull(); + expect(archiver.archiver).toBeInstanceOf(BaseArchiver); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsAssetsZipper.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsAssetsZipper.test.ts new file mode 100644 index 00000000000..bd2d02c123f --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsAssetsZipper.test.ts @@ -0,0 +1,146 @@ +import { createCmsAssetsZipper } from "~tests/mocks/createCmsAssetsZipper"; +import { createEntryAssets } from "~tests/mocks/createEntryAssets"; +import { useHandler } from "~tests/helpers/useHandler"; +import { AUTHOR_MODEL_ID } from "~tests/mocks/model"; +import type { Context } from "~/types"; +import type { ICmsAssetsZipperExecuteParams } from "~/tasks/utils/cmsAssetsZipper"; +import { + CmsAssetsZipperExecuteContinueWithoutResult, + CmsAssetsZipperExecuteDoneWithoutResult +} from "~/tasks/utils/cmsAssetsZipper"; +import type { CmsEntryMeta } from "@webiny/api-headless-cms/types"; +import type { ICmsEntryFetcherResult } from "~/tasks/utils/cmsEntryFetcher"; +import type { IContentEntryTraverser } from "@webiny/api-headless-cms"; +import type { IUniqueResolver } from "~/tasks/utils/uniqueResolver/abstractions/UniqueResolver"; +import type { IAsset } from "~/tasks/utils/entryAssets"; +import { createUniqueResolver } from "~tests/mocks/createUniqueResolver"; + +const defaultZipperExecuteParams: ICmsAssetsZipperExecuteParams = { + isCloseToTimeout() { + return false; + }, + isAborted() { + return false; + }, + fileAfter: undefined, + entryAfter: undefined +}; + +describe("cms assets zipper", () => { + let context: Context; + let traverser: IContentEntryTraverser; + let uniqueResolver: IUniqueResolver; + beforeEach(async () => { + const { createContext } = useHandler(); + context = await createContext(); + traverser = await context.cms.getEntryTraverser(AUTHOR_MODEL_ID); + uniqueResolver = createUniqueResolver(); + }); + + it("should throw abort error because task was aborted", async () => { + expect.assertions(1); + + const { cmsAssetsZipper } = createCmsAssetsZipper({ + createEntryAssets: () => { + return createEntryAssets({ + traverser, + uniqueResolver + }); + } + }); + + try { + await cmsAssetsZipper.execute({ + ...defaultZipperExecuteParams, + isAborted() { + return true; + } + }); + } catch (ex) { + expect(ex.message).toBe("Upload aborted."); + } + }); + + it("should return done without result because no items were found", async () => { + expect.assertions(1); + + const { cmsAssetsZipper } = createCmsAssetsZipper({ + createEntryAssets: () => { + return createEntryAssets({ + traverser, + uniqueResolver + }); + } + }); + + const result = await cmsAssetsZipper.execute(defaultZipperExecuteParams); + expect(result).toBeInstanceOf(CmsAssetsZipperExecuteDoneWithoutResult); + }); + + it("should return continue without result because no assets were found and timeout is close", async () => { + expect.assertions(1); + + let isCloseToTimeout = false; + + const { cmsAssetsZipper } = createCmsAssetsZipper({ + entryFetcher: async after => { + const meta: CmsEntryMeta = { + hasMoreItems: false, + totalCount: 2, + cursor: "1" + }; + if (after === "1") { + return { + items: [ + { + id: "2" + } + ], + meta: { + ...meta, + hasMoreItems: true, + cursor: "2" + } + } as ICmsEntryFetcherResult; + } else if (after === "2") { + return { + items: [], + meta: { + ...meta, + cursor: "2" + } + } as ICmsEntryFetcherResult; + } + return { + items: [ + { + id: "1" + } + ], + meta: { + ...meta, + hasMoreItems: true + } + } as ICmsEntryFetcherResult; + }, + createEntryAssets: () => { + return createEntryAssets({ + traverser, + uniqueResolver + }); + } + }); + + const result = await cmsAssetsZipper.execute({ + ...defaultZipperExecuteParams, + isCloseToTimeout() { + const result = isCloseToTimeout; + if (!result) { + isCloseToTimeout = true; + } + return result; + } + }); + expect(result).toBeInstanceOf(CmsAssetsZipperExecuteContinueWithoutResult); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsAssetsZipper/pointerStore.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsAssetsZipper/pointerStore.test.ts new file mode 100644 index 00000000000..47f6bab2492 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsAssetsZipper/pointerStore.test.ts @@ -0,0 +1,141 @@ +import { PointerStore } from "~/tasks/utils/cmsAssetsZipper/PointerStore"; + +describe("pointer store", () => { + it("should have defaults on class init", () => { + const store = new PointerStore({ + entryMeta: { + cursor: undefined + }, + fileCursor: undefined + }); + + expect(store.getEntryTotalItems()).toBe(0); + expect(store.getEntryHasMoreItems()).toBe(true); + expect(store.getEntryCursor()).toBe(undefined); + expect(store.getFileCursor()).toBe(undefined); + expect(store.getIsStoredFiles()).toBe(false); + expect(store.getTaskIsAborted()).toBe(false); + }); + + it("should have passed constructor params set", () => { + const store = new PointerStore({ + entryMeta: { + cursor: "1234567890" + }, + fileCursor: "0987654321" + }); + expect(store.getEntryTotalItems()).toBe(0); + expect(store.getEntryHasMoreItems()).toBe(true); + expect(store.getEntryCursor()).toBe("1234567890"); + expect(store.getFileCursor()).toBe("0987654321"); + expect(store.getIsStoredFiles()).toBe(false); + expect(store.getTaskIsAborted()).toBe(false); + }); + + it("should set entry meta", () => { + const store = new PointerStore({ + entryMeta: { + cursor: undefined + } + }); + + store.setEntryMeta({ + cursor: "0987654321", + hasMoreItems: true, + totalCount: 100 + }); + + expect(store.getEntryTotalItems()).toBe(100); + expect(store.getEntryHasMoreItems()).toBe(true); + expect(store.getEntryCursor()).toBe("0987654321"); + expect(store.getFileCursor()).toBe(undefined); + expect(store.getIsStoredFiles()).toBe(false); + expect(store.getTaskIsAborted()).toBe(false); + }); + + it("should set and reset file cursor", () => { + const store = new PointerStore({ + entryMeta: { + cursor: undefined + } + }); + + store.setFileCursor("0987654321"); + + expect(store.getEntryTotalItems()).toBe(0); + expect(store.getEntryHasMoreItems()).toBe(true); + expect(store.getEntryCursor()).toBe(undefined); + expect(store.getFileCursor()).toBe("0987654321"); + expect(store.getIsStoredFiles()).toBe(false); + expect(store.getTaskIsAborted()).toBe(false); + + store.resetFileCursor(); + + expect(store.getEntryTotalItems()).toBe(0); + expect(store.getEntryHasMoreItems()).toBe(true); + expect(store.getEntryCursor()).toBe(undefined); + expect(store.getFileCursor()).toBe(undefined); + expect(store.getIsStoredFiles()).toBe(false); + expect(store.getTaskIsAborted()).toBe(false); + }); + + it("should set is stored files", () => { + const store = new PointerStore({ + entryMeta: { + cursor: undefined + } + }); + + store.setIsStoredFiles(); + expect(store.getEntryTotalItems()).toBe(0); + expect(store.getEntryHasMoreItems()).toBe(true); + expect(store.getEntryCursor()).toBe(undefined); + expect(store.getFileCursor()).toBe(undefined); + expect(store.getIsStoredFiles()).toBe(true); + expect(store.getTaskIsAborted()).toBe(false); + }); + + it("should throw an error on multiple setIsStoredFiles calls", () => { + const store = new PointerStore({ + entryMeta: { + cursor: undefined + } + }); + store.setIsStoredFiles(); + expect(store.getIsStoredFiles()).toBe(true); + + expect(() => { + store.setIsStoredFiles(); + }).toThrow(`The "setIsStoredFiles" method should be called only once.`); + }); + + it("should set is task aborted", () => { + const store = new PointerStore({ + entryMeta: { + cursor: undefined + } + }); + + store.setTaskIsAborted(); + expect(store.getEntryTotalItems()).toBe(0); + expect(store.getEntryHasMoreItems()).toBe(true); + expect(store.getEntryCursor()).toBe(undefined); + expect(store.getFileCursor()).toBe(undefined); + expect(store.getIsStoredFiles()).toBe(false); + expect(store.getTaskIsAborted()).toBe(true); + }); + + it("should throw an error on multiple setTaskIsAborted calls", () => { + const store = new PointerStore({ + entryMeta: { + cursor: undefined + } + }); + store.setTaskIsAborted(); + expect(store.getTaskIsAborted()).toBe(true); + + expect(() => { + store.setTaskIsAborted(); + }).toThrow(`The "setTaskIsAborted" method should be called only once.`); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsEntryZipper.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsEntryZipper.test.ts new file mode 100644 index 00000000000..b9f7487f44c --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/cmsEntryZipper.test.ts @@ -0,0 +1,216 @@ +import AdmZip from "adm-zip"; +import { createCmsEntryZipper } from "~tests/mocks/createCmsEntryZipper"; +import { fetchItems, images } from "./mocks/cmsEntryZipperItems"; +import { createModelPlugin } from "~tests/mocks/model"; +import type { CmsModel } from "@webiny/api-headless-cms/types"; +import { createCmsEntryFetcher } from "~/tasks/utils/cmsEntryFetcher"; +import { createUniqueResolver } from "~tests/mocks/createUniqueResolver"; +import { createEntryAssets } from "~tests/mocks/createEntryAssets"; +import { MANIFEST_JSON } from "~/tasks/constants"; + +describe("cms entry zipper", () => { + const model = createModelPlugin().contentModel as CmsModel; + + it("should abort upload because of no items", async () => { + expect.assertions(2); + + const { cmsEntryZipper } = createCmsEntryZipper({ + fetcher: async () => { + return { + items: [], + meta: { + totalCount: 0, + cursor: null, + hasMoreItems: false + } + }; + }, + uniqueAssetsResolver: createUniqueResolver(), + entryAssets: createEntryAssets() + }); + + expect(cmsEntryZipper.execute).toBeFunction(); + + try { + const result = await cmsEntryZipper.execute({ + isCloseToTimeout() { + return false; + }, + isAborted() { + return false; + }, + model, + after: undefined, + exportAssets: false + }); + + expect(result).toEqual("should not happen"); + } catch (ex) { + expect(ex.message).toEqual("Upload aborted."); + } + }); + + it("should zip entries into a file - no assets", async () => { + const { cmsEntryZipper, getBuffer } = createCmsEntryZipper({ + fetcher: createCmsEntryFetcher(async after => { + return fetchItems(after); + }), + uniqueAssetsResolver: createUniqueResolver(), + entryAssets: createEntryAssets() + }); + + await cmsEntryZipper.execute({ + isCloseToTimeout() { + return false; + }, + isAborted() { + return false; + }, + model, + after: undefined, + exportAssets: false + }); + + const buffer = getBuffer(); + + expect(buffer).toBeInstanceOf(Buffer); + + const zipped = buffer!.toString("utf-8"); + + expect(zipped).toMatch("entries-1.json"); + expect(zipped).toMatch("entries-2.json"); + + const zip = new AdmZip(buffer); + + const zipEntries = zip.getEntries(); + expect(zipEntries).toHaveLength(5); + + expect(zipEntries[0].entryName).toEqual("entries-1.json"); + expect(zipEntries[1].entryName).toEqual("entries-2.json"); + expect(zipEntries[2].entryName).toEqual("entries-3.json"); + expect(zipEntries[3].entryName).toEqual("entries-4.json"); + expect(zipEntries[4].entryName).toEqual(MANIFEST_JSON); + + const entries1Json = zip.readAsText(zipEntries[0]); + + expect(entries1Json).toEqual( + JSON.stringify({ + items: [ + { + id: "1", + image: images[1].url + }, + { + id: "2", + image: images[2].url + } + ], + meta: { + totalCount: 8, + cursor: "2#0001", + hasMoreItems: true + } + }) + ); + + const entries2Json = zip.readAsText(zipEntries[1]); + + expect(entries2Json).toEqual( + JSON.stringify({ + items: [ + { + id: "3", + image: images[3].url + }, + { + id: "4", + image: images[4].url + } + ], + meta: { + totalCount: 8, + cursor: "4#0001", + hasMoreItems: true + }, + after: "2#0001" + }) + ); + + const entries3Json = zip.readAsText(zipEntries[2]); + + expect(entries3Json).toEqual( + JSON.stringify({ + items: [ + { + id: "5", + image: images[5].url + }, + { + id: "6", + image: images[6].url + } + ], + meta: { + totalCount: 8, + cursor: "6#0001", + hasMoreItems: true + }, + after: "4#0001" + }) + ); + + const entries4Json = zip.readAsText(zipEntries[3]); + + expect(entries4Json).toEqual( + JSON.stringify({ + items: [ + { + id: "7", + image: images[7].url + }, + { + id: "8", + image: images[8].url + } + ], + meta: { + totalCount: 8, + cursor: "8#0001", + hasMoreItems: false + }, + after: "6#0001" + }) + ); + + const filesJson = JSON.parse(zip.readAsText(zipEntries[4])); + expect(filesJson).toEqual({ + files: [ + { + id: 1, + name: "entries-1.json" + }, + { + id: 2, + name: "entries-2.json", + after: "2#0001" + }, + { + id: 3, + name: "entries-3.json", + after: "4#0001" + }, + { + id: 4, + name: "entries-4.json", + after: "6#0001" + } + ], + assets: [], + model: expect.objectContaining({ + group: model.group.id, + fields: model.fields, + modelId: model.modelId + }) + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/entryAssets.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/entryAssets.test.ts new file mode 100644 index 00000000000..82dcc6fe293 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/entryAssets.test.ts @@ -0,0 +1,229 @@ +import { useHandler } from "~tests/helpers/useHandler"; +import type { CmsEntry } from "@webiny/api-headless-cms/types"; +import type { IContentEntryTraverser } from "@webiny/api-headless-cms"; +import type { IAsset, IEntryAssets } from "~/tasks/utils/entryAssets"; +import { EntryAssets } from "~/tasks/utils/entryAssets"; +import { createUniqueResolver } from "~tests/mocks/createUniqueResolver"; + +const cloudfrontUrl = "https://aCloundfrontDistributionId.cloudfront.net"; + +const createImagePath = (file: string) => { + return `/files/${file}`; +}; + +const createImageUrl = (file: string) => { + return `${cloudfrontUrl}${createImagePath(file)}`; +}; + +describe("entry assets", () => { + let entryAssets: IEntryAssets; + let traverser: IContentEntryTraverser; + + beforeEach(async () => { + const { createContext } = useHandler(); + const context = await createContext(); + traverser = await context.cms.getEntryTraverser("author"); + entryAssets = new EntryAssets({ + traverser, + uniqueResolver: createUniqueResolver() + }); + }); + + it("should properly extract assets", async () => { + expect(entryAssets).toBeInstanceOf(EntryAssets); + + const image1 = `fileId1234/image-1-in-its-own-directory.jpg`; + const image2 = `fileId2345/image-2-in-its-own-directory.jpg`; + const image3 = `image-3-no-directory.jpg`; + const image4 = `fileId4567/image-4-in-its-own-directory.jpg`; + const image5 = `image-5-no-directory.jpg`; + const image6 = `fileId6789/image-6-in-its-own-directory.jpg`; + + const entry: Pick = { + values: { + fullName: "John Doe", + image: createImageUrl(image1), + images: [createImageUrl(image1), createImageUrl(image2), createImageUrl(image3)], + wrapper: { + image: createImageUrl(image4), + images: [ + createImageUrl(image3), + createImageUrl(image5), + createImageUrl(image6) + ], + anotherWrapper: { + image: createImageUrl(image4), + images: [ + createImageUrl(image1), + createImageUrl(image2), + createImageUrl(image3), + createImageUrl(image4), + createImageUrl(image5), + createImageUrl(image6) + ] + } + }, + wrappers: [ + { + image: createImageUrl(image4), + images: [ + createImageUrl(image3), + createImageUrl(image5), + createImageUrl(image6) + ] + }, + { + image: createImageUrl(image4), + images: [ + createImageUrl(image1), + createImageUrl(image2), + createImageUrl(image3), + createImageUrl(image4), + createImageUrl(image5), + createImageUrl(image6) + ] + } + ] + } + }; + + const result = await entryAssets.assignAssets(entry); + + expect(result).toHaveLength(6); + + const expected: IAsset[] = [ + { + key: image1, + url: createImageUrl(image1) + }, + { + key: image2, + url: createImageUrl(image2) + }, + { + key: image3, + url: createImageUrl(image3) + }, + { + key: image4, + url: createImageUrl(image4) + }, + { + key: image5, + url: createImageUrl(image5) + }, + { + key: image6, + url: createImageUrl(image6) + } + ]; + expect(result).toEqual(expected); + }); + + it("should properly extract asset from complex path", async () => { + const cloudfrontUrl = "https://odisadosadnsakl.cloudfront.aws"; + const filePath = "files"; + const fileKey = + "demo-pages/6022814891bd1300087bd24c/welcome-to-webiny__webiny-infrastructure-overview!.svg"; + const image = `${cloudfrontUrl}/${filePath}/${fileKey}`; + + const entry: Pick = { + values: { + fullName: "John Doe", + image + } + }; + + const result = await entryAssets.assignAssets(entry); + + const expected: IAsset[] = [ + { + key: fileKey, + url: image + } + ]; + + expect(result).toEqual(expected); + }); + + it("should properly extract asset alias from a path", async () => { + const cloudfrontUrl = "https://odisadosadnsakl.cloudfront.aws"; + const fileKey = "/demo-pages/welcome-to-webiny__webiny-infrastructure-overview!.svg"; + const image = `${cloudfrontUrl}${fileKey}`; + + const entry: Pick = { + values: { + fullName: "John Doe", + image + } + }; + + const result = await entryAssets.assignAssets(entry); + + const expected: IAsset[] = [ + { + alias: fileKey, + url: image + } + ]; + expect(result).toEqual(expected); + }); + + it("should not find any assets", async () => { + const result = await entryAssets.assignAssets([]); + + expect(result).toHaveLength(0); + + const emptyResult = await entryAssets.assignAssets({ + values: { + image: "", + images: {} + } + }); + + expect(emptyResult).toHaveLength(0); + + const emptyResult2 = await entryAssets.assignAssets({ + values: { + image: " ", + images: [" ", null, undefined] + } + }); + + expect(emptyResult2).toHaveLength(0); + + const emptyResult3 = await entryAssets.assignAssets({ + values: { + image: undefined, + images: undefined + } + }); + + expect(emptyResult3).toHaveLength(0); + + const emptyResult4 = await entryAssets.assignAssets({ + values: { + image: " bla bla bla" + } + }); + + expect(emptyResult4).toHaveLength(0); + + const emptyResult5 = await entryAssets.assignAssets({ + values: { + image: null, + images: null + } + }); + + expect(emptyResult5).toHaveLength(0); + + const emptyResult6 = await entryAssets.assignAssets(undefined as any); + + expect(emptyResult6).toHaveLength(0); + + const emptyResult7 = await entryAssets.assignAssets(null as any); + + expect(emptyResult7).toHaveLength(0); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/entryAssetsResolver.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/entryAssetsResolver.test.ts new file mode 100644 index 00000000000..70ab57103d3 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/entryAssetsResolver.test.ts @@ -0,0 +1,76 @@ +import { useHandler } from "~tests/helpers/useHandler"; +import type { Context } from "~/types"; +import { createImages } from "~tests/mocks/images"; +import type { IAssets, IEntryAssetsResolver } from "~/tasks/utils/entryAssets"; +import { EntryAssetsResolver } from "~/tasks/utils/entryAssets"; + +describe("entry assets resolver", () => { + let context: Context; + let entryAssetsResolver: IEntryAssetsResolver; + + beforeEach(async () => { + const { createContext } = useHandler(); + context = await createContext(); + + entryAssetsResolver = new EntryAssetsResolver({ + fetchFiles: async opts => { + const [items, meta] = await context.fileManager.listFiles(opts); + return { + items, + meta + }; + } + }); + }); + + it("should fetch assets - empty list", async () => { + const result = await entryAssetsResolver.resolve([]); + + expect(result).toEqual([]); + }); + + it("should fetch assets", async () => { + const images = createImages(); + + expect.assertions(images.length + 1); + + for (const image of images) { + try { + await context.fileManager.createFile(image.data); + } catch (ex) { + console.log(ex); + expect(ex.message).toEqual("Must not happen!"); + } + } + + const assets = images.reduce((items, item) => { + if (item.aliases.length > 0) { + items[item.url] = { + url: item.url, + alias: item.aliases[0] + }; + return items; + } + items[item.url] = { + url: item.url, + key: item.key + }; + return items; + }, {}); + + const results = await entryAssetsResolver.resolve(Object.values(assets)); + + expect(results.length).toEqual(images.length); + + for (const image of images) { + const result = results.find(asset => { + if (asset.key === image.key) { + return true; + } + const aliases = asset.aliases as string[]; + return aliases.some(a => image.aliases.includes(a)); + }); + expect(result).not.toBeUndefined(); + } + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/externalFileFetcher.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/externalFileFetcher.test.ts new file mode 100644 index 00000000000..401ead2c749 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/externalFileFetcher.test.ts @@ -0,0 +1,247 @@ +import { ExternalFileFetcher } from "~/tasks/utils/externalFileFetcher"; +import type { GenericRecord } from "@webiny/api/types"; + +const createFetcherResponse = (params: Partial): Response => { + return { + url: "", + body: null, + bodyUsed: false, + headers: new Headers(), + type: "basic", + ok: true, + status: 200, + statusText: "OK", + clone: jest.fn(), + redirected: false, + arrayBuffer: jest.fn(), + blob: jest.fn(), + formData: jest.fn(), + json: jest.fn(), + text: jest.fn(), + ...params + }; +}; + +describe("external file fetcher", () => { + it("should fail to fetch a file via GET", async () => { + expect.assertions(2); + const results: GenericRecord = { + url: null, + options: null + }; + const fetcher = new ExternalFileFetcher({ + fetcher: async (url, options) => { + results.url = url; + results.options = options; + throw new Error("Fetch error!"); + }, + getChecksumHeader: () => { + return "mocked-checksum"; + } + }); + + const url = "https://localhost/file.zip"; + try { + const result = await fetcher.fetch(url); + expect(result).toMatchObject({ + error: { + code: "GET_FETCH_ERROR", + data: { + url + }, + message: "Fetch error!" + } + }); + } catch (ex) { + expect(ex.message).toBe("Should not happen!"); + } + expect(results).toEqual({ + url, + options: { + method: "GET" + } + }); + }); + + it("should fail to fetch a file info via HEAD", async () => { + expect.assertions(3); + const results: GenericRecord = { + url: null, + options: null + }; + const fetcher = new ExternalFileFetcher({ + fetcher: async (url, options) => { + results.url = url; + results.options = options; + throw new Error("Fetch error!"); + }, + getChecksumHeader: () => { + return "mocked-checksum"; + } + }); + + const url = "https://localhost/file.zip"; + try { + const result = await fetcher.head(url); + expect(result).toMatchObject({ + error: { + code: "HEAD_FETCH_ERROR", + data: { + url + }, + message: "Fetch error!" + } + }); + } catch (ex) { + expect(ex.message).toBe("Should not happen!"); + } + expect(results.url).toEqual(url); + expect(results.options.method).toEqual("HEAD"); + }); + + it("should fetch a file via GET but fail due to missing headers", async () => { + const headers = new Headers(); + const fetcher = new ExternalFileFetcher({ + fetcher: async url => { + return createFetcherResponse({ + url: url.toString(), + headers + }); + }, + getChecksumHeader: () => { + return headers.get("etag") || "mocked-checksum"; + } + }); + + const url = "https://localhost/file.zip"; + const missingContentTypeResult = await fetcher.fetch(url); + expect(missingContentTypeResult).toMatchObject({ + error: { + code: "GET_FETCH_ERROR", + data: { + url + }, + message: `Content type not found for URL: ${url}` + } + }); + + headers.append("content-type", "application/zip"); + + const missingContentLengthResult = await fetcher.fetch(url); + expect(missingContentLengthResult).toMatchObject({ + error: { + code: "GET_FETCH_ERROR", + data: { + url + }, + message: `Content length not found for URL: ${url}` + } + }); + }); + + it("should fetch a file via GET but fail due to missing body", async () => { + const headers = new Headers({ + "content-type": "application/zip", + "content-length": "100" + }); + const fetcher = new ExternalFileFetcher({ + fetcher: async url => { + return createFetcherResponse({ + url: url.toString(), + headers + }); + }, + getChecksumHeader: () => { + return headers.get("etag") || "mocked-checksum"; + } + }); + const url = "https://localhost/file.zip"; + + const result = await fetcher.fetch(url); + + expect(result).toEqual({ + error: { + code: "GET_FETCH_ERROR", + data: { + url + }, + message: `Body not found for URL: ${url}`, + stack: expect.any(String) + } + }); + }); + + it("should fetch a file via GET and succeed", async () => { + const headers = new Headers({ + "content-type": "application/zip", + "content-length": "100" + }); + const fetcher = new ExternalFileFetcher({ + fetcher: async url => { + return createFetcherResponse({ + url: url.toString(), + headers, + body: new ReadableStream() + }); + }, + getChecksumHeader: () => { + return headers.get("etag") || "mocked-checksum"; + } + }); + const url = "https://localhost/file.zip"; + + const result = await fetcher.fetch(url); + + expect(result).toEqual({ + file: { + name: "file.zip", + size: 100, + url, + contentType: "application/zip", + body: expect.any(Object), + checksum: "mocked-checksum" + } + }); + }); + + it("should fetch a file via HEAD but fail due to missing headers and then succeed", async () => { + const headers = new Headers(); + const fetcher = new ExternalFileFetcher({ + fetcher: async url => { + return createFetcherResponse({ + url: url.toString(), + headers + }); + }, + getChecksumHeader: () => { + return headers.get("etag") || "mocked-checksum"; + } + }); + + const url = "https://localhost/file.zip"; + const missingContentTypeResult = await fetcher.head(url); + expect(missingContentTypeResult).toMatchObject({ + error: { + code: "HEAD_FETCH_ERROR", + data: { + url + }, + message: `Content type not found for URL: ${url}` + } + }); + + headers.append("content-type", "application/zip"); + + const result = await fetcher.head(url); + + expect(result).toEqual({ + file: { + name: "file.zip", + size: 0, + url, + contentType: "application/zip", + checksum: "mocked-checksum" + } + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/fileFetcher.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/fileFetcher.test.ts new file mode 100644 index 00000000000..d9ed34b3393 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/fileFetcher.test.ts @@ -0,0 +1,95 @@ +import { FileFetcher } from "~/tasks/utils/fileFetcher"; +import { GetObjectCommand, ListObjectsCommand, S3Client } from "@webiny/aws-sdk/client-s3"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { mockClient } from "aws-sdk-client-mock"; +import { basename } from "path"; + +const fileTxt = { + Key: "prefix-key/file.txt", + Size: 100 +}; +const fileZip = { + Key: "prefix-key/file.zip", + Size: 200 +}; +const fileTar = { + Key: "prefix-key/file.tar", + Size: 300 +}; +const fileNoKey = { + Key: "", + Size: 400 +}; +const files = [fileTxt, fileZip, fileTar, fileNoKey].sort((a, b) => { + return a.Key.localeCompare(b.Key); +}); + +describe("file fetcher", () => { + it("should not fetch any files - empty response", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(ListObjectsCommand).resolves({}); + mockedClient.on(GetObjectCommand).resolves({}); + + const fileFetcher = new FileFetcher({ + client: createS3Client(), + bucket: "a-bucket" + }); + + const result = await fileFetcher.list("prefix-key/"); + + expect(result).toEqual([]); + + const file = await fileFetcher.stream("prefix-key/file.txt"); + expect(file).toBeNull(); + }); + + it("should list files", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(ListObjectsCommand).resolves({ + Contents: files + }); + mockedClient.on(GetObjectCommand).resolves({ + Body: "mock" as any + }); + + const fileFetcher = new FileFetcher({ + client: createS3Client(), + bucket: "a-bucket" + }); + + const result = await fileFetcher.list("prefix-key/"); + + expect(result).toEqual( + files + .filter(file => !!file.Key) + .map(file => { + return { + name: basename(file.Key), + key: file.Key, + size: file.Size + }; + }) + ); + + const file = await fileFetcher.stream("prefix-key/file.txt"); + expect(file).not.toBeNull(); + expect(file).toEqual("mock"); + }); + + it("should return empty results due to errors while fetching files", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(ListObjectsCommand).rejects(new Error("ListObjectsCommand error")); + mockedClient.on(GetObjectCommand).rejects(new Error("GetObjectCommand error")); + + const fileFetcher = new FileFetcher({ + client: createS3Client(), + bucket: "a-bucket" + }); + + const result = await fileFetcher.list("prefix-key/"); + expect(result).toEqual([]); + + const file = await fileFetcher.stream("prefix-key/file.txt"); + expect(file).toBeNull(); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getBackOffSeconds.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getBackOffSeconds.test.ts new file mode 100644 index 00000000000..5aa98da0cf4 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getBackOffSeconds.test.ts @@ -0,0 +1,28 @@ +import { getBackOffSeconds, min, max } from "~/tasks/utils/helpers/getBackOffSeconds"; + +describe("get back off seconds", () => { + const values: [number, number][] = [ + [0, min], + [1, min], + [2, 20], + [3, 30], + [4, 40], + [5, 50], + [6, 60], + [7, 70], + [8, 80], + [9, max], + [10, max], + [11, max], + [12, max], + [13, max], + [14, max], + [15, max] + ]; + + it.each(values)("should return %s0 seconds - #%s", (iterations, expected) => { + const result = getBackOffSeconds(iterations); + + expect(result).toBe(expected); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getBucket.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getBucket.test.ts new file mode 100644 index 00000000000..401dfc1633c --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getBucket.test.ts @@ -0,0 +1,27 @@ +import { getBucket } from "~/tasks/utils/helpers/getBucket"; + +describe("get bucket", () => { + it("should get the bucket", async () => { + process.env.S3_BUCKET = "a-test-bucket"; + const bucket = getBucket(); + + expect(bucket).toEqual("a-test-bucket"); + }); + + it("should throw an error because the bucket is not set", async () => { + delete process.env.S3_BUCKET; + expect.assertions(2); + + try { + getBucket(); + } catch (ex) { + expect(ex.message).toEqual("Missing S3_BUCKET environment variable."); + } + process.env.S3_BUCKET = ""; + try { + getBucket(); + } catch (ex) { + expect(ex.message).toEqual("Missing S3_BUCKET environment variable."); + } + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getImportExportFileType.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getImportExportFileType.test.ts new file mode 100644 index 00000000000..162450ad87a --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/getImportExportFileType.test.ts @@ -0,0 +1,44 @@ +import { getImportExportFileType } from "~/tasks/utils/helpers/getImportExportFileType"; +import { CmsImportExportFileType } from "~/types"; + +describe("getImportExportFileType", () => { + it("should properly detect entries file type", () => { + const url = + "https://wby-fm-bucket-ba055b8.s3.eu-central-1.amazonaws.com/cms-export/author/66991d2f367e7500082f95728lyrbu562/aCustomFileNameForTheFile.we.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=%%2Feu-eu-1%2Fs3%2Faws4_request&X-Amz-Date=&X-Amz-Expires=&X-Amz-Security-Token=&X-Amz-SignedHeaders=host&x-id=GetObject"; + + const result = getImportExportFileType(url); + const { pathname } = new URL(url); + + expect(result).toEqual({ + type: CmsImportExportFileType.ENTRIES, + pathname + }); + }); + + it("should properly detect assets file type", () => { + const url = + "https://wby-fm-bucket-ba055b8.s3.eu-central-1.amazonaws.com/cms-export/author/66991d2f367e7500082f95728lyrbu562/aCustomFileName..wa.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=%%2Feu-eu-1%2Fs3%2Faws4_request&X-Amz-Date=&X-Amz-Expires=&X-Amz-Security-Token=&X-Amz-SignedHeaders=host&x-id=GetObject"; + + const result = getImportExportFileType(url); + const { pathname } = new URL(url); + + expect(result).toEqual({ + type: CmsImportExportFileType.ASSETS, + pathname + }); + }); + + it("should fail to detect a file type", () => { + const url = + "https://wby-fm-bucket-ba055b8.s3.eu-central-1.amazonaws.com/cms-export/author/66991d2f367e7500082f95728lyrbu562/aCustomFileNameForTheFile.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=%%2Feu-eu-1%2Fs3%2Faws4_request&X-Amz-Date=&X-Amz-Expires=&X-Amz-Security-Token=&X-Amz-SignedHeaders=host&x-id=GetObject"; + + const result = getImportExportFileType(url); + const { pathname } = new URL(url); + + expect(result).toEqual({ + type: "zip", + pathname, + error: true + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/matchKeyOrAlias.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/matchKeyOrAlias.test.ts new file mode 100644 index 00000000000..8987d44311e --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/helpers/matchKeyOrAlias.test.ts @@ -0,0 +1,89 @@ +import { matchKeyOrAlias } from "~/tasks/utils/helpers/matchKeyOrAlias"; +import { GenericRecord } from "@webiny/api/types"; + +const cloudfrontUrl = "https://d1zqvydzhnfn89.cloudfront.net"; + +describe("match key or alias", () => { + it("should log an error when given an invalid URL", () => { + const errors: any[] = []; + console.error = (...args: any[]) => { + errors.push(...args); + }; + + const url = "invalid-url"; + + const result = matchKeyOrAlias(url); + + expect(result).toBeNull(); + + expect(errors).toHaveLength(2); + expect(errors[0]).toEqual(`Url "${url}" is not valid.`); + expect(errors[1].code).toEqual("ERR_INVALID_URL"); + expect(errors[1].input).toEqual(url); + }); + + it("should properly match a public file", () => { + const keys: GenericRecord = { + "files/a-key/next.jpg": "a-key/next.jpg", + "files/private/a-key/next.jpg": "private/a-key/next.jpg", + "files/666bfc2abacd2d0008acbfbf/a-image.jpeg": "666bfc2abacd2d0008acbfbf/a-image.jpeg" + }; + + const errors: any[] = []; + console.error = (...args: any[]) => { + errors.push(...args); + }; + + expect.assertions(Object.keys(keys).length + 1); + + for (const key in keys) { + const expected = keys[key]; + const result = matchKeyOrAlias(`${cloudfrontUrl}/${key}`); + + expect(result).toEqual({ + key: expected + }); + } + expect(errors).toHaveLength(0); + }); + + it("should properly match a private", () => { + const keys: GenericRecord = { + "private/a-key/next.jpg": "a-key/next.jpg", + "private/files/a-key/next.jpg": "files/a-key/next.jpg", + "private/666bfc2abacd2d0008acbfbf/a-image.jpeg": "666bfc2abacd2d0008acbfbf/a-image.jpeg" + }; + + const errors: any[] = []; + console.error = (...args: any[]) => { + errors.push(...args); + }; + + expect.assertions(Object.keys(keys).length + 1); + + for (const key in keys) { + const expected = keys[key]; + const result = matchKeyOrAlias(`${cloudfrontUrl}/${key}`); + + expect(result).toEqual({ + key: expected + }); + } + expect(errors).toHaveLength(0); + }); + + it("should properly match alias", () => { + const keys: GenericRecord = { + "/a-key/next.jpg": "/a-key/next.jpg" + }; + + for (const alias in keys) { + const value = keys[alias]; + const result = matchKeyOrAlias(`${cloudfrontUrl}${value}`); + + expect(result).toEqual({ + alias + }); + } + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/mocks/cmsEntryZipperItems.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/mocks/cmsEntryZipperItems.ts new file mode 100644 index 00000000000..cafaf3763fd --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/mocks/cmsEntryZipperItems.ts @@ -0,0 +1,129 @@ +import type { CmsEntry } from "@webiny/api-headless-cms/types"; +import type { GenericRecord } from "@webiny/api/types"; +import type { ICmsEntryFetcherResult } from "~/tasks/utils/cmsEntryFetcher"; + +const cloudfrontUrl = "https://aCloundfrontDistributionId.cloudfront.net"; + +export interface IImage { + id: string; + url: string; + key: string; + alias?: string; +} + +export const images: GenericRecord = { + 1: { + id: "1", + url: `${cloudfrontUrl}/files/1.jpg`, + key: "files/1.jpg" + }, + 2: { + id: "2", + url: `${cloudfrontUrl}/possibly-an-alias/2.jpg`, + key: "files/2.jpg", + alias: "possibly-an-alias/2.jpg" + }, + 3: { + id: "3", + url: `${cloudfrontUrl}/files/3.jpg`, + key: "files/3.jpg" + }, + 4: { + id: "4", + url: `${cloudfrontUrl}/files/4.jpg`, + key: "files/4.jpg" + }, + 5: { + id: "5", + url: `${cloudfrontUrl}/files/5.jpg`, + key: "files/5.jpg" + }, + 6: { + id: "6", + url: `${cloudfrontUrl}/is-alias/6.jpg`, + key: "files/6.jpg", + alias: "is-alias/6.jpg" + }, + 7: { + id: "7", + url: `${cloudfrontUrl}/files/7.jpg`, + key: "files/7.jpg" + }, + 8: { + id: "8", + url: `${cloudfrontUrl}/files/8.jpg`, + key: "files/8.jpg" + } +}; + +interface IFindImageParamsKey { + key: string; + alias?: never; +} + +interface IFindImageParamsAlias { + key?: never; + alias: string; +} + +type IFindImageParams = IFindImageParamsKey | IFindImageParamsAlias; + +export const findImage = (params: IFindImageParams) => { + return Object.values(images).find(image => { + if (params.key) { + return image.url.endsWith(params.key); + } else if (params.alias) { + return image.url.endsWith(params.alias); + } + return false; + }); +}; + +export const createEntries = () => { + return Object.values(images).map(image => { + return { + id: `${image.id}#0001`, + entryId: image.id, + values: { + image: image.url + } + } as unknown as CmsEntry; + }); +}; + +export const fetchItems = (after?: string | null): ICmsEntryFetcherResult => { + const entries = createEntries(); + + if (!after) { + const items = entries.slice(0, 2); + return { + items, + meta: { + totalCount: createEntries().length, + cursor: items[items.length - 1]?.id || null, + hasMoreItems: true + } + } as ICmsEntryFetcherResult; + } + + const index = entries.findIndex(item => item.id === after); + if (index > -1) { + const items = entries.slice(index + 1, index + 3); + return { + items, + meta: { + totalCount: createEntries().length, + cursor: items[items.length - 1]?.id || null, + hasMoreItems: index + 3 < entries.length + } + } as ICmsEntryFetcherResult; + } + return { + items: [], + meta: { + totalCount: 0, + cursor: null, + hasMoreItems: false + } + } as ICmsEntryFetcherResult; +}; diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/multipartUpload.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/multipartUpload.test.ts new file mode 100644 index 00000000000..163a0937397 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/multipartUpload.test.ts @@ -0,0 +1,308 @@ +import { + createMultipartUpload, + IMultipartUploadHandler, + MultipartUploadHandler +} from "~/tasks/utils/upload"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { + AbortMultipartUploadCommand, + CompleteMultipartUploadCommand, + S3Client, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import { mockClient } from "aws-sdk-client-mock"; + +/** + * Private property, should not be able to change. + * But we need to test the write method. + */ +const mockMinBufferSize = (upload: IMultipartUploadHandler, size: number) => { + // @ts-expect-error + upload.minBufferSize = size; +}; + +describe("multipart upload", () => { + beforeEach(async () => { + process.env.S3_BUCKET = "a-mock-s3-bucket"; + }); + + it("should properly construct multipart upload", async () => { + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + minBufferSize: "6MB", + parts: undefined + }); + + expect(upload).toBeInstanceOf(MultipartUploadHandler); + // @ts-expect-error + expect(upload.minBufferSize).toEqual(6 * 1024 * 1024); + }); + + it("should properly add a new buffer to the upload but not write", async () => { + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + + const json = JSON.stringify({ + test: "test" + }); + + const result = await upload.add(Buffer.from(json)); + + expect(result.parts.length).toEqual(0); + expect(result.pause).toBeFunction(); + expect(result.canBePaused).toBeFunction(); + + expect(result.canBePaused()).toBeFalse(); + /** + * There must be something in the buffer. + */ + expect(upload.getBuffer()).toEqual({ + buffer: expect.toBeArrayOfSize(1), + bufferLength: json.length + }); + }); + + it("should fail to write because of no etag", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(UploadPartCommand).resolves({ + ETag: "" + }); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + + mockMinBufferSize(upload, 10); + + const json = JSON.stringify({ + test: "test" + }); + + try { + await upload.add(Buffer.from(json)); + } catch (ex) { + expect(ex.message).toEqual(`Failed to upload part: 1`); + } + }); + + it("should properly add a new buffer to the upload and write - should be pausable", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(UploadPartCommand).resolves({ + ETag: "aTestingETag" + }); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + mockMinBufferSize(upload, 10); + + const json = JSON.stringify({ + test: "test" + }); + + const result = await upload.add(Buffer.from(json)); + + expect(result.parts.length).toEqual(1); + expect(result.canBePaused()).toBeTrue(); + + const paused = await result.pause(); + + expect(paused).toEqual({ + parts: [ + { + partNumber: 1, + tag: "aTestingETag" + } + ], + uploadId: "testingUploadId" + }); + }); + + it("should fail to complete the upload because of no parts uploaded", async () => { + expect.assertions(1); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + + try { + await upload.complete(); + } catch (ex) { + expect(ex.message).toEqual("Failed to complete the upload, no parts were uploaded."); + } + }); + + it("should successfully complete the upload", async () => { + expect.assertions(1); + + const mockedClient = mockClient(S3Client); + mockedClient.on(CompleteMultipartUploadCommand).resolves({ + $metadata: {}, + ETag: "aTestingETag-complete" + }); + mockedClient.on(UploadPartCommand).resolves({ + ETag: "aTestingETag-part" + }); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + + mockMinBufferSize(upload, 10); + + const json = JSON.stringify({ testing: "testing" }); + await upload.add(Buffer.from(json)); + + const result = await upload.complete(); + + expect(result).toEqual({ + result: { + $metadata: {}, + ETag: "aTestingETag-complete" + }, + parts: [ + { + partNumber: 1, + tag: "aTestingETag-part" + } + ], + uploadId: "testingUploadId" + }); + }); + + it("should fail to pause the upload because of no parts uploaded", async () => { + expect.assertions(1); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + + try { + const result = await upload.add(Buffer.from("test")); + await result.pause(); + } catch (ex) { + expect(ex.message).toEqual("Failed to pause the upload, buffer was not empty."); + } + }); + + it("should fail to pause the upload because of no tags", async () => { + expect.assertions(1); + + const mockedClient = mockClient(S3Client); + mockedClient.on(UploadPartCommand).resolves({ + ETag: "aTestingETag-part" + }); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + minBufferSize: 10, + parts: undefined + }); + + mockMinBufferSize(upload, 10); + + try { + const result = await upload.add(Buffer.from(JSON.stringify({ testing: "testing" }))); + // @ts-expect-error + upload.parts = []; + await result.pause(); + } catch (ex) { + expect(ex.message).toEqual("Failed to pause the upload, no parts were uploaded."); + } + }); + + it("should successfully pause the upload", async () => { + expect.assertions(2); + + const mockedClient = mockClient(S3Client); + mockedClient.on(UploadPartCommand).resolves({ + ETag: "aTestingETag-pause" + }); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + mockMinBufferSize(upload, 10); + + const json = JSON.stringify({ + test: "test" + }); + + const uploaded = await upload.add(Buffer.from(json)); + expect(uploaded.canBePaused()).toBeTrue(); + + const result = await uploaded.pause(); + + expect(result).toEqual({ + parts: [ + { + partNumber: 1, + tag: "aTestingETag-pause" + } + ], + uploadId: "testingUploadId" + }); + }); + + it("should abort the upload", async () => { + expect.assertions(1); + const mockedClient = mockClient(S3Client); + mockedClient.on(AbortMultipartUploadCommand).resolves({ + $metadata: {} + }); + + const upload = createMultipartUpload({ + uploadId: "testingUploadId", + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + parts: undefined + }); + + const result = await upload.abort(); + + expect(result).toEqual({ + result: { + $metadata: {} + }, + parts: [], + uploadId: "testingUploadId" + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/multipartUploadFactory.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/multipartUploadFactory.test.ts new file mode 100644 index 00000000000..fe6dcccadfb --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/multipartUploadFactory.test.ts @@ -0,0 +1,151 @@ +import { MultipartUploadHandler, MultipartUploadFactory } from "~/tasks/utils/upload"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { + CompleteMultipartUploadCommand, + AbortMultipartUploadCommand, + CreateMultipartUploadCommand, + ListPartsCommand, + S3Client +} from "@webiny/aws-sdk/client-s3"; +import { mockClient } from "aws-sdk-client-mock"; + +describe("multipart upload factory", () => { + beforeEach(async () => { + process.env.S3_BUCKET = "a-mock-s3-bucket"; + const mockedClient = mockClient(S3Client); + mockedClient.on(CreateMultipartUploadCommand).resolves({ + UploadId: "testingUploadId" + }); + mockedClient.on(ListPartsCommand).resolves({ + UploadId: "aTestingUploadId", + Parts: [ + { + ETag: `"abc"`, + PartNumber: 1 + }, + { + ETag: `"def"`, + PartNumber: 2 + } + ] + }); + mockedClient.on(CompleteMultipartUploadCommand).resolves({}); + mockedClient.on(AbortMultipartUploadCommand).resolves({}); + }); + + it("should properly construct multipart upload factory", async () => { + const factory = new MultipartUploadFactory({ + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + createHandler: params => { + return new MultipartUploadHandler(params); + } + }); + + expect(factory).toBeInstanceOf(MultipartUploadFactory); + }); + + it("should properly start a new multipart upload", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(CreateMultipartUploadCommand).resolves({ + UploadId: "testingUploadId" + }); + + const factory = new MultipartUploadFactory({ + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + createHandler: params => { + return new MultipartUploadHandler(params); + } + }); + + const handler = await factory.start(); + + expect(handler).toBeInstanceOf(MultipartUploadHandler); + }); + + it("should fail to start a new multipart upload", async () => { + expect.assertions(1); + const mockedClient = mockClient(S3Client); + mockedClient.on(CreateMultipartUploadCommand).resolves({ + UploadId: "" + }); + + const factory = new MultipartUploadFactory({ + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + createHandler: params => { + return new MultipartUploadHandler(params); + } + }); + + try { + await factory.start(); + } catch (ex) { + expect(ex.message).toBe("Could not initiate multipart upload."); + } + }); + + it("should properly continue a multipart upload", async () => { + const factory = new MultipartUploadFactory({ + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + createHandler: params => { + return new MultipartUploadHandler(params); + } + }); + + const handler = await factory.start({ + uploadId: "testingUploadId" + }); + + expect(handler).toBeInstanceOf(MultipartUploadHandler); + }); + + it("should fail to continue a multipart upload - missing uploadId", async () => { + expect.assertions(1); + + const factory = new MultipartUploadFactory({ + client: createS3Client(), + bucket: getBucket(), + filename: "test.txt", + createHandler: params => { + return new MultipartUploadHandler(params); + } + }); + + try { + await factory.start({ + uploadId: "" + }); + } catch (ex) { + expect(ex.message).toBe(`Missing "uploadId" in the multipart upload handler.`); + } + }); + + it("should fail to continue a multipart upload - missing filename", async () => { + expect.assertions(1); + + const factory = new MultipartUploadFactory({ + client: createS3Client(), + bucket: getBucket(), + filename: "", + createHandler: params => { + return new MultipartUploadHandler(params); + } + }); + + try { + await factory.start({ + uploadId: "aTestingUploadId" + }); + } catch (ex) { + expect(ex.message).toBe(`Missing "filename" in the multipart upload handler.`); + } + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/sizeSegments.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/sizeSegments.test.ts new file mode 100644 index 00000000000..a40d44b077f --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/sizeSegments.test.ts @@ -0,0 +1,79 @@ +import { createSizeSegments } from "~/tasks/utils/helpers/sizeSegments"; +import bytes from "bytes"; + +describe("file segments", () => { + it("should create file segments - 10MB / 1MB", async () => { + const oneMb = bytes.parse("1MB"); + + const segments = createSizeSegments(10000000, oneMb); + + expect(segments).toHaveLength(10); + + expect(segments).toEqual([ + { start: 0, end: oneMb }, + { start: oneMb + 1, end: oneMb * 2 + 1 }, + { start: oneMb * 2 + 2, end: oneMb * 3 + 2 }, + { start: oneMb * 3 + 3, end: oneMb * 4 + 3 }, + { start: oneMb * 4 + 4, end: oneMb * 5 + 4 }, + { start: oneMb * 5 + 5, end: oneMb * 6 + 5 }, + { start: oneMb * 6 + 6, end: oneMb * 7 + 6 }, + { start: oneMb * 7 + 7, end: oneMb * 8 + 7 }, + { start: oneMb * 8 + 8, end: oneMb * 9 + 8 }, + { start: oneMb * 9 + 9, end: 10000000 } + ]); + }); + + it("should create file segments - 100MB / 1MB", async () => { + const segments = createSizeSegments(104857600, "1MB"); + + expect(segments).toHaveLength(100); + }); + + it("should create file segments - 1GB / 1MB", async () => { + const segments = createSizeSegments(1048576000, "1MB"); + + expect(segments).toHaveLength(1000); + }); + + it("should create file segments - 10GB / 1MB", async () => { + const segments = createSizeSegments(10485760000, "1MB"); + + expect(segments).toHaveLength(10000); + }); + + it("should create file segments - 100GB / 1MB", async () => { + const segments = createSizeSegments(104857600000, "1MB"); + + expect(segments).toHaveLength(100000); + }); + + it("should create file segments - 10MB / 5MB", async () => { + const segments = createSizeSegments(10000000, "5MB"); + + expect(segments).toHaveLength(2); + }); + + it("should create file segments - 100MB / 5MB", async () => { + const segments = createSizeSegments(104857600, "5MB"); + + expect(segments).toHaveLength(20); + }); + + it("should create file segments - 1GB / 5MB", async () => { + const segments = createSizeSegments(1048576000, "5MB"); + + expect(segments).toHaveLength(200); + }); + + it("should create file segments - 10GB / 5MB", async () => { + const segments = createSizeSegments(10485760000, "5MB"); + + expect(segments).toHaveLength(2000); + }); + + it("should create file segments - 100GB / 5MB", async () => { + const segments = createSizeSegments(104857600000, "5MB"); + + expect(segments).toHaveLength(20000); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/uniqueResolver.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/uniqueResolver.test.ts new file mode 100644 index 00000000000..34d8a6bf1f0 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/uniqueResolver.test.ts @@ -0,0 +1,99 @@ +import { UniqueResolver } from "~/tasks/utils/uniqueResolver/UniqueResolver"; +import { assets, IMockAsset } from "~tests/mocks/assets"; + +interface IItem { + id: string; +} + +describe("unique resolver", () => { + it("should return unique values only", async () => { + const resolver = new UniqueResolver(); + + const items: IItem[] = [ + { + id: "1" + }, + { + id: "2" + }, + { + id: "3" + }, + { + id: "1" + } + ]; + + const expected = [ + { + id: "1" + }, + { + id: "2" + }, + { + id: "3" + } + ]; + + const result = resolver.resolve(items, "id"); + + expect(result).toEqual(expected); + + /** + * Must return nothing because unique values were returned with last call. + */ + const anotherResult = resolver.resolve(items, "id"); + + expect(anotherResult).toEqual([]); + + const yetAnotherItems: IItem[] = [ + { + id: "4" + }, + { + id: "5" + }, + { + id: "4" + } + ]; + + const yetAnotherResult = resolver.resolve(yetAnotherItems, "id"); + + expect(yetAnotherResult).toEqual([ + { + id: "4" + }, + { + id: "5" + } + ]); + }); + + it("should return unique values only - large dataset", async () => { + const resolver = new UniqueResolver(); + + const result = resolver.resolve( + [ + ...assets, + ...assets, + { + url: "aMockUrl", + key: "aMockKeyWhichDefinitelyDoesNotExistInTheDataset" + } + ], + "key" + ); + + expect(result).toHaveLength(assets.length + 1); + + const extraAsset: IMockAsset = { + url: "aMockUrl-2", + key: "aMockKeyWhichDefinitelyDoesNotExistInTheDataset-2" + }; + const anotherResult = resolver.resolve([...assets, ...assets, extraAsset], "key"); + + expect(anotherResult).toEqual([extraAsset]); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/upload.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/upload.test.ts new file mode 100644 index 00000000000..54cf7737121 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/upload.test.ts @@ -0,0 +1,73 @@ +import { createPassThrough } from "~tests/mocks/createPassThrough"; +import { mockClient } from "aws-sdk-client-mock"; +import { + CreateMultipartUploadCommand, + createS3Client, + S3Client, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import { PassThrough } from "stream"; +import { Upload } from "~/tasks/utils/upload"; +import { WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; + +describe("upload", () => { + it("should properly create an instance of Upload", async () => { + const client = mockClient(S3Client); + client.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + client.on(UploadPartCommand).resolves({ ETag: "1" }); + + const stream = createPassThrough(); + + const upload = new Upload({ + client: createS3Client(), + bucket: "my-test-bucket", + stream, + filename: `test.${WEBINY_EXPORT_ENTRIES_EXTENSION}` + }); + + expect(upload.stream).toBeInstanceOf(PassThrough); + }); + + it("should abort upload", async () => { + expect.assertions(4); + + const client = mockClient(S3Client); + client.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + client.on(UploadPartCommand).resolves({ ETag: "1" }); + + const stream = createPassThrough(); + + const buffers: Buffer[] = []; + + let buffer: Buffer | undefined = undefined; + + stream.on("data", chunk => { + buffers.push(chunk); + }); + + stream.on("end", () => { + buffer = Buffer.concat(buffers); + }); + + const upload = new Upload({ + client: createS3Client(), + bucket: "my-test-bucket", + stream, + filename: `test.${WEBINY_EXPORT_ENTRIES_EXTENSION}` + }); + + expect(upload.stream).toBeInstanceOf(PassThrough); + + setTimeout(() => { + upload.abort(); + }, 250); + try { + await upload.done(); + } catch (ex) { + expect(ex.message).toEqual("Upload aborted."); + } + + expect(buffer).toBeUndefined(); + expect(buffers).toHaveLength(0); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/urlSigner.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/urlSigner.test.ts new file mode 100644 index 00000000000..a1059a9ed96 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/urlSigner.test.ts @@ -0,0 +1,62 @@ +import { UrlSigner } from "~/tasks/utils/urlSigner"; +import { + GetObjectCommand, + HeadObjectCommand, + ListObjectsCommand, + S3Client +} from "@webiny/aws-sdk/client-s3"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { mockClient } from "aws-sdk-client-mock"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; + +describe("url signer", () => { + beforeEach(async () => { + process.env.S3_BUCKET = "testing-bucket"; + }); + + it("should sign the url with head command", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(ListObjectsCommand).resolves({}); + mockedClient.on(HeadObjectCommand).resolves({}); + + const urlSigner = new UrlSigner({ + bucket: getBucket(), + client: createS3Client() + }); + + const result = await urlSigner.head({ + key: "a-key.zip", + timeout: 1000 + }); + expect(result).toEqual({ + bucket: getBucket(), + expiresOn: expect.toBeDateString(), + key: "a-key.zip", + url: expect.toBeString() + }); + }); + + it("should sign the url with get command", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(ListObjectsCommand).resolves({}); + mockedClient.on(GetObjectCommand).resolves({}); + + const urlSigner = new UrlSigner({ + bucket: getBucket(), + client: createS3Client() + }); + + const result = await urlSigner.get({ + key: "a-key.zip", + timeout: 1000 + }); + expect(result).toEqual({ + bucket: getBucket(), + expiresOn: expect.toBeDateString(), + key: "a-key.zip", + url: expect.toBeString() + }); + + expect(result.url).toContain("x-id=GetObject"); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/utils/zipper.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/utils/zipper.test.ts new file mode 100644 index 00000000000..381641925a3 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/utils/zipper.test.ts @@ -0,0 +1,182 @@ +import { Zipper } from "~/tasks/utils/zipper"; +import { Upload } from "~/tasks/utils/upload"; +import { createPassThrough } from "~tests/mocks/createPassThrough"; +import { createArchiver } from "~/tasks/utils/archiver"; +import { mockClient } from "aws-sdk-client-mock"; +import { Upload as BaseUpload } from "@webiny/aws-sdk/lib-storage"; +import { + CreateMultipartUploadCommand, + createS3Client, + S3Client, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import AdmZip from "adm-zip"; +import { WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; + +describe("zipper", () => { + it("should properly create a Zipper instance", async () => { + mockClient(S3Client); + + const upload = new Upload({ + client: createS3Client(), + bucket: "my-test-bucket", + stream: createPassThrough(), + filename: `test.${WEBINY_EXPORT_ENTRIES_EXTENSION}` + }); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true + } + }); + + const zipper = new Zipper({ + upload, + archiver + }); + + expect(zipper.archiver).not.toBeNull(); + }); + + it("should properly zip a file", async () => { + const stream = createPassThrough(); + + const buffers: Buffer[] = []; + + let buffer: Buffer | undefined = undefined; + + stream.on("data", chunk => { + buffers.push(chunk); + }); + + stream.on("end", () => { + buffer = Buffer.concat(buffers); + }); + + const client = mockClient(S3Client); + client.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + client.on(UploadPartCommand).resolves({ ETag: "1" }); + + const upload = new Upload({ + client: createS3Client(), + bucket: "my-test-bucket", + stream, + factory(params) { + return new BaseUpload(params); + }, + filename: `test.${WEBINY_EXPORT_ENTRIES_EXTENSION}` + }); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true + } + }); + + const zipper = new Zipper({ + upload, + archiver + }); + + const json = JSON.stringify({ + aReallyComplexJson: "yes!", + nested: { + a: "b", + c: "d", + evenMoreNested: { + e: "f", + g: "h" + } + } + }); + + await zipper.add(Buffer.from(json), { + name: "test.json" + }); + + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + await zipper.finalize(); + + await new Promise(resolve => { + setTimeout(resolve, 2000); + }); + + await zipper.done(); + + expect(buffer).toBeInstanceOf(Buffer); + + const zipped = buffer!.toString("utf-8"); + + expect(zipped).toMatch("test.json"); + + const zip = new AdmZip(buffer); + + const zipEntries = zip.getEntries(); + + expect(zipEntries.length).toEqual(1); + + expect(zipEntries[0].entryName).toEqual("test.json"); + + expect(zip.readAsText(zipEntries[0])).toEqual(json); + }); + + it("should abort zipper if no records", async () => { + expect.assertions(3); + const stream = createPassThrough(); + + const buffers: Buffer[] = []; + + let buffer: Buffer | undefined = undefined; + + stream.on("data", chunk => { + buffers.push(chunk); + }); + + stream.on("end", () => { + buffer = Buffer.concat(buffers); + }); + + const client = mockClient(S3Client); + client.on(CreateMultipartUploadCommand).resolves({ UploadId: "1" }); + client.on(UploadPartCommand).resolves({ ETag: "1" }); + + const upload = new Upload({ + client: createS3Client(), + bucket: "my-test-bucket", + stream, + factory(params) { + return new BaseUpload(params); + }, + filename: `test.${WEBINY_EXPORT_ENTRIES_EXTENSION}` + }); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true + } + }); + + const zipper = new Zipper({ + upload, + archiver + }); + + setTimeout(() => { + zipper.abort(); + }, 250); + + try { + await zipper.done(); + } catch (ex) { + expect(ex.message).toEqual("Upload aborted."); + } + + expect(buffer).toBeUndefined(); + expect(buffers).toHaveLength(0); + }); +}); diff --git a/packages/api-headless-cms-import-export/__tests__/tasks/validateImportFromUrl.test.ts b/packages/api-headless-cms-import-export/__tests__/tasks/validateImportFromUrl.test.ts new file mode 100644 index 00000000000..9878b34e4c8 --- /dev/null +++ b/packages/api-headless-cms-import-export/__tests__/tasks/validateImportFromUrl.test.ts @@ -0,0 +1,356 @@ +import { createRunner } from "@webiny/project-utils/testing/tasks"; +import { useHandler } from "~tests/helpers/useHandler"; +import { CmsImportExportFileType, Context, ICmsImportExportFile } from "~/types"; +import { createValidateImportFromUrlTask } from "~/tasks"; +import { ResponseDoneResult, ResponseErrorResult } from "@webiny/tasks"; +import { NonEmptyArray } from "@webiny/api/types"; +import { IValidateImportFromUrlInput } from "~/tasks/domain/abstractions/ValidateImportFromUrl"; +import { HeadObjectCommand, S3Client } from "@webiny/aws-sdk/client-s3"; +import { mockClient } from "aws-sdk-client-mock"; + +jest.mock("~/tasks/utils/externalFileFetcher", () => { + return { + ExternalFileFetcher: function () { + return { + mocked: "yes", + timeout: 100, + async fetch(url: string) { + if (url.includes("error")) { + return { + error: { + code: "GET_FETCH_ERROR", + message: "Fetch error.", + data: { + url + } + } + }; + } else if (url.includes("missing")) { + return { + file: null, + error: null + }; + } + return { + file: { + name: url.split("/").pop() as string, + size: 1234, + url, + contentType: "application/zip", + checksum: "checksum" + } + }; + }, + async head(url: string) { + if (url.includes("error")) { + return { + error: { + code: "HEAD_FETCH_ERROR", + message: "Fetch error.", + data: { + url + } + } + }; + } else if (url.includes("missing")) { + return { + file: null, + error: null + }; + } + return { + file: { + name: url.split("/").pop() as string, + size: 1234, + url, + contentType: "application/zip", + checksum: "checksum" + } + }; + } + }; + } + }; +}); + +describe("validate import from url task", () => { + let context: Context; + + beforeEach(async () => { + const { createContext } = useHandler(); + context = await createContext(); + }); + + it("should run the task and return a error response - no task with given id", async () => { + const definition = createValidateImportFromUrlTask(); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: "unknownTaskId", + tenant: "root", + locale: "en-US" + }); + + expect(result).toBeInstanceOf(ResponseErrorResult); + expect(result).toMatchObject({ + status: "error", + error: { + code: "TASK_NOT_FOUND", + message: 'Task "unknownTaskId" cannot be executed because it does not exist.' + }, + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: "unknownTaskId" + }); + }); + + it("should run the task and return a error response - faulty input", async () => { + const definition = createValidateImportFromUrlTask(); + + const task = await context.tasks.createTask({ + name: 'Import Content Entries from URL Controller for "modelId"', + definitionId: definition.id, + input: {} + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id, + tenant: "root", + locale: "en-US" + }); + + expect(result).toBeInstanceOf(ResponseErrorResult); + expect(result).toMatchObject({ + error: { + code: "NO_FILES_FOUND", + data: { + input: {} + }, + message: "No files found." + }, + status: "error", + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id + }); + }); + + it("should run the task and return a error response - file validation failed", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(HeadObjectCommand).resolves({ + ETag: `"checksum"` + }); + const definition = createValidateImportFromUrlTask(); + + const files: NonEmptyArray = [ + { + head: "https://example.com/file1.json", + get: "https://example.com/file1.json", + type: CmsImportExportFileType.ENTRIES, + error: undefined, + checksum: "checksum", + key: "key-file1.json" + }, + { + head: "https://example.com/file2.wa.zip", + get: "https://example.com/file2.wa.zip", + type: CmsImportExportFileType.ASSETS, + error: undefined, + checksum: "checksum", + key: "key-file2.wa.zip" + }, + { + head: "https://example.com/file3-error.wa.zip", + get: "https://example.com/file3-error.wa.zip", + type: CmsImportExportFileType.ASSETS, + error: undefined, + checksum: "checksum", + key: "key-file3-error.wa.zip" + }, + { + head: "https://example.com/file4-missing.wa.zip", + get: "https://example.com/file4-missing.wa.zip", + type: CmsImportExportFileType.ASSETS, + error: undefined, + checksum: "checksum", + key: "key-file4-missing.wa.zip" + } + ]; + + const task = await context.tasks.createTask({ + name: 'Import Content Entries from URL Controller for "modelId"', + definitionId: definition.id, + input: { + files + } + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id, + tenant: "root", + locale: "en-US" + }); + + expect(result).toBeInstanceOf(ResponseDoneResult); + expect(result).toEqual({ + status: "done", + locale: "en-US", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id, + message: undefined, + output: { + modelId: undefined, + error: { + code: "INVALID_FILES", + data: { + files: [ + "key-file1.json", + "key-file3-error.wa.zip", + "key-file4-missing.wa.zip" + ] + }, + message: "One or more files are invalid." + }, + files: [ + { + ...files[0], + checked: true, + error: { + data: { + pathname: "/file1.json", + type: "json" + }, + code: "FILE_TYPE_NOT_SUPPORTED", + message: "File type not supported." + }, + size: undefined, + type: undefined + }, + { + ...files[1], + checked: true, + size: 1234 + }, + { + ...files[2], + checked: true, + error: { + code: "HEAD_FETCH_ERROR", + data: { + url: files[2].head + }, + message: "Fetch error." + }, + type: undefined, + size: undefined + }, + { + ...files[3], + checked: true, + error: { + code: "FILE_NOT_FOUND", + data: { + url: files[3].head + }, + message: "File not found." + }, + type: undefined, + size: undefined + } + ] + } + }); + }); + + it("should properly validate all the files", async () => { + const mockedClient = mockClient(S3Client); + mockedClient.on(HeadObjectCommand).resolves({}); + + const definition = createValidateImportFromUrlTask(); + + const files: NonEmptyArray = [ + { + head: "https://example.com/file1.we.zip", + get: "https://example.com/file1.we.zip", + type: CmsImportExportFileType.ENTRIES, + error: undefined, + checksum: "checksum", + key: "key-file1.we.zip" + }, + { + head: "https://example.com/file2.wa.zip", + get: "https://example.com/file2.wa.zip", + type: CmsImportExportFileType.ASSETS, + error: undefined, + checksum: "checksum", + key: "key-file2.wa.zip" + } + ]; + + const task = await context.tasks.createTask({ + name: 'Import Content Entries from URL Controller for "modelId"', + definitionId: definition.id, + input: { + files, + model: { + modelId: "modelId", + fields: [] + } + } + }); + + const runner = createRunner({ + context, + task: definition + }); + + const result = await runner({ + webinyTaskId: task.id, + tenant: "root", + locale: "en-US" + }); + + expect(result).toBeInstanceOf(ResponseDoneResult); + expect(result).toMatchObject({ + locale: "en-US", + message: undefined, + output: { + files: [ + { + get: "https://example.com/file1.we.zip", + head: "https://example.com/file1.we.zip", + size: 1234, + type: CmsImportExportFileType.ENTRIES + }, + { + get: "https://example.com/file2.wa.zip", + head: "https://example.com/file2.wa.zip", + size: 1234, + type: CmsImportExportFileType.ASSETS + } + ] + }, + status: "done", + tenant: "root", + webinyTaskDefinitionId: definition.id, + webinyTaskId: task.id + }); + }); +}); diff --git a/packages/api-headless-cms-import-export/jest.setup.js b/packages/api-headless-cms-import-export/jest.setup.js new file mode 100644 index 00000000000..f0ad2654ddf --- /dev/null +++ b/packages/api-headless-cms-import-export/jest.setup.js @@ -0,0 +1,12 @@ +const base = require("../../jest.config.base"); +const presets = require("@webiny/project-utils/testing/presets")( + ["@webiny/api-file-manager", "storage-operations"], + ["@webiny/api-headless-cms", "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-import-export/package.json b/packages/api-headless-cms-import-export/package.json new file mode 100644 index 00000000000..bcab25c409d --- /dev/null +++ b/packages/api-headless-cms-import-export/package.json @@ -0,0 +1,65 @@ +{ + "name": "@webiny/api-headless-cms-import-export", + "version": "0.0.0", + "main": "index.js", + "description": "Import / Export for Webiny Headless CMS", + "keywords": [ + "api-headless-cms-import-export:base" + ], + "repository": { + "type": "git", + "url": "https://github.com/webiny/webiny-js.git", + "directory": "packages/api-headless-cms-import-export" + }, + "license": "MIT", + "dependencies": { + "@smithy/node-http-handler": "^2.1.6", + "@webiny/api-file-manager": "0.0.0", + "@webiny/api-headless-cms": "0.0.0", + "@webiny/aws-sdk": "0.0.0", + "@webiny/error": "0.0.0", + "@webiny/handler-graphql": "0.0.0", + "@webiny/plugins": "0.0.0", + "@webiny/tasks": "0.0.0", + "@webiny/utils": "0.0.0", + "archiver": "^7.0.1", + "bytes": "^3.1.2", + "uniqid": "^5.4.0", + "unzipper": "^0.12.3", + "zod": "^3.23.8" + }, + "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", + "@types/adm-zip": "^0.5.5", + "@types/unzipper": "^0.10.10", + "@webiny/api": "0.0.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": "0.0.0", + "@webiny/handler-aws": "0.0.0", + "@webiny/plugins": "0.0.0", + "@webiny/project-utils": "0.0.0", + "@webiny/wcp": "0.0.0", + "adm-zip": "^0.5.14", + "aws-sdk-client-mock": "^4.0.1", + "graphql": "^15.8.0", + "ttypescript": "^1.5.13", + "typescript": "^4.7.4" + }, + "scripts": { + "build": "yarn webiny run build", + "watch": "yarn webiny run watch" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/index.ts b/packages/api-headless-cms-import-export/src/crud/index.ts new file mode 100644 index 00000000000..589406d345a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/index.ts @@ -0,0 +1,184 @@ +import type { + CmsImportExportObject, + Context, + ICmsImportExportObjectAbortExportParams, + ICmsImportExportObjectAbortImportFromUrlParams, + ICmsImportExportObjectGetExportParams, + ICmsImportExportObjectGetImportFromUrlParams, + ICmsImportExportObjectGetValidateImportFromUrlParams, + ICmsImportExportObjectGetValidateImportFromUrlResult, + ICmsImportExportObjectImportFromUrlParams, + ICmsImportExportObjectImportFromUrlResult, + ICmsImportExportObjectStartExportParams, + ICmsImportExportObjectValidateImportFromUrlParams, + ICmsImportExportObjectValidateImportFromUrlResult, + ICmsImportExportRecord, + IListExportContentEntriesParams, + IListExportContentEntriesResult +} from "~/types"; +import { + AbortExportContentEntriesUseCase, + GetExportContentEntriesUseCase, + GetImportFromUrlUseCase, + GetValidateImportFromUrlUseCase, + ImportFromUrlUseCase, + ListExportContentEntriesUseCase, + ValidateImportFromUrlIntegrityUseCase, + ValidateImportFromUrlUseCase +} from "./useCases"; +import { NotFoundError } from "@webiny/handler-graphql"; +import { ExportContentEntriesUseCase } from "~/crud/useCases/exportContentEntries"; +import { UrlSigner } from "~/tasks/utils/urlSigner"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { AbortImportFromUrlUseCase } from "./useCases/abortImportFromUrl"; + +export const createHeadlessCmsImportExportCrud = async ( + context: Context +): Promise => { + const urlSigner = new UrlSigner({ + bucket: getBucket(), + client: createS3Client() + }); + + const getExportContentEntriesUseCase = new GetExportContentEntriesUseCase({ + getTask: context.tasks.getTask, + urlSigner + }); + + const listExportContentEntriesUseCase = new ListExportContentEntriesUseCase({ + listTasks: context.tasks.listTasks + }); + + const validateImportFromUrlUseCase = new ValidateImportFromUrlUseCase({ + getModelToAstConverter: context.cms.getModelToAstConverter, + getModel: context.cms.getModel + }); + const validateImportFromUrlIntegrityUseCase = new ValidateImportFromUrlIntegrityUseCase({ + triggerTask: context.tasks.trigger + }); + const getValidateImportFromUrlIntegrityUseCase = new GetValidateImportFromUrlUseCase({ + getTask: context.tasks.getTask + }); + + const importFromUrlUseCase = new ImportFromUrlUseCase({ + updateTask: context.tasks.updateTask, + getTask: context.tasks.getTask, + triggerTask: context.tasks.trigger + }); + + const getImportFromUrlUseCase = new GetImportFromUrlUseCase({ + getTask: context.tasks.getTask + }); + + const exportContentEntriesUseCase = new ExportContentEntriesUseCase({ + triggerTask: context.tasks.trigger + }); + + const abortExportContentEntriesUseCase = new AbortExportContentEntriesUseCase({ + abortTask: context.tasks.abort + }); + + const abortImportFromUrlUseCase = new AbortImportFromUrlUseCase({ + getTaskUseCase: getImportFromUrlUseCase, + abortTask: context.tasks.abort + }); + + const getExportContentEntries = async ( + params: ICmsImportExportObjectGetExportParams + ): Promise => { + const result = await getExportContentEntriesUseCase.execute(params); + if (!result) { + throw new NotFoundError( + `Export content entries task with id "${params.id}" not found.` + ); + } + return result; + }; + + const listExportContentEntries = async ( + params?: IListExportContentEntriesParams + ): Promise => { + return listExportContentEntriesUseCase.execute(params); + }; + + const exportContentEntries = async ( + params: ICmsImportExportObjectStartExportParams + ): Promise => { + return exportContentEntriesUseCase.execute(params); + }; + + const abortExportContentEntries = async ( + params: ICmsImportExportObjectAbortExportParams + ): Promise => { + return abortExportContentEntriesUseCase.execute(params); + }; + + const validateImportFromUrl = async ( + params: ICmsImportExportObjectValidateImportFromUrlParams + ): Promise => { + const { files, model } = await validateImportFromUrlUseCase.execute({ + data: params.data + }); + const result = await validateImportFromUrlIntegrityUseCase.execute({ + model, + files + }); + + return { + files, + modelId: model.modelId, + ...result + }; + }; + + const getValidateImportFromUrl = async ( + params: ICmsImportExportObjectGetValidateImportFromUrlParams + ): Promise => { + const result = await getValidateImportFromUrlIntegrityUseCase.execute(params); + if (!result) { + throw new NotFoundError( + `Validate import from URL task with id "${params.id}" not found.` + ); + } + return result; + }; + + const importFromUrl = async ( + params: ICmsImportExportObjectImportFromUrlParams + ): Promise => { + const result = await importFromUrlUseCase.execute(params); + if (!result) { + throw new NotFoundError(`Import from URL task with id "${params.id}" not found.`); + } + return result; + }; + + const getImportFromUrl = async ( + params: ICmsImportExportObjectGetImportFromUrlParams + ): Promise => { + const result = await getImportFromUrlUseCase.execute(params); + if (!result) { + throw new NotFoundError(`Import from URL task with id "${params.id}" not found.`); + } + return result; + }; + + const abortImportFromUrl = async ( + params: ICmsImportExportObjectAbortImportFromUrlParams + ): Promise => { + return await abortImportFromUrlUseCase.execute(params); + }; + + return { + getExportContentEntries, + listExportContentEntries, + exportContentEntries, + abortExportContentEntries, + validateImportFromUrl, + getValidateImportFromUrl, + importFromUrl, + getImportFromUrl, + abortImportFromUrl + }; +}; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/AbortExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/AbortExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..9ff157dc745 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/AbortExportContentEntriesUseCase.ts @@ -0,0 +1,35 @@ +import type { ICmsImportExportRecord } from "~/domain"; +import type { + IAbortExportContentEntriesUseCase, + IAbortExportContentEntriesUseCaseExecuteParams +} from "./abstractions/AbortExportContentEntriesUseCase"; +import { convertTaskToCmsExportRecord } from "~/crud/utils/convertTaskToExportRecord"; +import type { ITasksContextObject } from "@webiny/tasks"; +import type { + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput +} from "~/tasks/domain/abstractions/ExportContentEntriesController"; + +export interface IAbortExportContentEntriesUseCaseParams { + abortTask: ITasksContextObject["abort"]; +} + +export class AbortExportContentEntriesUseCase implements IAbortExportContentEntriesUseCase { + private readonly abortTask: ITasksContextObject["abort"]; + + public constructor(params: IAbortExportContentEntriesUseCaseParams) { + this.abortTask = params.abortTask; + } + + public async execute( + params: IAbortExportContentEntriesUseCaseExecuteParams + ): Promise { + const task = await this.abortTask< + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput + >({ + id: params.id + }); + return convertTaskToCmsExportRecord(task); + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/abstractions/AbortExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/abstractions/AbortExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..42cb3de650a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/abstractions/AbortExportContentEntriesUseCase.ts @@ -0,0 +1,11 @@ +import type { ICmsImportExportRecord } from "~/domain"; + +export interface IAbortExportContentEntriesUseCaseExecuteParams { + id: string; +} + +export interface IAbortExportContentEntriesUseCase { + execute( + params: IAbortExportContentEntriesUseCaseExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/index.ts new file mode 100644 index 00000000000..64d4dee582e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/abortExportContentEntries/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/AbortExportContentEntriesUseCase"; +export * from "./AbortExportContentEntriesUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/AbortImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/AbortImportFromUrlUseCase.ts new file mode 100644 index 00000000000..d04d75007b7 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/AbortImportFromUrlUseCase.ts @@ -0,0 +1,48 @@ +import { ITasksContextObject } from "@webiny/tasks"; +import { IImportFromUrlUseCaseExecuteResponse } from "../importFromUrl"; +import { + IAbortImportFromUrlUseCase, + IAbortImportFromUrlUseCaseExecuteParams +} from "./abstractions/AbortImportFromUrlUseCase"; +import { NotFoundError } from "@webiny/handler-graphql"; +import { convertTaskToImportRecord } from "~/crud/utils/convertTaskToImportRecord"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; +import { IGetImportFromUrlUseCase } from "~/crud/useCases/getImportFromUrl"; + +export interface IAbortImportFromUrlUseCaseParams { + getTaskUseCase: IGetImportFromUrlUseCase; + abortTask: ITasksContextObject["abort"]; +} + +export class AbortImportFromUrlUseCase implements IAbortImportFromUrlUseCase { + private readonly getTaskUseCase: IGetImportFromUrlUseCase; + private readonly abortTask: ITasksContextObject["abort"]; + + public constructor(params: IAbortImportFromUrlUseCaseParams) { + this.getTaskUseCase = params.getTaskUseCase; + this.abortTask = params.abortTask; + } + public async execute( + params: IAbortImportFromUrlUseCaseExecuteParams + ): Promise { + const task = await this.getTaskUseCase.execute(params); + if (!task) { + throw new NotFoundError(`Task with id "${params.id}" not found.`); + } + + try { + const result = await this.abortTask< + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput + >(params); + return convertTaskToImportRecord(result); + } catch (ex) { + console.log("Could not abort the task."); + console.error(ex); + throw ex; + } + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/abstractions/AbortImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/abstractions/AbortImportFromUrlUseCase.ts new file mode 100644 index 00000000000..0fd6af27047 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/abstractions/AbortImportFromUrlUseCase.ts @@ -0,0 +1,11 @@ +import type { IImportFromUrlUseCaseExecuteResponse } from "~/crud/useCases"; + +export interface IAbortImportFromUrlUseCaseExecuteParams { + id: string; +} + +export interface IAbortImportFromUrlUseCase { + execute: ( + params: IAbortImportFromUrlUseCaseExecuteParams + ) => Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/index.ts new file mode 100644 index 00000000000..31e90be6d43 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/abortImportFromUrl/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/AbortImportFromUrlUseCase"; +export * from "./AbortImportFromUrlUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/ExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/ExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..f2bc46dabcc --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/ExportContentEntriesUseCase.ts @@ -0,0 +1,45 @@ +import type { ICmsImportExportRecord } from "~/domain"; +import type { + IExportContentEntriesUseCase, + IExportContentEntriesUseCaseExecuteParams +} from "./abstractions/ExportContentEntriesUseCase"; +import type { + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput +} from "~/tasks/domain/abstractions/ExportContentEntriesController"; +import { EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK } from "~/tasks/constants"; +import { convertTaskToCmsExportRecord } from "~/crud/utils/convertTaskToExportRecord"; +import type { ITasksContextObject } from "@webiny/tasks"; + +export interface IExportContentEntriesUseCaseParams { + triggerTask: ITasksContextObject["trigger"]; +} + +export class ExportContentEntriesUseCase implements IExportContentEntriesUseCase { + private readonly triggerTask: ITasksContextObject["trigger"]; + + public constructor(params: IExportContentEntriesUseCaseParams) { + this.triggerTask = params.triggerTask; + } + + public async execute( + params: IExportContentEntriesUseCaseExecuteParams + ): Promise { + const task = await this.triggerTask< + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput + >({ + name: `Export Content Entries and Assets Controller for "${params.modelId}"`, + input: { + modelId: params.modelId, + exportAssets: params.exportAssets, + limit: params.limit, + where: params.where, + sort: params.sort + }, + definition: EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK + }); + + return convertTaskToCmsExportRecord(task); + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/abstractions/ExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/abstractions/ExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..8aefc1d8070 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/abstractions/ExportContentEntriesUseCase.ts @@ -0,0 +1,14 @@ +import type { ICmsImportExportRecord } from "~/domain"; +import type { CmsEntryListSort, CmsEntryListWhere } from "@webiny/api-headless-cms/types"; + +export interface IExportContentEntriesUseCaseExecuteParams { + modelId: string; + exportAssets: boolean; + limit?: number; + where?: CmsEntryListWhere; + sort?: CmsEntryListSort; +} + +export interface IExportContentEntriesUseCase { + execute(params: IExportContentEntriesUseCaseExecuteParams): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/index.ts new file mode 100644 index 00000000000..ed6191c3248 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/exportContentEntries/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/ExportContentEntriesUseCase"; +export * from "./ExportContentEntriesUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/GetExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/GetExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..9832711b6a7 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/GetExportContentEntriesUseCase.ts @@ -0,0 +1,72 @@ +import type { + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput +} from "~/tasks/domain/abstractions/ExportContentEntriesController"; +import { convertTaskToCmsExportRecord } from "~/crud/utils/convertTaskToExportRecord"; +import type { ITasksContextObject } from "@webiny/tasks"; +import type { + IGetExportContentEntriesUseCase, + IGetExportContentEntriesUseCaseExecuteParams, + IGetExportContentEntriesUseCaseExecuteResponse +} from "./abstractions/GetExportContentEntriesUseCase"; +import { EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK } from "~/tasks/constants"; +import type { IUrlSigner } from "~/tasks/utils/urlSigner"; +import { prependExportPath } from "~/tasks/utils/helpers/exportPath"; + +export interface IGetExportContentEntriesUseCaseParams { + getTask: ITasksContextObject["getTask"]; + urlSigner: IUrlSigner; +} + +export class GetExportContentEntriesUseCase implements IGetExportContentEntriesUseCase { + private readonly getTask: ITasksContextObject["getTask"]; + private readonly urlSigner: IUrlSigner; + + public constructor(params: IGetExportContentEntriesUseCaseParams) { + this.getTask = params.getTask; + this.urlSigner = params.urlSigner; + } + + public async execute( + params: IGetExportContentEntriesUseCaseExecuteParams + ): Promise { + const task = await this.getTask< + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput + >(params.id); + + if (task?.definitionId !== EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK) { + return null; + } + + const record = convertTaskToCmsExportRecord(task); + + if (!record.files) { + return record; + } + + const files = await Promise.all( + record.files.map(async file => { + const key = prependExportPath(file.key); + const { url: get } = await this.urlSigner.get({ + ...file, + key + }); + const { url: head } = await this.urlSigner.head({ + ...file, + key + }); + return { + ...file, + get, + head + }; + }) + ); + + return { + ...record, + files + }; + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/abstractions/GetExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/abstractions/GetExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..0c11ee008c3 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/abstractions/GetExportContentEntriesUseCase.ts @@ -0,0 +1,13 @@ +import type { ICmsImportExportRecord } from "~/domain/abstractions/CmsImportExportRecord"; + +export interface IGetExportContentEntriesUseCaseExecuteParams { + id: string; +} + +export type IGetExportContentEntriesUseCaseExecuteResponse = ICmsImportExportRecord; + +export interface IGetExportContentEntriesUseCase { + execute( + params: IGetExportContentEntriesUseCaseExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/index.ts new file mode 100644 index 00000000000..7f63e5280b1 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getExportContentEntries/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/GetExportContentEntriesUseCase"; +export * from "./GetExportContentEntriesUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/GetImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/GetImportFromUrlUseCase.ts new file mode 100644 index 00000000000..b7d5a602acb --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/GetImportFromUrlUseCase.ts @@ -0,0 +1,38 @@ +import type { IGetImportFromUrlUseCase } from "~/crud/useCases/getImportFromUrl/abstractions/GetImportFromUrlUseCase"; +import type { + IImportFromUrlUseCaseExecuteParams, + IImportFromUrlUseCaseExecuteResponse +} from "../importFromUrl/abstractions/ImportFromUrlUseCase"; +import type { ITasksContextObject } from "@webiny/tasks"; +import { IMPORT_FROM_URL_CONTROLLER_TASK } from "~/tasks/constants"; +import { convertTaskToImportRecord } from "~/crud/utils/convertTaskToImportRecord"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; + +export interface IGetImportFromUrlUseCaseParams { + getTask: ITasksContextObject["getTask"]; +} + +export class GetImportFromUrlUseCase implements IGetImportFromUrlUseCase { + private readonly getTask: ITasksContextObject["getTask"]; + + public constructor(params: IGetImportFromUrlUseCaseParams) { + this.getTask = params.getTask; + } + + public async execute( + params: IImportFromUrlUseCaseExecuteParams + ): Promise { + const task = await this.getTask< + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput + >(params.id); + + if (task?.definitionId !== IMPORT_FROM_URL_CONTROLLER_TASK) { + return null; + } + return convertTaskToImportRecord(task); + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/abstractions/GetImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/abstractions/GetImportFromUrlUseCase.ts new file mode 100644 index 00000000000..4fc2a1ec5cd --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/abstractions/GetImportFromUrlUseCase.ts @@ -0,0 +1,10 @@ +import type { + IImportFromUrlUseCaseExecuteParams, + IImportFromUrlUseCaseExecuteResponse +} from "~/crud/useCases/importFromUrl/abstractions/ImportFromUrlUseCase"; + +export interface IGetImportFromUrlUseCase { + execute( + params: IImportFromUrlUseCaseExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/index.ts new file mode 100644 index 00000000000..f6e0f6a3728 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getImportFromUrl/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/GetImportFromUrlUseCase"; +export * from "./GetImportFromUrlUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/GetValidateImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/GetValidateImportFromUrlUseCase.ts new file mode 100644 index 00000000000..13b61ee427a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/GetValidateImportFromUrlUseCase.ts @@ -0,0 +1,38 @@ +import type { ITasksContextObject } from "@webiny/tasks"; +import { VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK } from "~/tasks/constants"; +import type { + IGetValidateImportFromUrlExecuteParams, + IGetValidateImportFromUrlExecuteResponse, + IGetValidateImportFromUrlUseCase +} from "./abstractions/GetValidateImportFromUrlUseCase"; +import type { + IValidateImportFromUrlInput, + IValidateImportFromUrlOutput +} from "~/tasks/domain/abstractions/ValidateImportFromUrl"; +import { convertTaskToValidateImportFromUrlRecord } from "~/crud/utils/convertTaskToValidateImportFromUrlRecord"; + +export interface IGetValidateImportFromUrlUseCaseParams { + getTask: ITasksContextObject["getTask"]; +} + +export class GetValidateImportFromUrlUseCase implements IGetValidateImportFromUrlUseCase { + private readonly getTask: ITasksContextObject["getTask"]; + + public constructor(params: IGetValidateImportFromUrlUseCaseParams) { + this.getTask = params.getTask; + } + + public async execute( + params: IGetValidateImportFromUrlExecuteParams + ): Promise { + const task = await this.getTask( + params.id + ); + + if (task?.definitionId !== VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK) { + return null; + } + + return convertTaskToValidateImportFromUrlRecord(task); + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/abstractions/GetValidateImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/abstractions/GetValidateImportFromUrlUseCase.ts new file mode 100644 index 00000000000..d336a0cc05b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/abstractions/GetValidateImportFromUrlUseCase.ts @@ -0,0 +1,13 @@ +import type { ICmsImportExportValidateRecord } from "~/domain/abstractions/CmsImportExportValidateRecord"; + +export interface IGetValidateImportFromUrlExecuteParams { + id: string; +} + +export type IGetValidateImportFromUrlExecuteResponse = ICmsImportExportValidateRecord; + +export interface IGetValidateImportFromUrlUseCase { + execute( + params: IGetValidateImportFromUrlExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/index.ts new file mode 100644 index 00000000000..d20eb017dfd --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/getValidateImportFromUrl/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/GetValidateImportFromUrlUseCase"; +export * from "./GetValidateImportFromUrlUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/ImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/ImportFromUrlUseCase.ts new file mode 100644 index 00000000000..25cbf22a746 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/ImportFromUrlUseCase.ts @@ -0,0 +1,121 @@ +import type { + IImportFromUrlUseCase, + IImportFromUrlUseCaseExecuteParams, + IImportFromUrlUseCaseExecuteResponse +} from "./abstractions/ImportFromUrlUseCase"; +import type { ITasksContextObject } from "@webiny/tasks"; +import { TaskDataStatus } from "@webiny/tasks"; +import { + IMPORT_FROM_URL_CONTROLLER_TASK, + VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK +} from "~/tasks/constants"; +import { WebinyError } from "@webiny/error"; +import type { + IValidateImportFromUrlInput, + IValidateImportFromUrlOutput +} from "~/tasks/domain/abstractions/ValidateImportFromUrl"; +import { convertTaskToImportRecord } from "~/crud/utils/convertTaskToImportRecord"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; +import type { NonEmptyArray } from "@webiny/api/types"; +import type { + ICmsImportExportValidatedAssetsFile, + ICmsImportExportValidatedContentEntriesFile +} from "~/types"; + +export interface IImportFromUrlUseCaseParams { + updateTask: ITasksContextObject["updateTask"]; + getTask: ITasksContextObject["getTask"]; + triggerTask: ITasksContextObject["trigger"]; +} + +export class ImportFromUrlUseCase implements IImportFromUrlUseCase { + private readonly updateTask: ITasksContextObject["updateTask"]; + private readonly getTask: ITasksContextObject["getTask"]; + private readonly triggerTask: ITasksContextObject["trigger"]; + + public constructor(params: IImportFromUrlUseCaseParams) { + this.updateTask = params.updateTask; + this.getTask = params.getTask; + this.triggerTask = params.triggerTask; + } + + public async execute( + params: IImportFromUrlUseCaseExecuteParams + ): Promise { + /** + * First we need to check if the integrity task exists and that it ran successfully. + */ + const integrityTask = await this.getTask< + IValidateImportFromUrlInput, + IValidateImportFromUrlOutput + >(params.id); + if (integrityTask?.definitionId !== VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK) { + return null; + } else if (integrityTask.taskStatus !== TaskDataStatus.SUCCESS) { + throw new WebinyError({ + message: "Integrity check failed.", + code: "INTEGRITY_CHECK_FAILED", + data: { + status: integrityTask.taskStatus, + files: integrityTask.output?.files, + error: integrityTask.output?.error + } + }); + } else if (!integrityTask.output?.files?.length) { + throw new WebinyError({ + message: "No files found in the provided data.", + code: "NO_FILES_FOUND" + }); + } else if (integrityTask.output.importTaskId) { + throw new WebinyError({ + message: "Import was already started. You cannot start it again.", + code: "IMPORT_TASK_EXISTS", + data: { + id: integrityTask.output.importTaskId + } + }); + } + const errors = integrityTask.output.files.filter(file => !!file.error); + if (errors.length) { + throw new WebinyError({ + message: "Some files failed validation.", + code: "FILES_FAILED_VALIDATION", + data: { + files: errors + } + }); + } + /** + * Now we need to check if the actual import task already exists. + * We don't want to run the import task multiple times. + */ + const importTask = await this.triggerTask< + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput + >({ + name: `Import from URL`, + definition: IMPORT_FROM_URL_CONTROLLER_TASK, + input: { + modelId: integrityTask.output.modelId, + files: integrityTask.output.files as NonEmptyArray< + | ICmsImportExportValidatedContentEntriesFile + | ICmsImportExportValidatedAssetsFile + >, + maxInsertErrors: params.maxInsertErrors, + steps: {} + }, + parent: integrityTask + }); + + await this.updateTask(integrityTask.id, { + output: { + ...integrityTask.output, + importTaskId: importTask.id + } + }); + return convertTaskToImportRecord(importTask); + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/abstractions/ImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/abstractions/ImportFromUrlUseCase.ts new file mode 100644 index 00000000000..541b2e827df --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/abstractions/ImportFromUrlUseCase.ts @@ -0,0 +1,23 @@ +import type { GenericRecord } from "@webiny/api/types"; +import type { TaskDataStatus } from "@webiny/tasks"; + +export interface IImportFromUrlUseCaseExecuteParams { + id: string; + maxInsertErrors?: number; +} + +export interface IImportFromUrlUseCaseExecuteResponse { + id: string; + done: string[]; + failed: string[]; + aborted: string[]; + invalid: string[]; + status: TaskDataStatus; + error?: GenericRecord; +} + +export interface IImportFromUrlUseCase { + execute( + params: IImportFromUrlUseCaseExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/index.ts new file mode 100644 index 00000000000..c8f3f38ee5a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/importFromUrl/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/ImportFromUrlUseCase"; +export * from "./ImportFromUrlUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/index.ts new file mode 100644 index 00000000000..187d487085a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/index.ts @@ -0,0 +1,9 @@ +export * from "./abortExportContentEntries"; +export * from "./exportContentEntries"; +export * from "./getExportContentEntries"; +export * from "./getImportFromUrl"; +export * from "./getValidateImportFromUrl"; +export * from "./importFromUrl"; +export * from "./listExportContentEntries"; +export * from "./validateImportFromUrl"; +export * from "./validateImportFromUrlIntegrity"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/ListExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/ListExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..813c75dc1ab --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/ListExportContentEntriesUseCase.ts @@ -0,0 +1,44 @@ +import type { + IListExportContentEntriesUseCase, + IListExportContentEntriesUseCaseExecuteParams, + IListExportContentEntriesUseCaseExecuteResult +} from "./abstractions/ListExportContentEntriesUseCase"; +import type { ITasksContextObject } from "@webiny/tasks"; +import { convertTaskToCmsExportRecord } from "~/crud/utils/convertTaskToExportRecord"; +import { EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK } from "~/tasks/constants"; +import type { + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput +} from "~/tasks/domain/abstractions/ExportContentEntriesController"; + +export interface IListExportContentEntriesUseCaseParams { + listTasks: ITasksContextObject["listTasks"]; +} + +export class ListExportContentEntriesUseCase implements IListExportContentEntriesUseCase { + private readonly listTasks: ITasksContextObject["listTasks"]; + + public constructor(params: IListExportContentEntriesUseCaseParams) { + this.listTasks = params.listTasks; + } + + public async execute( + params?: IListExportContentEntriesUseCaseExecuteParams + ): Promise { + const result = await this.listTasks< + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput + >({ + ...params, + sort: ["createdOn_DESC"], + where: { + definitionId: EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK + } + }); + + return { + items: result.items.map(item => convertTaskToCmsExportRecord(item)), + meta: result.meta + }; + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/abstractions/ListExportContentEntriesUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/abstractions/ListExportContentEntriesUseCase.ts new file mode 100644 index 00000000000..c745168f2da --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/abstractions/ListExportContentEntriesUseCase.ts @@ -0,0 +1,11 @@ +import type { IListExportContentEntriesResult, IListExportContentEntriesParams } from "~/types"; + +export type IListExportContentEntriesUseCaseExecuteParams = IListExportContentEntriesParams; + +export type IListExportContentEntriesUseCaseExecuteResult = IListExportContentEntriesResult; + +export interface IListExportContentEntriesUseCase { + execute( + params?: IListExportContentEntriesUseCaseExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/index.ts new file mode 100644 index 00000000000..9b5921c9be8 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/listExportContentEntries/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/ListExportContentEntriesUseCase"; +export * from "./ListExportContentEntriesUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/ValidateImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/ValidateImportFromUrlUseCase.ts new file mode 100644 index 00000000000..d7429865eca --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/ValidateImportFromUrlUseCase.ts @@ -0,0 +1,95 @@ +import type { + IValidateImportFromUrlUseCase, + IValidateImportFromUrlUseCaseExecuteParams, + IValidateImportFromUrlUseCaseExecuteResult +} from "./abstractions/ValidateImportFromUrlUseCase"; +import { CmsImportExportFileType } from "~/types"; +import type { ICmsImportExportFile } from "~/types"; +import type { NonEmptyArray } from "@webiny/api/types"; +import { WebinyError } from "@webiny/error"; +import { getImportExportFileType } from "~/tasks/utils/helpers/getImportExportFileType"; +import { parseImportUrlData } from "~/crud/utils/parseImportUrlData"; +import type { CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; +import { makeSureModelsAreIdentical } from "~/crud/utils/makeSureModelsAreIdentical"; + +export interface IValidateImportFromUrlUseCaseParams { + getModel: HeadlessCms["getModel"]; + getModelToAstConverter: HeadlessCms["getModelToAstConverter"]; +} + +export class ValidateImportFromUrlUseCase implements IValidateImportFromUrlUseCase { + private readonly getModel: HeadlessCms["getModel"]; + private readonly getModelToAstConverter: HeadlessCms["getModelToAstConverter"]; + + public constructor(params: IValidateImportFromUrlUseCaseParams) { + this.getModel = params.getModel; + this.getModelToAstConverter = params.getModelToAstConverter; + } + + public async execute( + params: IValidateImportFromUrlUseCaseExecuteParams + ): Promise { + const { data } = params; + + const { model: validatedModel, files } = parseImportUrlData(data); + /** + * There must be at least one file in the data. + */ + if (files.length === 0) { + throw new WebinyError("No files found in the provided data.", "NO_FILES_FOUND"); + } + /** + * Next step of the validation is to verify that, at least, one entries type file exists in the data. + */ + const entries = files.find(file => { + return file.type === CmsImportExportFileType.ENTRIES; + }); + if (!entries) { + throw new WebinyError("No entries file found in the provided data.", "NO_ENTRIES_FILE"); + } + + let model: CmsModel; + try { + model = await this.getModel(validatedModel.modelId); + } catch (ex) { + if (ex.code !== "NOT_FOUND") { + throw ex; + } + throw new WebinyError( + `Model provided in the JSON data, "${validatedModel.modelId}", not found.`, + "MODEL_NOT_FOUND", + { + modelId: validatedModel.modelId + } + ); + } + + makeSureModelsAreIdentical({ + getModelToAstConverter: this.getModelToAstConverter, + model, + target: validatedModel + }); + + return { + model, + files: files.reduce((collection, file) => { + const result = getImportExportFileType(file.head); + if (result.error) { + file.error = { + message: "File type not supported.", + code: "FILE_TYPE_NOT_SUPPORTED", + data: { + type: result.type, + pathname: result.pathname + } + }; + collection.push(file); + return collection; + } + + collection.push(file); + return collection; + }, [] as unknown as NonEmptyArray) + }; + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/abstractions/ValidateImportFromUrlUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/abstractions/ValidateImportFromUrlUseCase.ts new file mode 100644 index 00000000000..c01991b7c57 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/abstractions/ValidateImportFromUrlUseCase.ts @@ -0,0 +1,18 @@ +import type { ICmsImportExportFile } from "~/types"; +import type { GenericRecord, NonEmptyArray } from "@webiny/api/types"; +import type { CmsModel } from "@webiny/api-headless-cms/types"; + +export interface IValidateImportFromUrlUseCaseExecuteParams { + data: string | GenericRecord; +} + +export interface IValidateImportFromUrlUseCaseExecuteResult { + model: CmsModel; + files: NonEmptyArray; +} + +export interface IValidateImportFromUrlUseCase { + execute( + params: IValidateImportFromUrlUseCaseExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/index.ts new file mode 100644 index 00000000000..d385c9136c6 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrl/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/ValidateImportFromUrlUseCase"; +export * from "./ValidateImportFromUrlUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/ValidateImportFromUrlIntegrityUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/ValidateImportFromUrlIntegrityUseCase.ts new file mode 100644 index 00000000000..ac1c64f3498 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/ValidateImportFromUrlIntegrityUseCase.ts @@ -0,0 +1,42 @@ +import type { + IValidateImportFromUrlIntegrityUseCase, + IValidateImportFromUrlIntegrityUseCaseExecuteParams, + IValidateImportFromUrlIntegrityUseCaseExecuteResult +} from "./abstractions/ValidateImportFromUrlIntegrityUseCase"; +import type { ITasksContextObject } from "@webiny/tasks"; +import { VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK } from "~/tasks/constants"; +import type { IValidateImportFromUrlInput } from "~/tasks/domain/abstractions/ValidateImportFromUrl"; + +export interface IValidateImportFromUrlIntegrityUseCaseParams { + triggerTask: ITasksContextObject["trigger"]; +} + +export class ValidateImportFromUrlIntegrityUseCase + implements IValidateImportFromUrlIntegrityUseCase +{ + private readonly triggerTask: ITasksContextObject["trigger"]; + + public constructor(params: IValidateImportFromUrlIntegrityUseCaseParams) { + this.triggerTask = params.triggerTask; + } + + public async execute( + params: IValidateImportFromUrlIntegrityUseCaseExecuteParams + ): Promise { + const { files, model } = params; + + const task = await this.triggerTask({ + name: `Validate Import from URL Integrity`, + definition: VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK, + input: { + model, + files + } + }); + + return { + id: task.id, + status: task.taskStatus + }; + } +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/abstractions/ValidateImportFromUrlIntegrityUseCase.ts b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/abstractions/ValidateImportFromUrlIntegrityUseCase.ts new file mode 100644 index 00000000000..4423396e9e0 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/abstractions/ValidateImportFromUrlIntegrityUseCase.ts @@ -0,0 +1,20 @@ +import type { NonEmptyArray } from "@webiny/api/types"; +import type { ICmsImportExportFile } from "~/types"; +import type { TaskDataStatus } from "@webiny/tasks"; +import type { CmsModel } from "@webiny/api-headless-cms/types"; + +export interface IValidateImportFromUrlIntegrityUseCaseExecuteParams { + files: NonEmptyArray; + model: CmsModel; +} + +export interface IValidateImportFromUrlIntegrityUseCaseExecuteResult { + id: string; + status: TaskDataStatus; +} + +export interface IValidateImportFromUrlIntegrityUseCase { + execute( + params: IValidateImportFromUrlIntegrityUseCaseExecuteParams + ): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/index.ts b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/index.ts new file mode 100644 index 00000000000..0713fbf3e92 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/useCases/validateImportFromUrlIntegrity/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/ValidateImportFromUrlIntegrityUseCase"; +export * from "./ValidateImportFromUrlIntegrityUseCase"; diff --git a/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToExportRecord.ts b/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToExportRecord.ts new file mode 100644 index 00000000000..495aaac12a9 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToExportRecord.ts @@ -0,0 +1,21 @@ +import type { ITask } from "@webiny/tasks"; +import { createCmsImportExportRecord } from "~/domain/CmsImportExportRecord"; +import type { + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput +} from "~/tasks/domain/abstractions/ExportContentEntriesController"; + +export const convertTaskToCmsExportRecord = ( + task: ITask +) => { + return createCmsImportExportRecord({ + id: task.id, + createdOn: task.createdOn, + createdBy: task.createdBy, + finishedOn: task.finishedOn || null, + modelId: task.input.modelId, + exportAssets: task.input.exportAssets, + files: task.output?.files || null, + status: task.taskStatus + }); +}; diff --git a/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToImportRecord.ts b/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToImportRecord.ts new file mode 100644 index 00000000000..f5819059cdf --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToImportRecord.ts @@ -0,0 +1,20 @@ +import type { ITask } from "@webiny/tasks"; +import type { ICmsImportExportObjectImportFromUrlResult } from "~/types"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; + +export const convertTaskToImportRecord = ( + task: ITask +): ICmsImportExportObjectImportFromUrlResult => { + return { + id: task.id, + status: task.taskStatus, + done: task.output?.done || [], + aborted: task.output?.aborted || [], + invalid: task.output?.invalid || [], + failed: task.output?.failed || [], + error: task.output?.error + }; +}; diff --git a/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToValidateImportFromUrlRecord.ts b/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToValidateImportFromUrlRecord.ts new file mode 100644 index 00000000000..a7416dcd2d7 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/utils/convertTaskToValidateImportFromUrlRecord.ts @@ -0,0 +1,32 @@ +import { createCmsImportValidateRecord } from "~/domain"; +import type { ITask } from "@webiny/tasks"; +import type { + IValidateImportFromUrlInput, + IValidateImportFromUrlOutput +} from "~/tasks/domain/abstractions/ValidateImportFromUrl"; +import type { NonEmptyArray } from "@webiny/api/types"; +import type { ICmsImportExportValidatedFile } from "~/types"; + +export const convertTaskToValidateImportFromUrlRecord = ( + task: ITask +) => { + const files = task.input.files.map(file => { + const output = task.output?.files?.find(f => f.checksum === file.checksum); + if (output) { + return { + ...output, + error: output.error, + type: output.type, + size: output.size + }; + } + return file; + }); + + return createCmsImportValidateRecord({ + id: task.id, + files: files as unknown as NonEmptyArray, + status: task.taskStatus, + error: task.output?.error + }); +}; diff --git a/packages/api-headless-cms-import-export/src/crud/utils/makeSureModelsAreIdentical.ts b/packages/api-headless-cms-import-export/src/crud/utils/makeSureModelsAreIdentical.ts new file mode 100644 index 00000000000..f91cc8f8741 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/utils/makeSureModelsAreIdentical.ts @@ -0,0 +1,85 @@ +import type { + CmsModel, + CmsModelAst, + CmsModelField, + HeadlessCms +} from "@webiny/api-headless-cms/types"; +import type { IExportedCmsModel } from "~/tasks/domain/abstractions/ExportContentEntriesController"; +import { ModelFieldTraverser } from "@webiny/api-headless-cms/utils"; +import { WebinyError } from "@webiny/error"; + +export interface IMakeSureModelsAreIdenticalParams { + getModelToAstConverter: HeadlessCms["getModelToAstConverter"]; + model: CmsModel; + target: IExportedCmsModel; +} + +interface IResult { + key: string; + path: string; + field: CmsModelField; +} + +const getModelValues = (ast: CmsModelAst): IResult[] => { + const traverser = new ModelFieldTraverser(); + + const results: IResult[] = []; + + traverser.traverse(ast, ({ field, path }) => { + const ref = field.settings?.models + ? `#R#${field.settings.models + .map(m => m.modelId) + .sort() + .join(",")}` + : ""; + + const key = `${field.type}@${path.join(".")}#${field.multipleValues ? "m" : "s"}${ref}`; + results.push({ + key, + field, + path: path.join(".") + }); + }); + + return results; +}; + +export const makeSureModelsAreIdentical = (params: IMakeSureModelsAreIdenticalParams): void => { + const { getModelToAstConverter, model, target } = params; + + const converter = getModelToAstConverter(); + + const modelAst = converter.toAst(model); + const targetAst = converter.toAst(target); + + const modelValues = getModelValues(modelAst); + const targetValues = getModelValues(targetAst); + /** + * First we will go through the model from the database. + * Then we will go through the exported model and check against the model from the database. + */ + for (const value of modelValues) { + if (targetValues.some(v => v.key === value.key)) { + continue; + } + throw new WebinyError({ + message: `Field "${value.field.fieldId}" not found in the model provided via the JSON data.`, + code: "MODEL_FIELD_NOT_FOUND", + data: { + ...value + } + }); + } + for (const value of targetValues) { + if (modelValues.some(v => v.key === value.key)) { + continue; + } + throw new WebinyError({ + message: `Field "${value.field.fieldId}" not found in the model from the database.`, + code: "MODEL_FIELD_NOT_FOUND", + data: { + ...value + } + }); + } +}; diff --git a/packages/api-headless-cms-import-export/src/crud/utils/parseImportUrlData.ts b/packages/api-headless-cms-import-export/src/crud/utils/parseImportUrlData.ts new file mode 100644 index 00000000000..56228bdf69e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/crud/utils/parseImportUrlData.ts @@ -0,0 +1,66 @@ +import { CmsImportExportFileType } from "~/types"; +import type { ICmsImportExportFile } from "~/types"; +import zod from "zod"; +import { WebinyError } from "@webiny/error"; +import { createZodError } from "@webiny/utils"; +import type { IExportedCmsModel } from "~/tasks/domain/abstractions/ExportContentEntriesController"; +import type { GenericRecord } from "@webiny/api/types"; + +const validateData = zod.object({ + /** + * Basic model validation. + * We will check it more thoroughly in the next step. + */ + model: zod.object({ + modelId: zod.string(), + fields: zod + .array( + zod.object({ + id: zod.string(), + fieldId: zod.string(), + type: zod.string(), + multipleValues: zod.boolean().optional(), + settings: zod + .object({ + fields: zod.array(zod.object({}).passthrough()).optional(), + templates: zod.array(zod.object({}).passthrough()).optional() + }) + .passthrough() + .optional() + }) + ) + .min(1) + }), + files: zod.array( + zod.object({ + get: zod.string().url(), + head: zod.string().url(), + key: zod.string(), + checksum: zod.string(), + type: zod.enum([CmsImportExportFileType.ENTRIES, CmsImportExportFileType.ASSETS]) + }) + ) +}); + +export interface IParseImportUrlDataResult { + model: IExportedCmsModel; + files: ICmsImportExportFile[]; +} + +export const parseImportUrlData = (input: string | GenericRecord): IParseImportUrlDataResult => { + let json: unknown; + try { + json = typeof input === "string" ? JSON.parse(input) : input; + } catch (ex) { + throw new WebinyError("Invalid input data provided.", "INVALID_INPUT_DATA"); + } + + const result = validateData.safeParse(json); + if (!result.success) { + throw createZodError(result.error); + } + return { + model: result.data.model as unknown as IExportedCmsModel, + files: result.data.files + }; +}; diff --git a/packages/api-headless-cms-import-export/src/domain/CmsImportExportRecord.ts b/packages/api-headless-cms-import-export/src/domain/CmsImportExportRecord.ts new file mode 100644 index 00000000000..0cb29c02eac --- /dev/null +++ b/packages/api-headless-cms-import-export/src/domain/CmsImportExportRecord.ts @@ -0,0 +1,33 @@ +import type { + ICmsImportExportRecord, + ICmsImportExportRecordFile +} from "~/domain/abstractions/CmsImportExportRecord"; +import type { TaskDataStatus } from "@webiny/tasks"; + +export class CmsImportExportRecord implements ICmsImportExportRecord { + public id: string; + public createdOn: string; + public createdBy: any; + public finishedOn: string | null; + public modelId: string; + public files: ICmsImportExportRecordFile[] | null; + public exportAssets: boolean; + public status: TaskDataStatus; + + constructor(data: ICmsImportExportRecord) { + this.id = data.id; + this.createdOn = data.createdOn; + this.createdBy = data.createdBy; + this.finishedOn = data.finishedOn; + this.modelId = data.modelId; + this.files = data.files; + this.exportAssets = data.exportAssets; + this.status = data.status; + } +} + +export const createCmsImportExportRecord = ( + data: ICmsImportExportRecord +): ICmsImportExportRecord => { + return new CmsImportExportRecord(data); +}; diff --git a/packages/api-headless-cms-import-export/src/domain/CmsImportExportValidateRecord.ts b/packages/api-headless-cms-import-export/src/domain/CmsImportExportValidateRecord.ts new file mode 100644 index 00000000000..32cb84ab612 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/domain/CmsImportExportValidateRecord.ts @@ -0,0 +1,24 @@ +import type { ICmsImportExportValidateRecord } from "~/domain/abstractions/CmsImportExportValidateRecord"; +import type { ICmsImportExportValidatedFile } from "~/types"; +import type { GenericRecord, NonEmptyArray } from "@webiny/api/types"; +import type { TaskDataStatus } from "@webiny/tasks"; + +export class CmsImportExportValidateRecord implements ICmsImportExportValidateRecord { + public readonly id: string; + public readonly files: NonEmptyArray | undefined; + public readonly status: TaskDataStatus; + public readonly error: GenericRecord | undefined; + + public constructor(params: ICmsImportExportValidateRecord) { + this.id = params.id; + this.files = params.files; + this.status = params.status; + this.error = params.error; + } +} + +export const createCmsImportValidateRecord = ( + data: ICmsImportExportValidateRecord +): ICmsImportExportValidateRecord => { + return new CmsImportExportValidateRecord(data); +}; diff --git a/packages/api-headless-cms-import-export/src/domain/abstractions/CmsImportExportRecord.ts b/packages/api-headless-cms-import-export/src/domain/abstractions/CmsImportExportRecord.ts new file mode 100644 index 00000000000..c46de384860 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/domain/abstractions/CmsImportExportRecord.ts @@ -0,0 +1,20 @@ +import type { CmsIdentity } from "@webiny/api-headless-cms/types"; +import type { TaskDataStatus } from "@webiny/tasks"; +import type { CmsImportExportFileType } from "~/types"; + +export interface ICmsImportExportRecordFile { + key: string; + checksum: string; + type: CmsImportExportFileType; +} + +export interface ICmsImportExportRecord { + id: string; + createdOn: string; + createdBy: CmsIdentity; + finishedOn: string | null; + modelId: string; + files: ICmsImportExportRecordFile[] | null; + exportAssets: boolean; + status: TaskDataStatus; +} diff --git a/packages/api-headless-cms-import-export/src/domain/abstractions/CmsImportExportValidateRecord.ts b/packages/api-headless-cms-import-export/src/domain/abstractions/CmsImportExportValidateRecord.ts new file mode 100644 index 00000000000..4181131afcb --- /dev/null +++ b/packages/api-headless-cms-import-export/src/domain/abstractions/CmsImportExportValidateRecord.ts @@ -0,0 +1,10 @@ +import type { ICmsImportExportValidatedFile } from "~/types"; +import type { GenericRecord, NonEmptyArray } from "@webiny/api/types"; +import type { TaskDataStatus } from "@webiny/tasks"; + +export interface ICmsImportExportValidateRecord { + id: string; + files: NonEmptyArray | undefined; + status: TaskDataStatus; + error?: GenericRecord; +} diff --git a/packages/api-headless-cms-import-export/src/domain/index.ts b/packages/api-headless-cms-import-export/src/domain/index.ts new file mode 100644 index 00000000000..31991a49d4e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/domain/index.ts @@ -0,0 +1,4 @@ +export * from "./abstractions/CmsImportExportRecord"; +export * from "./CmsImportExportRecord"; +export * from "./abstractions/CmsImportExportValidateRecord"; +export * from "./CmsImportExportValidateRecord"; diff --git a/packages/api-headless-cms-import-export/src/graphql/index.ts b/packages/api-headless-cms-import-export/src/graphql/index.ts new file mode 100644 index 00000000000..a18a1e07407 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/graphql/index.ts @@ -0,0 +1,24 @@ +import type { Context } from "~/types"; +import { CmsGraphQLSchemaPlugin } from "@webiny/api-headless-cms"; +import { createTypeDefs } from "./typeDefs"; +import { createResolvers } from "./resolvers"; +import { listModels } from "~/graphql/models"; +import type { NonEmptyArray } from "@webiny/api/types"; +import { CmsModel } from "@webiny/api-headless-cms/types"; + +export const attachHeadlessCmsImportExportGraphQL = async (context: Context): Promise => { + const models = await listModels(context); + + if (models.length === 0) { + return; + } + + const plugin = new CmsGraphQLSchemaPlugin({ + typeDefs: createTypeDefs(models as NonEmptyArray), + resolvers: createResolvers(models as NonEmptyArray) + }); + + plugin.name = "headlessCms.graphql.importExport"; + + context.plugins.register(plugin); +}; diff --git a/packages/api-headless-cms-import-export/src/graphql/models.ts b/packages/api-headless-cms-import-export/src/graphql/models.ts new file mode 100644 index 00000000000..c75ad717635 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/graphql/models.ts @@ -0,0 +1,16 @@ +import type { Context } from "~/types"; +import { CmsModel } from "@webiny/api-headless-cms/types"; + +export const listModels = async (context: Context): Promise => { + return await context.security.withoutAuthorization(async () => { + try { + const models = await context.cms.listModels(); + + return models.filter(model => { + return !model.isPrivate; + }); + } catch (ex) { + return []; + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/graphql/resolvers.ts b/packages/api-headless-cms-import-export/src/graphql/resolvers.ts new file mode 100644 index 00000000000..6814e6cd886 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/graphql/resolvers.ts @@ -0,0 +1,169 @@ +import type { Context } from "~/types"; +import { createZodError } from "@webiny/utils"; +import { resolve, resolveList } from "@webiny/handler-graphql"; +import zod from "zod"; +import type { GenericRecord, NonEmptyArray } from "@webiny/api/types"; +import { CmsEntryListSort, CmsEntryListWhere, CmsModel } from "@webiny/api-headless-cms/types"; + +const validateAbortExportContentEntries = zod.object({ + id: zod.string() +}); + +const validateGetExportContentEntries = zod.object({ + id: zod.string() +}); + +const validateListExportContentEntries = zod.object({ + limit: zod.number().optional().default(50), + after: zod.string().optional() +}); + +const validateImportFromUrl = zod.object({ + data: zod.string().or(zod.object({}).passthrough()) +}); + +const getValidateImportFromUrl = zod.object({ + id: zod.string() +}); + +const getImportFromUrl = zod.object({ + id: zod.string() +}); + +const importFromUrlValidation = zod.object({ + id: zod.string(), + maxInsertErrors: zod.number().optional().default(100), + overwrite: zod.boolean().optional().default(false) +}); + +const abortImportFromUrl = zod.object({ + id: zod.string() +}); + +const validateExportContentEntriesInput = zod.object({ + exportAssets: zod.boolean().optional().default(false), + limit: zod.number().optional(), + where: zod.object({}).passthrough().optional().default({}), + sort: zod.array(zod.string()).optional() +}); +/** + * Create export resolver for each of the models given. + */ +const createExportContentEntries = (models: NonEmptyArray) => { + return models.reduce>((resolvers, model) => { + resolvers[`export${model.pluralApiName}ContentEntries`] = async ( + _: unknown, + input: unknown, + context: Context + ) => { + return resolve(async () => { + const result = validateExportContentEntriesInput.safeParse(input); + + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.exportContentEntries({ + ...result.data, + modelId: model.modelId, + sort: result.data.sort ? (result.data.sort as CmsEntryListSort) : undefined, + where: result.data.where ? (result.data.where as CmsEntryListWhere) : undefined + }); + }); + }; + return resolvers; + }, {}); +}; + +export const createResolvers = (models: NonEmptyArray) => { + return { + Query: { + async getExportContentEntries(_: unknown, input: unknown, context: Context) { + return resolve(async () => { + const result = validateGetExportContentEntries.safeParse(input); + + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.getExportContentEntries(result.data); + }); + }, + async listExportContentEntries(_: unknown, input: unknown, context: Context) { + return resolveList(async () => { + const result = validateListExportContentEntries.safeParse(input); + if (!result.success) { + throw createZodError(result.error); + } + return await context.cmsImportExport.listExportContentEntries(result.data); + }); + }, + async getValidateImportFromUrl(_: unknown, input: unknown, context: Context) { + return resolve(async () => { + const result = getValidateImportFromUrl.safeParse(input); + + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.getValidateImportFromUrl(result.data); + }); + }, + async getImportFromUrl(_: unknown, input: unknown, context: Context) { + return resolve(async () => { + const result = getImportFromUrl.safeParse(input); + + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.getImportFromUrl(result.data); + }); + } + }, + Mutation: { + ...createExportContentEntries(models), + async abortExportContentEntries(_: unknown, input: unknown, context: Context) { + return resolve(async () => { + const result = validateAbortExportContentEntries.safeParse(input); + + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.abortExportContentEntries(result.data); + }); + }, + async validateImportFromUrl(_: unknown, input: unknown, context: Context) { + return resolve(async () => { + const result = validateImportFromUrl.safeParse(input); + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.validateImportFromUrl(result.data); + }); + }, + async importFromUrl(_: unknown, input: unknown, context: Context) { + return resolve(async () => { + const result = importFromUrlValidation.safeParse(input); + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.importFromUrl(result.data); + }); + }, + async abortImportFromUrl(_: unknown, input: unknown, context: Context) { + return resolve(async () => { + const result = abortImportFromUrl.safeParse(input); + if (!result.success) { + throw createZodError(result.error); + } + + return await context.cmsImportExport.abortImportFromUrl(result.data); + }); + } + } + }; +}; diff --git a/packages/api-headless-cms-import-export/src/graphql/typeDefs.ts b/packages/api-headless-cms-import-export/src/graphql/typeDefs.ts new file mode 100644 index 00000000000..844e6421741 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/graphql/typeDefs.ts @@ -0,0 +1,166 @@ +import type { NonEmptyArray } from "@webiny/api/types"; +import { CmsModel } from "@webiny/api-headless-cms/types"; + +const createExportContentEntriesByModel = (models: NonEmptyArray): string => { + return models + .map(model => { + return /* GraphQL */ ` + export${model.pluralApiName}ContentEntries( + # limit on how much entries will be fetched in a single batch - mostly used for testing + limit: Int + # do we export assets as well? default is false + exportAssets: Boolean + # filter the entries by providing a where input + where: ${model.singularApiName}ListWhereInput + # if after is provided, export will start after the provided cursor + after: String + ): ExportContentEntriesResponse! + + `; + }) + .join("\n"); +}; + +export const createTypeDefs = (models: NonEmptyArray): string => { + return /* GraphQL */ ` + enum ExportContentEntriesExportRecordStatusEnum { + pending + running + failed + success + aborted + } + + type ExportContentEntriesExportRecordFile { + get: String! + head: String! + key: String! + type: String! + checksum: String! + } + + type ExportContentEntriesExportRecord { + id: ID! + createdOn: DateTime! + createdBy: CmsIdentity! + finishedOn: DateTime + modelId: String! + files: [ExportContentEntriesExportRecordFile!] + exportAssets: Boolean! + status: ExportContentEntriesExportRecordStatusEnum! + } + + type ExportContentEntriesResponse { + data: ExportContentEntriesExportRecord + error: CmsError + } + + type ListExportContentEntriesExportRecord { + id: ID! + createdOn: DateTime! + createdBy: CmsIdentity! + finishedOn: DateTime + modelId: String! + exportAssets: Boolean! + status: ExportContentEntriesExportRecordStatusEnum! + } + + type ListExportContentEntriesResponse { + data: [ListExportContentEntriesExportRecord!] + meta: CmsListMeta + error: CmsError + } + + type AbortExportContentEntriesResponse { + data: ExportContentEntriesExportRecord + error: CmsError + } + + type ValidateImportFromUrlResponseDataFileError { + message: String! + data: JSON + } + + type ValidateImportFromUrlResponseDataFile { + get: String + head: String + type: String + checksum: String + key: String + size: Number + checked: Boolean + error: ValidateImportFromUrlResponseDataFileError + } + + type ValidateImportFromUrlResponseData { + id: ID! + files: [ValidateImportFromUrlResponseDataFile!] + status: String! + error: CmsError + } + + type GetValidateImportFromUrlResponseData { + id: ID! + files: [ValidateImportFromUrlResponseDataFile!] + status: String! + error: CmsError + } + + type GetValidateImportFromUrlResponse { + data: GetValidateImportFromUrlResponseData + error: CmsError + } + + type ValidateImportFromUrlResponse { + data: ValidateImportFromUrlResponseData + error: CmsError + } + + type ImportFromUrlResponseDataFileError { + message: String! + data: JSON + } + + type ImportFromUrlResponseDataFile { + get: String! + head: String! + type: String! + checksum: String! + size: Number! + error: ImportFromUrlResponseDataFileError + } + + type ImportFromUrlResponseData { + id: ID! + files: [ImportFromUrlResponseDataFile!] + status: String! + } + + type ImportFromUrlResponse { + data: ImportFromUrlResponseData + error: CmsError + } + + enum ExportContentEntriesModelsListEnum { + ${models.map(model => model.modelId).join("\n")} + } + + extend type Query { + getExportContentEntries(id: ID!): ExportContentEntriesResponse! + listExportContentEntries(after: String, limit: Int): ListExportContentEntriesResponse! + getValidateImportFromUrl(id: ID!): GetValidateImportFromUrlResponse! + getImportFromUrl(id: ID!): ImportFromUrlResponse! + } + + extend type Mutation { + ${createExportContentEntriesByModel(models)} + + abortExportContentEntries(id: ID!): AbortExportContentEntriesResponse! + validateImportFromUrl(data: JSON!): ValidateImportFromUrlResponse! + # the id is a task id returned from the validateImportFromUrl mutation + # it will be used to get the file information and start a new task for the actual import + importFromUrl(id: ID!): ImportFromUrlResponse! + abortImportFromUrl(id: ID!): ImportFromUrlResponse! + } + `; +}; diff --git a/packages/api-headless-cms-import-export/src/index.ts b/packages/api-headless-cms-import-export/src/index.ts new file mode 100644 index 00000000000..f18a0c98e6f --- /dev/null +++ b/packages/api-headless-cms-import-export/src/index.ts @@ -0,0 +1,41 @@ +import { ContextPlugin } from "@webiny/api"; +import type { Plugin } from "@webiny/plugins/types"; +import { attachHeadlessCmsImportExportGraphQL } from "~/graphql"; +import type { Context } from "./types"; +import { isHeadlessCmsReady } from "@webiny/api-headless-cms"; +import { createHeadlessCmsImportExportCrud } from "~/crud"; +import { + createExportContentAssets, + createExportContentEntriesControllerTask, + createExportContentEntriesTask, + createImportFromUrlControllerTask, + createImportFromUrlDownloadTask, + createValidateImportFromUrlTask, + createImportFromUrlProcessEntriesTask, + createImportFromUrlProcessAssetsTask +} from "~/tasks"; + +export const createHeadlessCmsImportExport = (): Plugin[] => { + const plugin = new ContextPlugin(async context => { + const installed = await isHeadlessCmsReady(context); + if (!installed) { + return; + } + + context.plugins.register( + createExportContentEntriesControllerTask(), + createExportContentEntriesTask(), + createExportContentAssets(), + createValidateImportFromUrlTask(), + createImportFromUrlControllerTask(), + createImportFromUrlDownloadTask(), + createImportFromUrlProcessEntriesTask(), + createImportFromUrlProcessAssetsTask() + ); + + context.cmsImportExport = await createHeadlessCmsImportExportCrud(context); + await attachHeadlessCmsImportExportGraphQL(context); + }); + plugin.name = "headlessCms.context.importExport"; + return [plugin]; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/constants.ts b/packages/api-headless-cms-import-export/src/tasks/constants.ts new file mode 100644 index 00000000000..8446f1a364b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/constants.ts @@ -0,0 +1,17 @@ +export const EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK = "exportContentEntriesController"; +export const EXPORT_CONTENT_ENTRIES_TASK = "exportContentEntries"; +export const EXPORT_CONTENT_ASSETS_TASK = "exportContentAssets"; + +export const VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK = "validateImportFromUrlIntegrity"; +export const IMPORT_FROM_URL_CONTROLLER_TASK = "importFromUrlController"; +export const IMPORT_FROM_URL_DOWNLOAD_TASK = "importFromUrlDownload"; +export const IMPORT_FROM_URL_PROCESS_ENTRIES_TASK = "importFromUrlProcessEntries"; +export const IMPORT_FROM_URL_PROCESS_ASSETS_TASK = "importFromUrlProcessAssets"; + +export const WEBINY_EXPORT_ENTRIES_EXTENSION = "we.zip"; +export const WEBINY_EXPORT_ASSETS_EXTENSION = "wa.zip"; + +export const EXPORT_BASE_PATH = "cms-export"; +export const IMPORT_BASE_PATH = "cms-import"; + +export const MANIFEST_JSON = "manifest.json"; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/ExportContentEntriesController.ts b/packages/api-headless-cms-import-export/src/tasks/domain/ExportContentEntriesController.ts new file mode 100644 index 00000000000..48e56946fda --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/ExportContentEntriesController.ts @@ -0,0 +1,296 @@ +import uniqueId from "uniqid"; +import type { IGetTaskResponse, ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import { TaskDataStatus } from "@webiny/tasks"; +import type { Context } from "~/types"; +import { CmsImportExportFileType } from "~/types"; +import type { + IExportContentEntriesController, + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput, + IExportContentEntriesControllerOutputFile, + IExportedCmsModel +} from "~/tasks/domain/abstractions/ExportContentEntriesController"; +import { ExportContentEntriesControllerState } from "~/tasks/domain/abstractions/ExportContentEntriesController"; +import { + EXPORT_BASE_PATH, + EXPORT_CONTENT_ASSETS_TASK, + EXPORT_CONTENT_ENTRIES_TASK +} from "~/tasks/constants"; +import type { + IExportContentEntriesInput, + IExportContentEntriesOutput +} from "~/tasks/domain/abstractions/ExportContentEntries"; +import type { + IExportContentAssetsInput, + IExportContentAssetsOutput +} from "~/tasks/domain/abstractions/ExportContentAssets"; +import { getBackOffSeconds } from "~/tasks/utils/helpers/getBackOffSeconds"; +import type { CmsModel } from "@webiny/api-headless-cms/types"; + +const prepareExportModel = (model: Pick): IExportedCmsModel => { + return { + modelId: model.modelId, + fields: model.fields + }; +}; + +export class ExportContentEntriesController< + C extends Context = Context, + I extends IExportContentEntriesControllerInput = IExportContentEntriesControllerInput, + O extends IExportContentEntriesControllerOutput = IExportContentEntriesControllerOutput +> implements IExportContentEntriesController +{ + public async run(params: ITaskRunParams): Promise> { + const { context, response, input, store, trigger } = params; + const { state, modelId } = input; + + let model: CmsModel; + try { + model = await context.cms.getModel(modelId); + } catch (ex) { + return response.error({ + message: `Model "${modelId}" not found.`, + code: "MODEL_NOT_FOUND" + }); + } + + const backOffSeconds = getBackOffSeconds(store.getTask().iterations); + + const taskId = store.getTask().id; + + let entriesTask: IGetTaskResponse; + + /** + * In case of no state yet, we will start the content entries export process. + */ + const prefix = input.prefix || uniqueId(`${EXPORT_BASE_PATH}/${model.modelId}/${taskId}`); + if (!state) { + const task = await trigger({ + definition: EXPORT_CONTENT_ENTRIES_TASK, + input: { + prefix, + exportAssets: input.exportAssets, + modelId: model.modelId, + limit: input.limit, + where: input.where, + sort: input.sort + }, + name: `Export Content Entries ${taskId}` + }); + + return response.continue( + { + ...input, + prefix, + contentEntriesTaskId: task.id, + state: ExportContentEntriesControllerState.entryExport + }, + { + seconds: backOffSeconds + } + ); + } + /** + * If the state of the task is "entryExport", we need to check if there are any child tasks of the "Export Content Entries" task. + * If there are, we need to wait for them to finish before we can proceed. + * If there are no child tasks, we'll return an error. + * If there are child tasks, but they are not finished, we'll return a "continue" response, which will make the task wait for X seconds before checking again. + */ + // + else if (state === ExportContentEntriesControllerState.entryExport) { + if (!input.contentEntriesTaskId) { + return response.error({ + message: `Missing "contentEntriesTaskId" in the input, but the input notes that the task is in "entryExport" state. This should not happen.`, + code: "MISSING_CONTENT_ENTRIES_TASK_ID" + }); + } + entriesTask = await this.getEntriesTask(context, input.contentEntriesTaskId); + if (!entriesTask) { + return response.error({ + message: `Task "${input.contentEntriesTaskId}" not found.`, + code: "TASK_NOT_FOUND" + }); + } + if ( + entriesTask.taskStatus == TaskDataStatus.RUNNING || + entriesTask.taskStatus === TaskDataStatus.PENDING + ) { + return response.continue(input, { + seconds: backOffSeconds + }); + } else if (entriesTask.taskStatus === TaskDataStatus.FAILED) { + return response.error({ + message: `Failed to export content entries. Task "${entriesTask.id}" failed.`, + code: "EXPORT_ENTRIES_FAILED" + }); + } else if (entriesTask.taskStatus === TaskDataStatus.ABORTED) { + return response.error({ + message: `Export content entries process was aborted. Task "${entriesTask.id}" was aborted.`, + code: "EXPORT_ENTRIES_ABORTED" + }); + } else if (!entriesTask.output) { + return response.error({ + message: `No output found on task "${entriesTask.id}". Stopping export process.`, + code: "NO_OUTPUT" + }); + } + /** + * Possibly the task does not require any assets to be exported. + */ + if (!input.exportAssets || entriesTask.output.files.length === 0) { + const files: IExportContentEntriesControllerOutputFile[] = []; + for (const file of entriesTask.output.files) { + files.push({ + key: file.key, + checksum: file.checksum, + type: CmsImportExportFileType.ENTRIES + }); + } + + const output: IExportContentEntriesControllerOutput = { + files, + model: prepareExportModel(model) + }; + return response.done("Export done, without assets.", output as O); + } + + const assetTask = await trigger({ + definition: EXPORT_CONTENT_ASSETS_TASK, + input: { + prefix, + modelId: model.modelId, + limit: input.limit, + where: input.where, + sort: input.sort, + entryAfter: undefined, + fileAfter: undefined + }, + name: `Export Content Assets ${taskId}` + }); + + return response.continue( + { + ...input, + contentAssetsTaskId: assetTask.id, + state: ExportContentEntriesControllerState.assetsExport + }, + { + seconds: backOffSeconds + } + ); + } + /** + * If the state is "assetsExport", we need to check if there are any child tasks of the "Export Content Assets" task. + * If there are, we need to wait for them to finish before we can proceed. + * If there are no child tasks, we'll return as done. + * If there are child tasks, but they are not finished, we'll return a "continue" response, which will make the task wait for X seconds before checking again. + */ + // + else if (state === ExportContentEntriesControllerState.assetsExport) { + if (!input.contentEntriesTaskId) { + return response.error({ + message: `Missing "contentEntriesTaskId" in the input, but the input notes that the task is in "assetsExport" state. This should not happen.`, + code: "MISSING_CONTENT_ENTRIES_TASK_ID" + }); + } else if (!input.contentAssetsTaskId) { + return response.error({ + message: `Missing "contentAssetsTaskId" in the input, but the input notes that the task is in "assetsExport" state. This should not happen.`, + code: "MISSING_CONTENT_ASSETS_TASK_ID" + }); + } + + const assetsTask = await this.getAssetsTask(context, input.contentAssetsTaskId); + if (!assetsTask) { + return response.error({ + message: `Task "${input.contentAssetsTaskId}" not found.`, + code: "TASK_NOT_FOUND" + }); + } + if ( + assetsTask.taskStatus == TaskDataStatus.RUNNING || + assetsTask.taskStatus === TaskDataStatus.PENDING + ) { + return response.continue( + { + ...input + }, + { + seconds: backOffSeconds + } + ); + } else if (assetsTask.taskStatus === TaskDataStatus.FAILED) { + return response.error({ + message: `Failed to export content assets. Task "${assetsTask.id}" failed.`, + code: "EXPORT_ASSETS_FAILED" + }); + } else if (assetsTask.taskStatus === TaskDataStatus.ABORTED) { + return response.error({ + message: `Export content assets process was aborted. Task "${assetsTask.id}" was aborted.`, + code: "EXPORT_ASSETS_ABORTED" + }); + } + + entriesTask = await this.getEntriesTask(context, input.contentEntriesTaskId); + + const files: IExportContentEntriesControllerOutputFile[] = []; + const entriesFiles = entriesTask?.output?.files || []; + for (const file of entriesFiles) { + files.push({ + key: file.key, + checksum: file.checksum, + type: CmsImportExportFileType.ENTRIES + }); + } + const assetFiles = assetsTask.output?.files || []; + for (const file of assetFiles) { + files.push({ + key: file.key, + checksum: file.checksum, + type: CmsImportExportFileType.ASSETS + }); + } + + const output: IExportContentEntriesControllerOutput = { + model: prepareExportModel(model), + files + }; + + return response.done("Export done, with assets.", output as O); + } + + return response.error({ + message: `Invalid state "${state}".`, + code: "INVALID_STATE" + }); + } + + private async getEntriesTask(context: Context, id: string) { + try { + const result = await context.tasks.getTask< + IExportContentEntriesInput, + IExportContentEntriesOutput + >(id); + if (result?.definitionId === EXPORT_CONTENT_ENTRIES_TASK) { + return result; + } + return null; + } catch (ex) { + return null; + } + } + + private async getAssetsTask(context: Context, id: string) { + try { + const result = await context.tasks.getTask< + IExportContentAssetsInput, + IExportContentAssetsOutput + >(id); + if (result?.definitionId == EXPORT_CONTENT_ASSETS_TASK) { + return result; + } + return null; + } catch (ex) { + return null; + } + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/ImportFromUrlController.ts b/packages/api-headless-cms-import-export/src/tasks/domain/ImportFromUrlController.ts new file mode 100644 index 00000000000..213ac2f1665 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/ImportFromUrlController.ts @@ -0,0 +1,110 @@ +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import type { + IImportFromUrlController, + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; +import { IImportFromUrlControllerInputStep } from "~/tasks/domain/abstractions/ImportFromUrlController"; +import type { Context } from "~/types"; +import { ImportFromUrlControllerDownloadStep } from "~/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerDownloadStep"; +import { ImportFromUrlControllerProcessEntriesStep } from "./importFromUrlControllerSteps/ImportFromUrlControllerProcessEntriesStep"; +import { ImportFromUrlControllerProcessAssetsStep } from "./importFromUrlControllerSteps/ImportFromUrlControllerProcessAssetsStep"; + +const getDefaultStepValues = () => { + return { + files: [], + triggered: false, + finished: false, + done: [], + failed: [], + invalid: [], + aborted: [] + }; +}; + +export class ImportFromUrlController< + C extends Context = Context, + I extends IImportFromUrlControllerInput = IImportFromUrlControllerInput, + O extends IImportFromUrlControllerOutput = IImportFromUrlControllerOutput +> implements IImportFromUrlController +{ + public async run(params: ITaskRunParams): Promise> { + const { context, response, input } = params; + + if (!input.modelId) { + return response.error({ + message: `Missing "modelId" in the input.`, + code: "MISSING_MODEL_ID" + }); + } else if (Array.isArray(input.files) === false || input.files.length === 0) { + return response.error({ + message: `No files found in the provided data.`, + code: "NO_FILES_FOUND" + }); + } + + try { + await context.cms.getModel(input.modelId); + } catch (ex) { + return response.error({ + message: `Model "${input.modelId}" not found.`, + code: "MODEL_NOT_FOUND" + }); + } + + const steps = input.steps || {}; + + const downloadStep = + steps[IImportFromUrlControllerInputStep.DOWNLOAD] || getDefaultStepValues(); + if (!downloadStep.done) { + const step = new ImportFromUrlControllerDownloadStep(); + return await step.execute(params); + } else if (downloadStep.failed.length) { + return response.error({ + message: `Failed to download files.`, + code: "FAILED_DOWNLOADING_FILES", + data: steps + }); + } + + const processEntriesStep = + steps[IImportFromUrlControllerInputStep.PROCESS_ENTRIES] || getDefaultStepValues(); + if (!processEntriesStep.done) { + const step = new ImportFromUrlControllerProcessEntriesStep(); + return await step.execute(params); + } else if (processEntriesStep.failed.length) { + return response.error({ + message: `Failed to process entries.`, + code: "FAILED_PROCESSING_ENTRIES", + data: steps + }); + } + + const processAssetsStep = + steps[IImportFromUrlControllerInputStep.PROCESS_ASSETS] || getDefaultStepValues(); + if (!processAssetsStep.done) { + const step = new ImportFromUrlControllerProcessAssetsStep(); + return await step.execute(params); + } else if (processAssetsStep.failed.length) { + return response.error({ + message: `Failed to process assets.`, + code: "FAILED_PROCESSING_ASSETS", + data: steps + }); + } + + const files = downloadStep.files + .concat(processEntriesStep.files) + .concat(processAssetsStep.files); + + const output: IImportFromUrlControllerOutput = { + files, + done: [], + invalid: [], + failed: [], + aborted: [] + }; + + return response.done(output as O); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/ImportFromUrlDownload.ts b/packages/api-headless-cms-import-export/src/tasks/domain/ImportFromUrlDownload.ts new file mode 100644 index 00000000000..90d7c50c4ba --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/ImportFromUrlDownload.ts @@ -0,0 +1,116 @@ +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks/types"; +import type { + IImportFromUrlDownload, + IImportFromUrlDownloadInput, + IImportFromUrlDownloadOutput +} from "~/tasks/domain/abstractions/ImportFromUrlDownload"; +import type { Context } from "~/types"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import type { IMultipartUploadFactoryContinueParams } from "~/tasks/utils/upload"; +import { createMultipartUpload, createMultipartUploadFactory } from "~/tasks/utils/upload"; +import { prependImportPath } from "~/tasks/utils/helpers/importPath"; +import type { IDownloadFileFromUrlProcessResponseType } from "./downloadFileFromUrl"; +import { createDownloadFileFromUrl } from "./downloadFileFromUrl"; + +type ProcessType = IDownloadFileFromUrlProcessResponseType<"continue" | "aborted">; + +export class ImportFromUrlDownload< + C extends Context = Context, + I extends IImportFromUrlDownloadInput = IImportFromUrlDownloadInput, + O extends IImportFromUrlDownloadOutput = IImportFromUrlDownloadOutput +> implements IImportFromUrlDownload +{ + public async run(params: ITaskRunParams): Promise> { + const { context, response, input, isCloseToTimeout, isAborted } = params; + + if (!input.modelId) { + return response.error({ + message: `Missing "modelId" in the input.`, + code: "MISSING_MODEL_ID" + }); + } else if (!input.file) { + return response.error({ + message: `No file found in the provided data.`, + code: "NO_FILE_FOUND" + }); + } + + try { + await context.cms.getModel(input.modelId); + } catch (ex) { + return response.error({ + message: `Model "${input.modelId}" not found.`, + code: "MODEL_NOT_FOUND" + }); + } + + const client = createS3Client(); + + const filename = prependImportPath(input.file.key); + const uploadFactory = createMultipartUploadFactory({ + client, + bucket: getBucket(), + filename, + createHandler: createMultipartUpload + }); + + const uploadParams: IMultipartUploadFactoryContinueParams = { + uploadId: input.uploadId + }; + const upload = await uploadFactory.start(uploadParams); + + const download = createDownloadFileFromUrl({ + fetch, + file: { + url: input.file.get, + size: input.file.size, + key: input.file.key + }, + nextRange: input.nextRange, + upload + }); + let result: ProcessType; + try { + result = await download.process(async ({ stop }) => { + const isClose = isCloseToTimeout(); + if (isClose) { + return stop("continue"); + } else if (isAborted()) { + return stop("aborted"); + } + }); + } catch (ex) { + return response.error(ex); + } + + switch (result) { + case "aborted": + await upload.abort(); + return response.aborted(); + case "continue": + const continueValue: I = { + ...input, + uploadId: upload.getUploadId(), + done: download.isDone(), + nextRange: download.getNextRange() + }; + return response.continue({ + ...continueValue + }); + case "done": + const output: IImportFromUrlDownloadOutput = { + file: filename + }; + return response.done(output as O); + /** + * There should be nothing else other than "continue" or "aborted" or null. + */ + default: + await upload.abort(); + return response.error({ + message: `Method not implemented. Result: ${result}` + }); + } + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentAssets.ts b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentAssets.ts new file mode 100644 index 00000000000..b0e9dcff88d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentAssets.ts @@ -0,0 +1,40 @@ +import type { CmsEntryListSort, CmsEntryListWhere } from "@webiny/api-headless-cms/types"; +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; +import type { Context } from "~/types"; + +export interface IExportContentAssetsInputFile { + readonly key: string; + readonly checksum: string; +} + +export interface IExportContentAssetsInput { + modelId: string; + prefix: string; + limit?: number; + where?: CmsEntryListWhere; + sort?: CmsEntryListSort; + entryAfter: string | undefined; + fileAfter: string | undefined; + files?: IExportContentAssetsInputFile[]; +} + +export interface IExportContentAssetsOutputFile { + readonly key: string; + readonly checksum: string; +} + +export interface IExportContentAssetsOutput extends ITaskResponseDoneResultOutput { + files: IExportContentAssetsOutputFile[]; +} + +export interface IExportContentAssets< + C extends Context = Context, + I extends IExportContentAssetsInput = IExportContentAssetsInput, + O extends IExportContentAssetsOutput = IExportContentAssetsOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentEntries.ts b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentEntries.ts new file mode 100644 index 00000000000..71d6201ddf4 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentEntries.ts @@ -0,0 +1,42 @@ +import type { CmsEntryListSort, CmsEntryListWhere } from "@webiny/api-headless-cms/types"; +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; +import type { Context } from "~/types"; + +export interface IExportContentEntriesInputFile { + readonly key: string; + readonly checksum: string; +} + +export interface IExportContentEntriesInput { + modelId: string; + prefix: string; + exportAssets: boolean; + limit?: number; + where?: CmsEntryListWhere; + sort?: CmsEntryListSort; + after?: string; + combine?: boolean; + lastFileProcessed?: string; + files?: IExportContentEntriesInputFile[]; +} + +export interface IExportContentEntriesOutputFile { + readonly key: string; + readonly checksum: string; +} + +export interface IExportContentEntriesOutput extends ITaskResponseDoneResultOutput { + files: IExportContentEntriesOutputFile[]; +} + +export interface IExportContentEntries< + C extends Context = Context, + I extends IExportContentEntriesInput = IExportContentEntriesInput, + O extends IExportContentEntriesOutput = IExportContentEntriesOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentEntriesController.ts b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentEntriesController.ts new file mode 100644 index 00000000000..1f23e0bb877 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ExportContentEntriesController.ts @@ -0,0 +1,52 @@ +import type { + CmsEntryListSort, + CmsEntryListWhere, + CmsModelField +} from "@webiny/api-headless-cms/types"; +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; +import type { CmsImportExportFileType, Context } from "~/types"; + +export enum ExportContentEntriesControllerState { + entryExport = "entryExport", + assetsExport = "assetsExport" +} + +export interface IExportContentEntriesControllerInput { + modelId: string; + exportAssets: boolean; + contentEntriesTaskId?: string; + contentAssetsTaskId?: string; + prefix?: string; + limit?: number; + where?: CmsEntryListWhere; + sort?: CmsEntryListSort; + state?: ExportContentEntriesControllerState; +} + +export interface IExportContentEntriesControllerOutputFile { + readonly key: string; + readonly checksum: string; + readonly type: CmsImportExportFileType; +} + +export interface IExportedCmsModel { + modelId: string; + fields: CmsModelField[]; +} + +export interface IExportContentEntriesControllerOutput extends ITaskResponseDoneResultOutput { + model: IExportedCmsModel; + files: IExportContentEntriesControllerOutputFile[]; +} + +export interface IExportContentEntriesController< + C extends Context = Context, + I extends IExportContentEntriesControllerInput = IExportContentEntriesControllerInput, + O extends IExportContentEntriesControllerOutput = IExportContentEntriesControllerOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ImportFromUrlController.ts b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ImportFromUrlController.ts new file mode 100644 index 00000000000..ea93fa7f0f3 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ImportFromUrlController.ts @@ -0,0 +1,75 @@ +import type { + Context, + ICmsImportExportValidatedAssetsFile, + ICmsImportExportValidatedContentEntriesFile +} from "~/types"; +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; +import type { NonEmptyArray } from "@webiny/api/types"; + +export enum IImportFromUrlControllerInputStep { + DOWNLOAD = "download", + PROCESS_ENTRIES = "processEntries", + PROCESS_ASSETS = "processAssets" +} + +export interface IImportFromUrlControllerInputSteps { + [IImportFromUrlControllerInputStep.DOWNLOAD]?: { + files: string[]; + triggered: boolean; + finished: boolean; + done: string[]; + failed: string[]; + invalid: string[]; + aborted: string[]; + }; + [IImportFromUrlControllerInputStep.PROCESS_ENTRIES]?: { + files: string[]; + triggered: boolean; + finished: boolean; + done: string[]; + failed: string[]; + invalid: string[]; + aborted: string[]; + }; + [IImportFromUrlControllerInputStep.PROCESS_ASSETS]?: { + files: string[]; + triggered: boolean; + finished: boolean; + done: string[]; + failed: string[]; + invalid: string[]; + aborted: string[]; + }; +} + +export interface IImportFromUrlControllerInput { + modelId: string; + files: NonEmptyArray< + ICmsImportExportValidatedContentEntriesFile | ICmsImportExportValidatedAssetsFile + >; + maxInsertErrors: number | undefined; + steps: IImportFromUrlControllerInputSteps; +} + +export interface IImportFromUrlControllerOutput extends ITaskResponseDoneResultOutput { + /** + * Should contain all local files created by the import process. + */ + files: string[]; + done: string[]; + invalid: string[]; + aborted: string[]; + failed: string[]; +} + +export interface IImportFromUrlController< + C extends Context = Context, + I extends IImportFromUrlControllerInput = IImportFromUrlControllerInput, + O extends IImportFromUrlControllerOutput = IImportFromUrlControllerOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ImportFromUrlDownload.ts b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ImportFromUrlDownload.ts new file mode 100644 index 00000000000..82963c48d9b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ImportFromUrlDownload.ts @@ -0,0 +1,26 @@ +import type { Context, ICmsImportExportValidatedValidFile } from "~/types"; +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; + +export interface IImportFromUrlDownloadInput { + modelId: string; + file: ICmsImportExportValidatedValidFile; + nextRange?: number; + done?: boolean; + uploadId?: string; +} + +export interface IImportFromUrlDownloadOutput extends ITaskResponseDoneResultOutput { + file: string; +} + +export interface IImportFromUrlDownload< + C extends Context = Context, + I extends IImportFromUrlDownloadInput = IImportFromUrlDownloadInput, + O extends IImportFromUrlDownloadOutput = IImportFromUrlDownloadOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ValidateImportFromUrl.ts b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ValidateImportFromUrl.ts new file mode 100644 index 00000000000..50ccd4b1f9b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/abstractions/ValidateImportFromUrl.ts @@ -0,0 +1,27 @@ +import type { Context, ICmsImportExportFile, ICmsImportExportValidatedFile } from "~/types"; +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; +import type { NonEmptyArray } from "@webiny/api/types"; +import type { IExportedCmsModel } from "~/tasks/domain/abstractions/ExportContentEntriesController"; + +export interface IValidateImportFromUrlInput { + files: NonEmptyArray; + model: IExportedCmsModel; +} + +export interface IValidateImportFromUrlOutput extends ITaskResponseDoneResultOutput { + modelId: string; + files: NonEmptyArray; + importTaskId?: string; +} + +export interface IValidateImportFromUrl< + C extends Context = Context, + I extends IValidateImportFromUrlInput = IValidateImportFromUrlInput, + O extends IValidateImportFromUrlOutput = IValidateImportFromUrlOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/createExportContentAssets.ts b/packages/api-headless-cms-import-export/src/tasks/domain/createExportContentAssets.ts new file mode 100644 index 00000000000..461b7b25ee3 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/createExportContentAssets.ts @@ -0,0 +1,53 @@ +import type { ICreateCmsAssetsZipperCallable } from "./exportContentAssets/ExportContentAssets"; +import { ExportContentAssets } from "./exportContentAssets/ExportContentAssets"; +import { createS3Client } from "@webiny/aws-sdk/client-s3"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { CmsAssetsZipper } from "../utils/cmsAssetsZipper"; +import { createUploadFactory } from "~/tasks/utils/upload"; +import { createArchiver } from "~/tasks/utils/archiver"; +import { Zipper } from "~/tasks/utils/zipper"; +import { WEBINY_EXPORT_ASSETS_EXTENSION } from "~/tasks/constants"; + +export const createExportContentAssets = () => { + const client = createS3Client(); + const bucket = getBucket(); + const createUpload = createUploadFactory({ + client, + bucket + }); + + const createCmsAssetsZipper: ICreateCmsAssetsZipperCallable = config => { + const upload = createUpload(config.filename); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true, + /** + * No point in compressing the assets, since they are already compressed. + */ + gzipOptions: { + level: 0 + }, + comment: WEBINY_EXPORT_ASSETS_EXTENSION + } + }); + + const zipper = new Zipper({ + upload, + archiver + }); + + return new CmsAssetsZipper({ + entryFetcher: config.entryFetcher, + createEntryAssets: config.createEntryAssets, + createEntryAssetsResolver: config.createEntryAssetsResolver, + fileFetcher: config.fileFetcher, + zipper + }); + }; + + return new ExportContentAssets({ + createCmsAssetsZipper + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/createExportContentEntries.ts b/packages/api-headless-cms-import-export/src/tasks/domain/createExportContentEntries.ts new file mode 100644 index 00000000000..6f967c92571 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/createExportContentEntries.ts @@ -0,0 +1,54 @@ +import { createS3Client } from "@webiny/aws-sdk/client-s3"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { ExportContentEntries } from "~/tasks/domain/exportContentEntries/ExportContentEntries"; +import type { ICreateCmsEntryZipperConfig } from "~/tasks/domain/exportContentEntries/ExportContentEntries"; +import { CmsEntryZipper } from "../utils/cmsEntryZipper"; +import { createUploadFactory } from "~/tasks/utils/upload"; +import { createArchiver } from "~/tasks/utils/archiver"; +import { Zipper } from "~/tasks/utils/zipper"; +import { EntryAssets } from "../utils/entryAssets"; +import { UniqueResolver } from "~/tasks/utils/uniqueResolver/UniqueResolver"; +import { WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; + +export const createExportContentEntries = () => { + const client = createS3Client(); + const bucket = getBucket(); + const createUpload = createUploadFactory({ + client, + bucket + }); + + const createCmsEntryZipper = (config: ICreateCmsEntryZipperConfig) => { + const upload = createUpload(config.filename); + + const archiver = createArchiver({ + format: "zip", + options: { + gzip: true, + gzipOptions: { + level: 9 + }, + comment: WEBINY_EXPORT_ENTRIES_EXTENSION + } + }); + + const zipper = new Zipper({ + upload, + archiver + }); + + return new CmsEntryZipper({ + fetcher: config.fetcher, + zipper, + entryAssets: new EntryAssets({ + uniqueResolver: new UniqueResolver(), + traverser: config.traverser + }), + uniqueAssetsResolver: new UniqueResolver() + }); + }; + + return new ExportContentEntries({ + createCmsEntryZipper + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/createImportFromUrlProcessAssets.ts b/packages/api-headless-cms-import-export/src/tasks/domain/createImportFromUrlProcessAssets.ts new file mode 100644 index 00000000000..defe661db33 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/createImportFromUrlProcessAssets.ts @@ -0,0 +1,46 @@ +import { Context } from "~/types"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { ImportFromUrlProcessAssets } from "./importFromUrlProcessAssets/ImportFromUrlProcessAssets"; +import type { + IImportFromUrlProcessAssets, + IImportFromUrlProcessAssetsInput, + IImportFromUrlProcessAssetsOutput +} from "./importFromUrlProcessAssets/abstractions/ImportFromUrlProcessAssets"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { createCompressedFileReader, createDecompressor } from "~/tasks/utils/decompressor"; +import { createMultipartUpload, createMultipartUploadFactory } from "~/tasks/utils/upload"; +import { FileFetcher } from "~/tasks/utils/fileFetcher"; + +export const createImportFromUrlProcessAssets = < + C extends Context = Context, + I extends IImportFromUrlProcessAssetsInput = IImportFromUrlProcessAssetsInput, + O extends IImportFromUrlProcessAssetsOutput = IImportFromUrlProcessAssetsOutput +>(): IImportFromUrlProcessAssets => { + const client = createS3Client(); + const bucket = getBucket(); + const reader = createCompressedFileReader({ + client, + bucket + }); + const decompressor = createDecompressor({ + createUploadFactory: filename => { + return createMultipartUploadFactory({ + filename, + client, + bucket, + createHandler: createMultipartUpload + }); + } + }); + + const fileFetcher = new FileFetcher({ + client, + bucket + }); + + return new ImportFromUrlProcessAssets({ + fileFetcher, + reader, + decompressor + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/createImportFromUrlProcessEntries.ts b/packages/api-headless-cms-import-export/src/tasks/domain/createImportFromUrlProcessEntries.ts new file mode 100644 index 00000000000..5037ea8f775 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/createImportFromUrlProcessEntries.ts @@ -0,0 +1,47 @@ +import { Context } from "~/types"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { ImportFromUrlProcessEntries } from "./importFromUrlProcessEntries/ImportFromUrlProcessEntries"; +import type { + IImportFromUrlProcessEntries, + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesOutput +} from "./importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntries"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { createCompressedFileReader, createDecompressor } from "~/tasks/utils/decompressor"; +import { createMultipartUpload, createMultipartUploadFactory } from "~/tasks/utils/upload"; +import { FileFetcher } from "~/tasks/utils/fileFetcher"; + +export const createImportFromUrlProcessEntries = < + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +>(): IImportFromUrlProcessEntries => { + const client = createS3Client(); + const bucket = getBucket(); + + const reader = createCompressedFileReader({ + client, + bucket + }); + const decompressor = createDecompressor({ + createUploadFactory: filename => { + return createMultipartUploadFactory({ + filename, + client, + bucket, + createHandler: createMultipartUpload + }); + } + }); + + const fileFetcher = new FileFetcher({ + client, + bucket + }); + + return new ImportFromUrlProcessEntries({ + fileFetcher, + reader, + decompressor + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/createValidateImportFromUrl.ts b/packages/api-headless-cms-import-export/src/tasks/domain/createValidateImportFromUrl.ts new file mode 100644 index 00000000000..cfd5ce108b1 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/createValidateImportFromUrl.ts @@ -0,0 +1,24 @@ +import { ValidateImportFromUrl } from "~/tasks/domain/validateImportFromUrl/ValidateImportFromUrl"; +import { ExternalFileFetcher } from "~/tasks/utils/externalFileFetcher"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { FileFetcher } from "~/tasks/utils/fileFetcher"; + +export const createValidateImportFromUrl = () => { + const fileFetcher = new ExternalFileFetcher({ + fetcher: fetch, + getChecksumHeader: headers => (headers.get("etag") || "").replaceAll('"', "") + }); + + const internalFileFetcher = new FileFetcher({ + client: createS3Client(), + bucket: getBucket() + }); + + return new ValidateImportFromUrl({ + fileFetcher, + fileExists: async key => { + return internalFileFetcher.exists(key); + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/DownloadFileFromUrl.ts b/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/DownloadFileFromUrl.ts new file mode 100644 index 00000000000..2bbb7e39325 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/DownloadFileFromUrl.ts @@ -0,0 +1,114 @@ +import type { IMultipartUploadHandler } from "~/tasks/utils/upload"; +import type { + IDownloadFileFromUrl, + IDownloadFileFromUrlProcessOnIterationCallable, + IDownloadFileFromUrlProcessResponseType +} from "./abstractions/DownloadFileFromUrl"; +import { createSizeSegments } from "~/tasks/utils/helpers/sizeSegments"; + +export interface IDownloadFileFromUrlFile { + url: string; + size: number; + key: string; +} + +interface IRange { + start: number; + end: number; +} + +export interface IDownloadFileFromUrlParams { + file: IDownloadFileFromUrlFile; + fetch: typeof fetch; + upload: IMultipartUploadHandler; + nextRange?: number; +} + +export class DownloadFileFromUrl implements IDownloadFileFromUrl { + private readonly file: IDownloadFileFromUrlFile; + private readonly upload: IMultipartUploadHandler; + private readonly fetch: typeof fetch; + private nextRange: number; + private readonly ranges: IRange[]; + + public constructor(params: IDownloadFileFromUrlParams) { + this.file = params.file; + this.fetch = params.fetch; + this.nextRange = params.nextRange || 0; + this.upload = params.upload; + this.ranges = createSizeSegments(this.file.size, "10MB"); + } + + public async process( + onIteration: IDownloadFileFromUrlProcessOnIterationCallable + ): Promise> { + let iteration = 0; + + while (true) { + const next = this.ranges[this.nextRange]; + + if (this.isDone() || !next) { + await this.upload.complete(); + return "done"; + } + let status: IDownloadFileFromUrlProcessResponseType | undefined = undefined; + await onIteration({ + iteration, + next: this.nextRange, + segment: next, + stop: input => { + status = input; + } + }); + if (status === "done") { + await this.upload.complete(); + return status; + } else if (status === "aborted") { + await this.upload.abort(); + return status; + } else if (status === "continue") { + return status; + } + iteration++; + + const headers = new Headers(); + + if (this.ranges.length > 1) { + headers.set("Range", `bytes=${next.start}-${next.end}`); + } + const result = await this.fetch(this.file.url, { + method: "GET", + keepalive: true, + mode: "cors", + headers + }); + if (!result.ok) { + throw new Error(`Failed to fetch URL: ${this.file.url}`); + } else if (!result.body) { + throw new Error(`Body not found for URL: ${this.file.url}`); + } + const buffer = await result.arrayBuffer(); + + await this.upload.add(Buffer.from(buffer)); + this.nextRange++; + } + } + + public async abort(): Promise { + await this.upload.abort(); + } + + public getNextRange(): number { + return this.nextRange; + } + + public isDone(): boolean { + return !this.ranges[this.nextRange]; + } +} + +export const createDownloadFileFromUrl = ( + params: IDownloadFileFromUrlParams +): IDownloadFileFromUrl => { + return new DownloadFileFromUrl(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/abstractions/DownloadFileFromUrl.ts b/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/abstractions/DownloadFileFromUrl.ts new file mode 100644 index 00000000000..0f392e792a5 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/abstractions/DownloadFileFromUrl.ts @@ -0,0 +1,24 @@ +export interface IDownloadFileRange { + start: number; + end: number; +} + +export interface IDownloadFileFromUrlProcessOnIterationCallableParams { + iteration: number; + next: number; + stop: (status: T) => void; + segment: IDownloadFileRange; +} +export interface IDownloadFileFromUrlProcessOnIterationCallable { + (params: IDownloadFileFromUrlProcessOnIterationCallableParams): Promise; +} + +export type IDownloadFileFromUrlProcessResponseType = T | "done"; + +export interface IDownloadFileFromUrl { + process( + onIteration: IDownloadFileFromUrlProcessOnIterationCallable + ): Promise>; + getNextRange(): number; + isDone(): boolean; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/index.ts b/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/index.ts new file mode 100644 index 00000000000..37208c15687 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/downloadFileFromUrl/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/DownloadFileFromUrl"; +export * from "./DownloadFileFromUrl"; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/exportContentAssets/ExportContentAssets.ts b/packages/api-headless-cms-import-export/src/tasks/domain/exportContentAssets/ExportContentAssets.ts new file mode 100644 index 00000000000..db706d2a3e6 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/exportContentAssets/ExportContentAssets.ts @@ -0,0 +1,215 @@ +import type { Context } from "~/types"; +import type { + IExportContentAssets, + IExportContentAssetsInput, + IExportContentAssetsOutput +} from "~/tasks/domain/abstractions/ExportContentAssets"; +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import type { ICmsEntryFetcher } from "~/tasks/utils/cmsEntryFetcher"; +import { createCmsEntryFetcher } from "~/tasks/utils/cmsEntryFetcher"; +import type { IEntryAssets, IEntryAssetsResolver } from "~/tasks/utils/entryAssets"; +import { EntryAssets, EntryAssetsResolver } from "~/tasks/utils/entryAssets"; +import type { + ICmsAssetsZipper, + ICmsAssetsZipperExecuteResult +} from "~/tasks/utils/cmsAssetsZipper"; +import { + CmsAssetsZipperExecuteContinueResult, + CmsAssetsZipperExecuteContinueWithoutResult, + CmsAssetsZipperExecuteDoneResult, + CmsAssetsZipperExecuteDoneWithoutResult +} from "~/tasks/utils/cmsAssetsZipper"; +import type { IFileFetcher } from "~/tasks/utils/fileFetcher"; +import { FileFetcher } from "~/tasks/utils/fileFetcher"; +import type { CmsModel } from "@webiny/api-headless-cms/types"; +import { getErrorProperties } from "@webiny/tasks/utils"; +import { getBucket } from "~/tasks/utils/helpers/getBucket"; +import { createS3Client } from "~/tasks/utils/helpers/s3Client"; +import { UniqueResolver } from "~/tasks/utils/uniqueResolver/UniqueResolver"; +import { WEBINY_EXPORT_ASSETS_EXTENSION } from "~/tasks/constants"; + +export interface ICreateCmsAssetsZipperCallableConfig { + filename: string; + entryFetcher: ICmsEntryFetcher; + createEntryAssets: () => IEntryAssets; + createEntryAssetsResolver: () => IEntryAssetsResolver; + fileFetcher: IFileFetcher; +} + +export interface ICreateCmsAssetsZipperCallable { + (config: ICreateCmsAssetsZipperCallableConfig): ICmsAssetsZipper; +} + +const getFilename = (input: IExportContentAssetsInput): string => { + const current = [input.entryAfter, input.fileAfter] + .filter(item => item !== undefined) + .join("-"); + + return `${input.prefix}/assets${ + current ? `-${current}` : "" + }.${WEBINY_EXPORT_ASSETS_EXTENSION}`; +}; + +export interface IExportContentAssetsParams { + createCmsAssetsZipper: ICreateCmsAssetsZipperCallable; +} + +export class ExportContentAssets< + C extends Context = Context, + I extends IExportContentAssetsInput = IExportContentAssetsInput, + O extends IExportContentAssetsOutput = IExportContentAssetsOutput +> implements IExportContentAssets +{ + private readonly createCmsAssetsZipper: ICreateCmsAssetsZipperCallable; + + public constructor(params: IExportContentAssetsParams) { + this.createCmsAssetsZipper = params.createCmsAssetsZipper; + } + + public async run(params: ITaskRunParams): Promise> { + const { response, context, input, isCloseToTimeout, isAborted } = params; + + let model: CmsModel; + try { + model = await context.cms.getModel(input.modelId); + } catch (ex) { + return response.error({ + message: `Could not fetch entry manager for model "${input.modelId}".`, + code: "MODEL_NOT_FOUND", + data: { + error: getErrorProperties(ex) + } + }); + } + + const traverser = await context.cms.getEntryTraverser(model.modelId); + + const entryFetcher = createCmsEntryFetcher(async after => { + const input = { + where: params.input.where, + limit: params.input.limit || 10000, + sort: params.input.sort, + after + }; + const [items, meta] = await context.cms.listLatestEntries(model, input); + + return { + items, + meta + }; + }); + + const fileFetcher = new FileFetcher({ + client: createS3Client(), + bucket: getBucket() + }); + + const filename = getFilename(input); + + const zipper = this.createCmsAssetsZipper({ + filename, + fileFetcher, + entryFetcher, + createEntryAssets: () => { + return new EntryAssets({ + traverser, + uniqueResolver: new UniqueResolver() + }); + }, + createEntryAssetsResolver: () => { + return new EntryAssetsResolver({ + fetchFiles: async params => { + const [items, meta] = await context.fileManager.listFiles(params); + return { + items, + meta + }; + } + }); + } + }); + + let result: ICmsAssetsZipperExecuteResult; + + try { + result = await zipper.execute({ + fileAfter: input.fileAfter, + entryAfter: input.entryAfter, + isAborted() { + return isAborted(); + }, + isCloseToTimeout(seconds?: number) { + return isCloseToTimeout(seconds); + } + }); + } catch (ex) { + return response.error(ex); + } + + const files = Array.isArray(input.files) ? input.files : []; + /** + * Zipper is done, but there is no result? + * We will output existing input files. + */ + if (result instanceof CmsAssetsZipperExecuteDoneWithoutResult) { + return response.done({ + files + } as O); + } + /** + * Zipper is done and there is a result? + * We will output existing input files and the new file. + */ + // + else if (result instanceof CmsAssetsZipperExecuteDoneResult) { + return response.done({ + files: files.concat([ + { + key: result.key, + checksum: result.checksum + } + ]) + } as O); + } + /** + * Zipper is not done and there is no result? + * Let's continue with the next iteration. + */ + // + else if (result instanceof CmsAssetsZipperExecuteContinueWithoutResult) { + return response.continue({ + ...input, + fileAfter: result.fileCursor, + entryAfter: result.entryCursor + }); + } + /** + * Zipper is not done and there is a result? + * Let's merge the existing files with the new file and continue with the next iteration. + */ + // + else if (result instanceof CmsAssetsZipperExecuteContinueResult) { + return response.continue({ + ...input, + fileAfter: result.fileCursor, + entryAfter: result.entryCursor, + files: files.concat([ + { + key: result.key, + checksum: result.checksum + } + ]) + }); + } + + return response.error({ + message: "Unknown zipper result.", + code: "UNKNOWN_ZIPPER_RESULT", + data: { + type: typeof result, + constructor: result?.constructor?.name || "unknown constructor", + result + } + }); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/exportContentEntries/ExportContentEntries.ts b/packages/api-headless-cms-import-export/src/tasks/domain/exportContentEntries/ExportContentEntries.ts new file mode 100644 index 00000000000..014feaa6202 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/exportContentEntries/ExportContentEntries.ts @@ -0,0 +1,112 @@ +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import type { Context } from "~/types"; +import type { CmsModel } from "@webiny/api-headless-cms/types"; +import { getErrorProperties } from "@webiny/tasks/utils"; +import type { ICmsEntryZipper, ICmsEntryZipperConfig } from "~/tasks/utils/cmsEntryZipper"; +import { CmsEntryZipperExecuteContinueResult } from "~/tasks/utils/cmsEntryZipper"; +import type { + IExportContentEntries, + IExportContentEntriesInput, + IExportContentEntriesOutput +} from "~/tasks/domain/abstractions/ExportContentEntries"; +import { createCmsEntryFetcher } from "~/tasks/utils/cmsEntryFetcher/createCmsEntryFetcher"; +import type { IContentEntryTraverser } from "@webiny/api-headless-cms"; +import { WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; + +export interface IExportContentEntriesConfig { + createCmsEntryZipper(config: Pick): ICmsEntryZipper; +} + +export interface ICreateCmsEntryZipperConfig extends Pick { + filename: string; + model: Pick; + traverser: IContentEntryTraverser; +} + +export class ExportContentEntries< + C extends Context = Context, + I extends IExportContentEntriesInput = IExportContentEntriesInput, + O extends IExportContentEntriesOutput = IExportContentEntriesOutput +> implements IExportContentEntries +{ + private readonly createCmsEntryZipper: (config: ICreateCmsEntryZipperConfig) => ICmsEntryZipper; + + public constructor(config: IExportContentEntriesConfig) { + this.createCmsEntryZipper = config.createCmsEntryZipper; + } + + public async run(params: ITaskRunParams): Promise> { + const { context, response, input, isCloseToTimeout, isAborted } = params; + + const { prefix: basePrefix } = input; + + let model: CmsModel; + try { + model = await context.cms.getModel(input.modelId); + } catch (ex) { + return response.error({ + message: `Could not fetch entry manager for model "${input.modelId}".`, + code: "MODEL_NOT_FOUND", + data: { + error: getErrorProperties(ex) + } + }); + } + + const prefix = `${basePrefix}/entries`; + const fetcher = createCmsEntryFetcher(async after => { + const input = { + where: params.input.where, + limit: params.input.limit || 100000, + sort: params.input.sort, + after + }; + const [items, meta] = await context.cms.listLatestEntries(model, input); + + return { + items, + meta + }; + }); + + const filenamePrefix = [prefix, input.after].filter(Boolean).join("-"); + + const filename = `${filenamePrefix}.${WEBINY_EXPORT_ENTRIES_EXTENSION}`; + + const traverser = await context.cms.getEntryTraverser(model.modelId); + + const entryZipper = this.createCmsEntryZipper({ + filename, + model, + fetcher, + traverser + }); + + const result = await entryZipper.execute({ + isCloseToTimeout, + isAborted, + model, + after: input.after, + exportAssets: input.exportAssets + }); + + const files = (input.files || []).concat([ + { + key: result.key, + checksum: result.checksum + } + ]); + + if (result instanceof CmsEntryZipperExecuteContinueResult) { + return response.continue({ + ...input, + files, + after: result.cursor + }); + } + const output: IExportContentEntriesOutput = { + files + }; + return response.done(output as O); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerDownloadStep.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerDownloadStep.ts new file mode 100644 index 00000000000..4ec8a736d2d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerDownloadStep.ts @@ -0,0 +1,121 @@ +import type { ImportFromUrlControllerStep } from "./abstractions/ImportFromUrlControllerStep"; +import type { + IImportFromUrlDownloadInput, + IImportFromUrlDownloadOutput +} from "~/tasks/domain/abstractions/ImportFromUrlDownload"; +import { IMPORT_FROM_URL_DOWNLOAD_TASK } from "~/tasks/constants"; +import { getBackOffSeconds } from "~/tasks/utils/helpers/getBackOffSeconds"; +import type { Context } from "~/types"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; +import { IImportFromUrlControllerInputStep } from "~/tasks/domain/abstractions/ImportFromUrlController"; +import type { ITask, ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import { getChildTasks } from "./getChildTasks"; + +export class ImportFromUrlControllerDownloadStep< + C extends Context = Context, + I extends IImportFromUrlControllerInput = IImportFromUrlControllerInput, + O extends IImportFromUrlControllerOutput = IImportFromUrlControllerOutput +> implements ImportFromUrlControllerStep +{ + public async execute(params: ITaskRunParams): Promise> { + const { context, response, input, trigger, store } = params; + + const task = store.getTask() as ITask; + + const step = input.steps[IImportFromUrlControllerInputStep.DOWNLOAD]; + if (!step?.triggered) { + for (const file of input.files) { + await trigger({ + name: `Import From Url - Download`, + definition: IMPORT_FROM_URL_DOWNLOAD_TASK, + input: { + file, + modelId: input.modelId + } + }); + } + + const output: I = { + ...input, + steps: { + ...input.steps, + [IImportFromUrlControllerInputStep.DOWNLOAD]: { + ...step, + triggered: true + } + } + }; + + return response.continue(output, { + seconds: getBackOffSeconds(task.iterations) + }); + } else if (step.finished !== true) { + const { failed, running, invalid, aborted, done, collection } = await getChildTasks< + IImportFromUrlDownloadInput, + IImportFromUrlDownloadOutput + >({ + context, + task, + definition: IMPORT_FROM_URL_DOWNLOAD_TASK + }); + + /** + * If there are any running tasks, we should continue waiting. + */ + if (running.length > 0) { + return response.continue(input, { + seconds: getBackOffSeconds(task.iterations) + }); + } else if (collection.length === 0) { + return response.error({ + message: "No download tasks found. We are not continuing.", + code: "NO_DOWNLOAD_TASKS" + }); + } + + const files = collection + .map(item => { + return item.output?.file; + }) + .filter((file): file is string => { + return !!file; + }); + + const output: I = { + ...input, + steps: { + ...input.steps, + [IImportFromUrlControllerInputStep.DOWNLOAD]: { + ...step, + files, + failed, + invalid, + aborted, + done, + finished: true + } + } + }; + + if (failed.length > 0 || aborted.length > 0 || invalid.length > 0) { + return response.error({ + message: "Some download tasks failed.", + code: "DOWNLOAD_FAILED", + data: { + failed, + aborted, + invalid + } + }); + } + + return response.continue(output); + } + return response.error({ + message: "Impossible to get to this point. Fatal error." + }); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerProcessAssetsStep.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerProcessAssetsStep.ts new file mode 100644 index 00000000000..d3ce9f3cc35 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerProcessAssetsStep.ts @@ -0,0 +1,120 @@ +import type { Context } from "~/types"; +import { CmsImportExportFileType } from "~/types"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; +import { IImportFromUrlControllerInputStep } from "~/tasks/domain/abstractions/ImportFromUrlController"; +import type { ImportFromUrlControllerStep } from "~/tasks/domain/importFromUrlControllerSteps/abstractions/ImportFromUrlControllerStep"; +import type { ITask, ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import { prependImportPath } from "~/tasks/utils/helpers/importPath"; +import { getBackOffSeconds } from "~/tasks/utils/helpers/getBackOffSeconds"; +import { IMPORT_FROM_URL_PROCESS_ASSETS_TASK } from "~/tasks/constants"; +import { getChildTasks } from "~/tasks/domain/importFromUrlControllerSteps/getChildTasks"; +import type { IImportFromUrlProcessAssetsInput } from "../importFromUrlProcessAssets/abstractions/ImportFromUrlProcessAssets"; + +export class ImportFromUrlControllerProcessAssetsStep< + C extends Context = Context, + I extends IImportFromUrlControllerInput = IImportFromUrlControllerInput, + O extends IImportFromUrlControllerOutput = IImportFromUrlControllerOutput +> implements ImportFromUrlControllerStep +{ + public async execute(params: ITaskRunParams): Promise> { + const { response, input, trigger, store, context } = params; + + const task = store.getTask() as ITask; + + const step = input.steps[IImportFromUrlControllerInputStep.PROCESS_ASSETS]; + if (!step?.triggered) { + const files = input.files.filter(file => { + return file.type === CmsImportExportFileType.ASSETS; + }); + if (files.length === 0) { + const output: IImportFromUrlControllerOutput = { + error: { + message: "No assets files found.", + code: "NO_ASSETS_FILES" + }, + files: [], + aborted: [], + done: [], + failed: [], + invalid: [] + }; + return response.done(output as O); + } + const inputFiles: string[] = []; + for (const file of files) { + const key = prependImportPath(file.key); + await trigger({ + name: `Import From Url - Process assets`, + definition: IMPORT_FROM_URL_PROCESS_ASSETS_TASK, + input: { + file: { + key, + type: file.type + }, + maxInsertErrors: input.maxInsertErrors, + modelId: input.modelId + } + }); + inputFiles.push(key); + } + + const output: I = { + ...input, + steps: { + ...input.steps, + [IImportFromUrlControllerInputStep.PROCESS_ASSETS]: { + ...step, + triggered: true, + files: inputFiles + } + } + }; + + return response.continue(output, { + seconds: getBackOffSeconds(task.iterations) + }); + } else if (step.finished !== true) { + const { failed, running, invalid, aborted, collection, done } = await getChildTasks({ + context, + task, + definition: IMPORT_FROM_URL_PROCESS_ASSETS_TASK + }); + + /** + * If there are any running tasks, we should continue waiting. + */ + if (running.length > 0) { + return response.continue(input, { + seconds: getBackOffSeconds(task.iterations) + }); + } else if (collection.length === 0) { + return response.error({ + message: "No process assets tasks found. We are not continuing.", + code: "NO_PROCESS_ASSETS_TASKS" + }); + } + + const output: I = { + ...input, + steps: { + ...input.steps, + [IImportFromUrlControllerInputStep.PROCESS_ASSETS]: { + ...step, + failed, + invalid, + aborted, + done, + finished: true + } + } + }; + return response.continue(output); + } + return response.error({ + message: "Impossible to get to this point. Fatal error." + }); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerProcessEntriesStep.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerProcessEntriesStep.ts new file mode 100644 index 00000000000..0010e5adf33 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/ImportFromUrlControllerProcessEntriesStep.ts @@ -0,0 +1,120 @@ +import type { ImportFromUrlControllerStep } from "./abstractions/ImportFromUrlControllerStep"; +import { IMPORT_FROM_URL_PROCESS_ENTRIES_TASK } from "~/tasks/constants"; +import { getBackOffSeconds } from "~/tasks/utils/helpers/getBackOffSeconds"; +import type { Context } from "~/types"; +import { CmsImportExportFileType } from "~/types"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; +import { IImportFromUrlControllerInputStep } from "~/tasks/domain/abstractions/ImportFromUrlController"; +import type { ITask, ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import { getChildTasks } from "./getChildTasks"; +import type { IImportFromUrlProcessEntriesInput } from "../importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntries"; +import { prependImportPath } from "~/tasks/utils/helpers/importPath"; + +export class ImportFromUrlControllerProcessEntriesStep< + C extends Context = Context, + I extends IImportFromUrlControllerInput = IImportFromUrlControllerInput, + O extends IImportFromUrlControllerOutput = IImportFromUrlControllerOutput +> implements ImportFromUrlControllerStep +{ + public async execute(params: ITaskRunParams): Promise> { + const { context, response, input, trigger, store } = params; + + const task = store.getTask() as ITask; + + const step = input.steps[IImportFromUrlControllerInputStep.PROCESS_ENTRIES]; + if (!step?.triggered) { + const files = input.files.filter(file => { + return file.type === CmsImportExportFileType.ENTRIES; + }); + if (files.length === 0) { + const output: IImportFromUrlControllerOutput = { + error: { + message: "No entries files found.", + code: "NO_ENTRIES_FILES" + }, + files: [], + aborted: [], + done: [], + failed: [], + invalid: [] + }; + return response.done(output as O); + } + const inputFiles: string[] = []; + for (const file of files) { + const key = prependImportPath(file.key); + await trigger({ + name: `Import From Url - Process entries`, + definition: IMPORT_FROM_URL_PROCESS_ENTRIES_TASK, + input: { + file: { + key, + type: file.type + }, + maxInsertErrors: input.maxInsertErrors, + modelId: input.modelId + } + }); + inputFiles.push(key); + } + + const output: I = { + ...input, + steps: { + ...input.steps, + [IImportFromUrlControllerInputStep.PROCESS_ENTRIES]: { + ...step, + triggered: true, + files: inputFiles + } + } + }; + + return response.continue(output, { + seconds: getBackOffSeconds(task.iterations) + }); + } else if (step.finished !== true) { + const { failed, running, invalid, aborted, collection, done } = await getChildTasks({ + context, + task, + definition: IMPORT_FROM_URL_PROCESS_ENTRIES_TASK + }); + + /** + * If there are any running tasks, we should continue waiting. + */ + if (running.length > 0) { + return response.continue(input, { + seconds: getBackOffSeconds(task.iterations) + }); + } else if (collection.length === 0) { + return response.error({ + message: "No process entries tasks found. We are not continuing.", + code: "NO_PROCESS_ENTRIES_TASKS" + }); + } + + const output: I = { + ...input, + steps: { + ...input.steps, + [IImportFromUrlControllerInputStep.PROCESS_ENTRIES]: { + ...step, + failed, + invalid, + aborted, + done, + finished: true + } + } + }; + return response.continue(output); + } + return response.error({ + message: "Impossible to get to this point. Fatal error." + }); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/abstractions/ImportFromUrlControllerStep.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/abstractions/ImportFromUrlControllerStep.ts new file mode 100644 index 00000000000..37b25616346 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/abstractions/ImportFromUrlControllerStep.ts @@ -0,0 +1,14 @@ +import type { Context } from "~/types"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; + +export interface ImportFromUrlControllerStep< + C extends Context = Context, + I extends IImportFromUrlControllerInput = IImportFromUrlControllerInput, + O extends IImportFromUrlControllerOutput = IImportFromUrlControllerOutput +> { + execute(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/getChildTasks.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/getChildTasks.ts new file mode 100644 index 00000000000..e37a7187778 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlControllerSteps/getChildTasks.ts @@ -0,0 +1,60 @@ +import type { ITask, ITaskResponseDoneResultOutput } from "@webiny/tasks"; +import { TaskDataStatus } from "@webiny/tasks"; +import type { Context } from "~/types"; + +export interface IGetChildTasksParams { + context: Context; + task: ITask; + definition: string; +} + +export const getChildTasks = async ({ + context, + task, + definition +}: IGetChildTasksParams) => { + const running: string[] = []; + const done: string[] = []; + const invalid: string[] = []; + const aborted: string[] = []; + const failed: string[] = []; + const collection: ITask[] = []; + + const { items } = await context.tasks.listTasks({ + where: { + parentId: task.id, + definitionId: definition + } + }); + for (const task of items) { + collection.push(task); + if ( + task.taskStatus === TaskDataStatus.RUNNING || + task.taskStatus === TaskDataStatus.PENDING + ) { + running.push(task.id); + continue; + } else if (task.taskStatus === TaskDataStatus.SUCCESS) { + done.push(task.id); + continue; + } else if (task.taskStatus === TaskDataStatus.FAILED) { + failed.push(task.id); + continue; + } else if (task.taskStatus === TaskDataStatus.ABORTED) { + aborted.push(task.id); + continue; + } + /** + * Impossible to be in a state not listed above, but just in case. + */ + invalid.push(task.id); + } + return { + running, + done, + invalid, + aborted, + failed, + collection + }; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessAssets/ImportFromUrlProcessAssets.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessAssets/ImportFromUrlProcessAssets.ts new file mode 100644 index 00000000000..429b0cbe6d1 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessAssets/ImportFromUrlProcessAssets.ts @@ -0,0 +1,301 @@ +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks/types"; +import type { + IImportFromUrlProcessAssets, + IImportFromUrlProcessAssetsInput, + IImportFromUrlProcessAssetsOutput +} from "~/tasks/domain/importFromUrlProcessAssets/abstractions/ImportFromUrlProcessAssets"; +import { CmsImportExportFileType, Context } from "~/types"; +import type { IFileFetcher } from "~/tasks/utils/fileFetcher"; +import type { + ICompressedFileReader, + IDecompressor, + IUnzipperFile +} from "~/tasks/utils/decompressor"; +import { MANIFEST_JSON } from "~/tasks/constants"; +import { getFilePath } from "~/tasks/utils/helpers/getFilePath"; +import { WebinyError } from "@webiny/error"; +import type { ICmsAssetsManifestJson } from "~/tasks/utils/types"; +import type { IResolvedAsset } from "~/tasks/utils/entryAssets"; + +export interface IImportFromUrlProcessAssetsParams { + fileFetcher: IFileFetcher; + reader: ICompressedFileReader; + decompressor: IDecompressor; +} + +export class ImportFromUrlProcessAssets< + C extends Context = Context, + I extends IImportFromUrlProcessAssetsInput = IImportFromUrlProcessAssetsInput, + O extends IImportFromUrlProcessAssetsOutput = IImportFromUrlProcessAssetsOutput +> implements IImportFromUrlProcessAssets +{ + private readonly fileFetcher: IFileFetcher; + private readonly reader: ICompressedFileReader; + private readonly decompressor: IDecompressor; + + public constructor(params: IImportFromUrlProcessAssetsParams) { + this.fileFetcher = params.fileFetcher; + this.reader = params.reader; + this.decompressor = params.decompressor; + } + + public async run(params: ITaskRunParams): Promise> { + const { context, response, input, isCloseToTimeout, isAborted } = params; + + const maxInsertErrors = input.maxInsertErrors || 100; + + if (!input.modelId) { + return response.error({ + message: `Missing "modelId" in the input.`, + code: "MISSING_MODEL_ID" + }); + } else if (!input.file) { + return response.error({ + message: `No file found in the provided data.`, + code: "NO_FILE_FOUND" + }); + } else if (input.file.type !== CmsImportExportFileType.ASSETS) { + return response.error({ + message: `Invalid file type. Expected "${CmsImportExportFileType.ASSETS}" but got "${input.file.type}".`, + code: "INVALID_FILE_TYPE" + }); + } + + const recordExists = async (id: string): Promise => { + try { + const result = await context.fileManager.getFile(id); + return !!result; + } catch (ex) { + return false; + } + }; + + const result = structuredClone({ + ...input, + errors: [] + }); + /** + * Read the compressed archive and get all the file information. + */ + const sources = await this.reader.read(result.file.key); + if (sources.length === 0) { + return response.error({ + message: `No files found in the compressed archive.`, + code: "NO_FILES_FOUND" + }); + } + + /** + * Read the manifest file. + * Should not be possible it does not exist, but let's handle it anyway. + */ + let manifest: ICmsAssetsManifestJson; + try { + manifest = await this.readManifest(sources, result); + } catch (ex) { + console.error(ex); + return response.error(ex); + } + + while (true) { + if (isCloseToTimeout()) { + return response.continue(result); + } else if (isAborted()) { + return response.aborted(); + } else if (result.errors.length > maxInsertErrors) { + return response.error({ + message: `Too many errors encountered while processing assets.`, + code: "TOO_MANY_ERRORS", + data: { + errors: result.errors + } + }); + } + const record = this.takeNextAssetRecord(manifest, result.lastAsset); + if (!record) { + return response.done("No more assets to process"); + } + result.lastAsset = record.id; + const source = sources.find(file => file.path === record.key); + if (!source) { + result.errors.push({ + file: record.key, + message: `File not found in the compressed archive.` + }); + continue; + } + /** + * Check if the file exists physically. + */ + const existsPhysically = await this.fileFetcher.exists(source.path); + if (existsPhysically && !input.override) { + console.log( + `Asset "${record.id}" / ${source.path} file already exists. Skipping...` + ); + continue; + } + /** + * Check if the file record already exists. + */ + const exists = await recordExists(record.id); + if (exists && !input.override) { + console.log( + `Asset "${record.id}" / ${source.path} record already exists. Skipping...` + ); + continue; + } + /** + * Upload the file to the S3 bucket, if it does not exist already. + */ + if (!existsPhysically) { + try { + const file = await this.decompressor.extract({ + source, + target: record.key + }); + if (!file?.Key) { + result.errors.push({ + file: record.key, + message: `Could not upload the file "${source.path}" to "${record.key}".` + }); + continue; + } + } catch (ex) { + result.errors.push({ + file: record.key, + message: ex.message + }); + continue; + } + } + /** + * Update an existing file record. + */ + if (exists) { + try { + await context.fileManager.updateFile(record.id, { + ...record + }); + } catch (ex) { + result.errors.push({ + file: record.key, + message: ex.message + }); + } + continue; + } + /** + * Create a new file record. + */ + try { + await context.fileManager.createFile({ + ...record, + id: record.id, + key: record.key, + size: record.size, + type: record.type, + name: record.name, + meta: record.meta, + aliases: record.aliases, + extensions: record.extensions, + location: record.location, + tags: record.tags + }); + } catch (ex) { + result.errors.push({ + file: record.key, + message: ex.message + }); + } + } + } + + private async readManifest( + sources: IUnzipperFile[], + input: I + ): Promise { + let manifest: string | undefined = input?.manifest; + + if (!manifest) { + const extractPath = getFilePath(input.file.key); + const source = sources.find(file => file.path === MANIFEST_JSON); + if (!source) { + throw new WebinyError({ + message: `No manifest file found in the compressed archive.`, + code: "NO_MANIFEST_FILE" + }); + } + const target = `extracted/${extractPath.path}/${source.path}`; + try { + const file = await this.decompressor.extract({ + source, + target + }); + if (!file.Key) { + throw new Error(`Could not upload the file "${source.path}" to "${target}".`); + } + manifest = file.Key; + } catch (ex) { + throw new WebinyError({ + message: ex.message, + code: "MANIFEST_DECOMPRESS_FAILED" + }); + } + } + + let file: string | null; + try { + file = await this.fileFetcher.read(manifest); + if (!file) { + throw new WebinyError({ + message: `Could not fetch the manifest file "${manifest}".`, + code: "MANIFEST_NOT_FOUND" + }); + } + } catch (ex) { + throw new WebinyError({ + message: ex.message, + code: "MANIFEST_FETCH_FAILED", + data: ex + }); + } + + let json: ICmsAssetsManifestJson; + try { + json = JSON.parse(file); + } catch (ex) { + throw new WebinyError({ + message: ex.message, + code: "MANIFEST_PARSE_FAILED", + data: ex + }); + } + if (!json.assets?.length) { + throw new WebinyError({ + message: `Invalid manifest file "${manifest}". Missing "assets" property.`, + code: "INVALID_MANIFEST_FILE" + }); + } else if (!json.size) { + throw new WebinyError({ + message: `Invalid manifest file "${manifest}". Missing "size" property.`, + code: "INVALID_MANIFEST_FILE" + }); + } + return json; + } + + private takeNextAssetRecord( + manifest: ICmsAssetsManifestJson, + lastAsset: string | undefined + ): IResolvedAsset | null { + if (!lastAsset) { + return manifest.assets[0] as IResolvedAsset; + } + const lastIndex = manifest.assets.findIndex(asset => asset.id === lastAsset); + if (lastIndex === -1) { + return null; + } + const asset = manifest.assets[lastIndex + 1]; + return asset || null; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessAssets/abstractions/ImportFromUrlProcessAssets.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessAssets/abstractions/ImportFromUrlProcessAssets.ts new file mode 100644 index 00000000000..2b58315a1d6 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessAssets/abstractions/ImportFromUrlProcessAssets.ts @@ -0,0 +1,36 @@ +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; +import type { Context, ICmsImportExportValidatedValidFile } from "~/types"; + +export type IImportFromUrlProcessAssetsInputFile = Pick< + ICmsImportExportValidatedValidFile, + "key" | "type" +>; + +export interface IImportFromUrlProcessAssetsInputError { + file: string; + message: string; +} + +export interface IImportFromUrlProcessAssetsInput { + modelId: string; + file: IImportFromUrlProcessAssetsInputFile; + maxInsertErrors?: number; + override?: boolean; + manifest?: string; + lastAsset?: string; + errors?: IImportFromUrlProcessAssetsInputError[]; +} + +export type IImportFromUrlProcessAssetsOutput = ITaskResponseDoneResultOutput; + +export interface IImportFromUrlProcessAssets< + C extends Context = Context, + I extends IImportFromUrlProcessAssetsInput = IImportFromUrlProcessAssetsInput, + O extends IImportFromUrlProcessAssetsOutput = IImportFromUrlProcessAssetsOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntries.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntries.ts new file mode 100644 index 00000000000..bcb75511828 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntries.ts @@ -0,0 +1,101 @@ +import type { Context } from "~/types"; +import { CmsImportExportFileType } from "~/types"; +import type { + IImportFromUrlProcessEntries, + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesOutput +} from "./abstractions/ImportFromUrlProcessEntries"; +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import type { ICmsEntryManager } from "@webiny/api-headless-cms/types"; +import { ImportFromUrlProcessEntriesDecompress } from "~/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntriesDecompress"; +import type { IFileFetcher } from "~/tasks/utils/fileFetcher"; +import { ImportFromUrlProcessEntriesInsert } from "./ImportFromUrlProcessEntriesInsert"; +import type { ICompressedFileReader, IDecompressor } from "~/tasks/utils/decompressor"; + +export interface IImportFromUrlProcessEntriesParams { + fileFetcher: IFileFetcher; + reader: ICompressedFileReader; + decompressor: IDecompressor; +} + +export class ImportFromUrlProcessEntries< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> implements IImportFromUrlProcessEntries +{ + private readonly fileFetcher: IFileFetcher; + private readonly reader: ICompressedFileReader; + private readonly decompressor: IDecompressor; + + public constructor(params: IImportFromUrlProcessEntriesParams) { + this.fileFetcher = params.fileFetcher; + this.reader = params.reader; + this.decompressor = params.decompressor; + } + + public async run(params: ITaskRunParams): Promise> { + const { context, response, input } = params; + + if (!input.modelId) { + return response.error({ + message: `Missing "modelId" in the input.`, + code: "MISSING_MODEL_ID" + }); + } else if (!input.file) { + return response.error({ + message: `No file found in the provided data.`, + code: "NO_FILE_FOUND" + }); + } else if (input.file.type !== CmsImportExportFileType.ENTRIES) { + return response.error({ + message: `Invalid file type. Expected "${CmsImportExportFileType.ENTRIES}" but got "${input.file.type}".`, + code: "INVALID_FILE_TYPE" + }); + } + + let entryManager: ICmsEntryManager; + try { + entryManager = await context.cms.getEntryManager(input.modelId); + } catch (ex) { + return response.error({ + message: `Model "${input.modelId}" not found.`, + code: "MODEL_NOT_FOUND" + }); + } + + if (!input.decompress?.done) { + try { + const decompress = new ImportFromUrlProcessEntriesDecompress({ + reader: this.reader, + decompressor: this.decompressor + }); + + return await decompress.run(params); + } catch (ex) { + console.error(ex); + return response.error({ + message: ex.message, + code: ex.code || "DECOMPRESS_ERROR", + data: ex.data, + stack: ex.stack + }); + } + } + + try { + const insert = new ImportFromUrlProcessEntriesInsert({ + entryManager, + fileFetcher: this.fileFetcher + }); + return await insert.run(params); + } catch (ex) { + return response.error({ + message: ex.message, + code: ex.code || "DECOMPRESS_ERROR", + data: ex.data, + stack: ex.stack + }); + } + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntriesDecompress.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntriesDecompress.ts new file mode 100644 index 00000000000..916950ab717 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntriesDecompress.ts @@ -0,0 +1,97 @@ +import type { ICompressedFileReader, IDecompressor } from "~/tasks/utils/decompressor"; +import type { + IImportFromUrlProcessEntriesDecompress, + IImportFromUrlProcessEntriesDecompressRunParams, + IImportFromUrlProcessEntriesDecompressRunResult +} from "./abstractions/ImportFromUrlProcessEntriesDecompress"; +import type { + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesOutput +} from "./abstractions/ImportFromUrlProcessEntries"; +import { getFilePath } from "~/tasks/utils/helpers/getFilePath"; +import type { Context } from "~/types"; +import { WebinyError } from "@webiny/error"; + +export interface IImportFromUrlProcessEntriesDecompressParams { + reader: ICompressedFileReader; + decompressor: IDecompressor; +} + +export class ImportFromUrlProcessEntriesDecompress< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> implements IImportFromUrlProcessEntriesDecompress +{ + private readonly reader: ICompressedFileReader; + private readonly decompressor: IDecompressor; + + public constructor(params: IImportFromUrlProcessEntriesDecompressParams) { + this.reader = params.reader; + this.decompressor = params.decompressor; + } + + public async run( + params: IImportFromUrlProcessEntriesDecompressRunParams + ): Promise> { + const { response, input, isCloseToTimeout, isAborted } = params; + const result = structuredClone(input); + + const files = (await this.reader.read(result.file.key)).sort((a, b) => { + return a.uncompressedSize - b.uncompressedSize; + }); + if (files.length === 0) { + return response.error({ + message: `No files found in the compressed archive.`, + code: "NO_FILES_FOUND" + }); + } + + const extractPath = getFilePath(result.file.key); + + while (true) { + const next = result.decompress?.next || 0; + const source = files.at(next); + if (!source) { + return response.continue({ + ...result, + decompress: { + ...result.decompress, + done: true + } + }); + } else if (isAborted()) { + return response.aborted(); + } else if (isCloseToTimeout() || result.decompress?.done) { + return response.continue({ + ...result + }); + } + + try { + const target = `extracted/${extractPath.path}/${source.path}`; + const file = await this.decompressor.extract({ + source, + target + }); + if (!file.Key) { + throw new WebinyError({ + message: `Could not upload the file "${source.path}".`, + code: "FILE_NOT_UPLOAD", + data: { + source: source.path, + target + } + }); + } + result.decompress = { + ...result.decompress, + next: next + 1, + files: [...(result.decompress?.files || []), file.Key] + }; + } catch (ex) { + return response.error(ex); + } + } + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntriesInsert.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntriesInsert.ts new file mode 100644 index 00000000000..dd2bf49290d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/ImportFromUrlProcessEntriesInsert.ts @@ -0,0 +1,206 @@ +import type { + IImportFromUrlProcessEntriesInsert, + IImportFromUrlProcessEntriesInsertRunParams, + IImportFromUrlProcessEntriesInsertRunResult +} from "./abstractions/ImportFromUrlProcessEntriesInsert"; +import type { + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesInsertProcessedFileErrorsInput, + IImportFromUrlProcessEntriesInsertProcessedFileInput, + IImportFromUrlProcessEntriesOutput +} from "./abstractions/ImportFromUrlProcessEntries"; +import type { ICmsEntryManager } from "@webiny/api-headless-cms/types"; +import type { Context } from "~/types"; +import { MANIFEST_JSON } from "~/tasks/constants"; +import type { IFileFetcher } from "~/tasks/utils/fileFetcher"; +import type { ICmsEntryEntriesJson } from "~/tasks/utils/types"; + +export interface IImportFromUrlProcessEntriesInsertParams { + entryManager: ICmsEntryManager; + fileFetcher: IFileFetcher; +} + +export class ImportFromUrlProcessEntriesInsert< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> implements IImportFromUrlProcessEntriesInsert +{ + private readonly entryManager: ICmsEntryManager; + private readonly fileFetcher: IFileFetcher; + + public constructor(params: IImportFromUrlProcessEntriesInsertParams) { + this.entryManager = params.entryManager; + this.fileFetcher = params.fileFetcher; + } + + public async run( + params: IImportFromUrlProcessEntriesInsertRunParams + ): Promise> { + const { response, input, isAborted, isCloseToTimeout } = params; + + const result = structuredClone(input); + + const files = (result.decompress?.files || []).filter( + file => !file.endsWith(MANIFEST_JSON) + ); + if (files.length === 0) { + return response.error({ + message: `No entry files found in the compressed archive.`, + code: "NO_FILES_FOUND", + data: { + files: result.decompress?.files || [] + } + }); + } + + const maxInsertErrors = result.maxInsertErrors || 10; + + const processed: IImportFromUrlProcessEntriesInsertProcessedFileInput[] = + result.insert?.processed || []; + + while (true) { + if (isAborted()) { + return response.aborted(); + } else if (isCloseToTimeout()) { + return response.continue({ + ...result + }); + } + const file = this.takeFile(files, result.insert?.file); + if (!file) { + const output: IImportFromUrlProcessEntriesOutput = { + files: processed + }; + + return response.done(output as O); + } + const data = await this.readAndParse(file, result); + if (!data) { + result.insert = { + ...result.insert, + file: this.takeNextFile(files, file), + failed: [ + ...(result.insert?.failed || []), + { + key: file, + message: `Failed to read and parse the file. Please check logs for more detailed information.` + } + ] + }; + continue; + } + const errors: IImportFromUrlProcessEntriesInsertProcessedFileErrorsInput[] = []; + + let success = 0; + for (const item of data.items) { + if (errors.length >= maxInsertErrors) { + return response.error({ + message: `Max insert errors reached.`, + code: "MAX_INSERT_ERRORS", + data: { + errors + } + }); + } + try { + await this.entryManager.create(item); + success++; + } catch (ex) { + console.error(`Failed to insert entry "${item.id}"`, ex); + errors.push({ + id: item.id, + message: ex.message + }); + } + } + processed.push({ + key: file, + success, + total: data.items.length, + errors + }); + result.insert = { + ...result.insert, + file: this.takeNextFile(files, file), + processed + }; + } + } + /** + * Method reads and parses the target file. + * In case of any error, it will log it, attach to the result parameter and return null. + */ + private async readAndParse(key: string, result: I): Promise { + const data = await this.fileFetcher.read(key); + if (!data) { + const message = `No contents found for file "${key}".`; + console.error(message); + result.insert = { + ...result.insert, + failed: [ + ...(result.insert?.failed || []), + { + key, + message + } + ] + }; + return null; + } + let parsed: Partial; + try { + parsed = JSON.parse(data); + } catch (ex) { + const message = `Failed to parse JSON for file "${key}".`; + console.error(message); + result.insert = { + ...result.insert, + failed: [ + ...(result.insert?.failed || []), + { + key, + message + } + ] + }; + return null; + } + if (!parsed.items) { + const message = `Missing "items" in the parsed JSON for file "${key}".`; + console.error(message); + result.insert = { + ...result.insert, + failed: [ + ...(result.insert?.failed || []), + { + key, + message + } + ] + }; + return null; + } + return parsed as ICmsEntryEntriesJson; + } + + private takeFile(files: string[], last?: string): string | undefined { + if (!last) { + return files[0]; + } + return files.find(file => file === last); + } + + private takeNextFile(files: string[], last: string): string | undefined { + const index = files.indexOf(last); + if (index < 0) { + return `notFound:${last}`; + } + const next = files.at(index + 1); + if (next) { + return next; + } + + return `completedWith:${last}`; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntries.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntries.ts new file mode 100644 index 00000000000..777e637d83a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntries.ts @@ -0,0 +1,65 @@ +import type { Context, ICmsImportExportValidatedValidFile } from "~/types"; +import type { + ITaskResponseDoneResultOutput, + ITaskResponseResult, + ITaskRunParams +} from "@webiny/tasks"; + +export type IImportFromUrlProcessEntriesInputFile = Pick< + ICmsImportExportValidatedValidFile, + "key" | "type" +>; + +export interface IImportFromUrlProcessEntriesInsertFailedFileInput { + key: string; + message: string; +} + +export interface IImportFromUrlProcessEntriesInsertProcessedFileErrorsInput { + message: string; + id: string; +} + +export interface IImportFromUrlProcessEntriesInsertProcessedFileInput { + key: string; + success: number; + total: number; + errors: IImportFromUrlProcessEntriesInsertProcessedFileErrorsInput[]; +} +export interface IImportFromUrlProcessEntriesInsertInput { + processed?: IImportFromUrlProcessEntriesInsertProcessedFileInput[]; + failed?: IImportFromUrlProcessEntriesInsertFailedFileInput[]; + file?: string; + done?: boolean; +} + +export interface IImportFromUrlProcessEntriesInput { + modelId: string; + file: IImportFromUrlProcessEntriesInputFile; + maxInsertErrors: number | undefined; + decompress?: { + done?: boolean; + files?: string[]; + next?: number; + }; + insert?: IImportFromUrlProcessEntriesInsertInput; +} + +export interface IImportFromUrlProcessEntriesOutputFile { + key: string; + success: number; + total: number; + errors: IImportFromUrlProcessEntriesInsertProcessedFileErrorsInput[]; +} + +export interface IImportFromUrlProcessEntriesOutput extends ITaskResponseDoneResultOutput { + files: IImportFromUrlProcessEntriesOutputFile[]; +} + +export interface IImportFromUrlProcessEntries< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> { + run(params: ITaskRunParams): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntriesDecompress.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntriesDecompress.ts new file mode 100644 index 00000000000..22df577a8bc --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntriesDecompress.ts @@ -0,0 +1,27 @@ +import type { + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesOutput +} from "./ImportFromUrlProcessEntries"; +import type { Context } from "~/types"; +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; + +export type IImportFromUrlProcessEntriesDecompressRunParams< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> = ITaskRunParams; + +export type IImportFromUrlProcessEntriesDecompressRunResult< + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> = ITaskResponseResult; + +export interface IImportFromUrlProcessEntriesDecompress< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> { + run( + params: IImportFromUrlProcessEntriesDecompressRunParams + ): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntriesInsert.ts b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntriesInsert.ts new file mode 100644 index 00000000000..f1901bd8b20 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntriesInsert.ts @@ -0,0 +1,27 @@ +import type { + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesOutput +} from "./ImportFromUrlProcessEntries"; +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import type { Context } from "~/types"; + +export type IImportFromUrlProcessEntriesInsertRunParams< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> = ITaskRunParams; + +export type IImportFromUrlProcessEntriesInsertRunResult< + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> = ITaskResponseResult; + +export interface IImportFromUrlProcessEntriesInsert< + C extends Context = Context, + I extends IImportFromUrlProcessEntriesInput = IImportFromUrlProcessEntriesInput, + O extends IImportFromUrlProcessEntriesOutput = IImportFromUrlProcessEntriesOutput +> { + run( + params: IImportFromUrlProcessEntriesInsertRunParams + ): Promise>; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/domain/validateImportFromUrl/ValidateImportFromUrl.ts b/packages/api-headless-cms-import-export/src/tasks/domain/validateImportFromUrl/ValidateImportFromUrl.ts new file mode 100644 index 00000000000..ab25fd2fc7b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/domain/validateImportFromUrl/ValidateImportFromUrl.ts @@ -0,0 +1,175 @@ +import type { + IValidateImportFromUrl, + IValidateImportFromUrlInput, + IValidateImportFromUrlOutput +} from "~/tasks/domain/abstractions/ValidateImportFromUrl"; +import type { IExternalFileFetcher } from "~/tasks/utils/externalFileFetcher"; +import type { ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; +import type { Context, ICmsImportExportValidatedFile } from "~/types"; +import { getImportExportFileType } from "~/tasks/utils/helpers/getImportExportFileType"; +import type { NonEmptyArray } from "@webiny/api/types"; +import { prependImportPath } from "~/tasks/utils/helpers/importPath"; + +export interface IFileExists { + (key: string): Promise; +} + +export interface IValidateImportFromUrlParams { + fileFetcher: IExternalFileFetcher; + fileExists: IFileExists; +} + +export class ValidateImportFromUrl< + C extends Context = Context, + I extends IValidateImportFromUrlInput = IValidateImportFromUrlInput, + O extends IValidateImportFromUrlOutput = IValidateImportFromUrlOutput +> implements IValidateImportFromUrl +{ + private readonly fileFetcher: IExternalFileFetcher; + private readonly fileExists: IFileExists; + + public constructor(params: IValidateImportFromUrlParams) { + this.fileFetcher = params.fileFetcher; + this.fileExists = params.fileExists; + } + + public async run(params: ITaskRunParams): Promise> { + const { response, input } = params; + + const { files = [], model } = input; + + const results: ICmsImportExportValidatedFile[] = []; + + for (const target of files) { + const { get, head, checksum } = target; + const { type, pathname, error: fileTypeError } = getImportExportFileType(head); + if (fileTypeError) { + results.push({ + checked: true, + get, + head, + key: target.key, + checksum, + error: { + message: "File type not supported.", + code: "FILE_TYPE_NOT_SUPPORTED", + data: { + pathname, + type + } + }, + size: undefined, + type: undefined + }); + continue; + } + const { file, error } = await this.fileFetcher.head(head); + if (error) { + results.push({ + checked: true, + get, + head, + error, + key: target.key, + checksum, + size: undefined, + type: undefined + }); + continue; + } else if (!file) { + results.push({ + checked: true, + get, + head, + key: target.key, + checksum, + error: { + message: "File not found.", + code: "FILE_NOT_FOUND", + data: { + url: head + } + }, + size: undefined, + type: undefined + }); + continue; + } else if (file.checksum !== checksum) { + results.push({ + checked: true, + get, + head, + key: target.key, + checksum, + error: { + message: "Checksum does not match.", + code: "CHECKSUM_MISMATCH", + data: { + expected: checksum, + received: file.checksum + } + }, + size: file.size, + type + }); + continue; + } + const key = prependImportPath(target.key); + const exists = await this.fileExists(key); + if (exists) { + results.push({ + checked: true, + get, + head, + key: target.key, + checksum, + error: { + message: "File already exists.", + code: "FILE_ALREADY_EXISTS", + data: { + key + } + }, + size: file.size, + type + }); + continue; + } + + results.push({ + checked: true, + get, + head, + key: target.key, + checksum, + size: file.size, + type + }); + } + if (results.length === 0) { + return response.error({ + message: "No files found.", + code: "NO_FILES_FOUND" + }); + } + + const output: IValidateImportFromUrlOutput = { + files: results as NonEmptyArray, + modelId: model?.modelId + }; + const filesWithErrors = results.filter(file => !!file.error); + if (filesWithErrors.length > 0) { + output.error = { + message: "One or more files are invalid.", + code: "INVALID_FILES", + data: { + files: filesWithErrors.map(file => { + return file.key; + }) + } + }; + } + + return response.done(output as O); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/exportContentAssets.ts b/packages/api-headless-cms-import-export/src/tasks/exportContentAssets.ts new file mode 100644 index 00000000000..650433ae0de --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/exportContentAssets.ts @@ -0,0 +1,29 @@ +import { createTaskDefinition } from "@webiny/tasks"; +import type { Context } from "~/types"; +import type { + IExportContentAssetsInput, + IExportContentAssetsOutput +} from "~/tasks/domain/abstractions/ExportContentAssets"; +import { EXPORT_CONTENT_ASSETS_TASK } from "~/tasks/constants"; + +export const createExportContentAssets = () => { + return createTaskDefinition({ + id: EXPORT_CONTENT_ASSETS_TASK, + title: "Export Content Assets", + maxIterations: 50, + isPrivate: true, + description: "Export content assets from a specific model.", + async run(params) { + const { createExportContentAssets } = await import( + /* webpackChunkName: "createExportContentAssets" */ "./domain/createExportContentAssets" + ); + + try { + const runner = createExportContentAssets(); + return await runner.run(params); + } catch (ex) { + return params.response.error(ex); + } + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/exportContentEntries.ts b/packages/api-headless-cms-import-export/src/tasks/exportContentEntries.ts new file mode 100644 index 00000000000..b127473eaaf --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/exportContentEntries.ts @@ -0,0 +1,29 @@ +import { createTaskDefinition } from "@webiny/tasks"; +import { EXPORT_CONTENT_ENTRIES_TASK } from "./constants"; +import type { Context } from "~/types"; +import type { + IExportContentEntriesInput, + IExportContentEntriesOutput +} from "~/tasks/domain/abstractions/ExportContentEntries"; + +export const createExportContentEntriesTask = () => { + return createTaskDefinition({ + id: EXPORT_CONTENT_ENTRIES_TASK, + title: "Export Content Entries", + maxIterations: 50, + isPrivate: true, + description: "Export content entries from a specific model.", + async run(params) { + const { createExportContentEntries } = await import( + /* webpackChunkName: "createExportContentEntries" */ "./domain/createExportContentEntries" + ); + + try { + const runner = createExportContentEntries(); + return await runner.run(params); + } catch (ex) { + return params.response.error(ex); + } + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/exportContentEntriesController.ts b/packages/api-headless-cms-import-export/src/tasks/exportContentEntriesController.ts new file mode 100644 index 00000000000..810854665b3 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/exportContentEntriesController.ts @@ -0,0 +1,33 @@ +import { createTaskDefinition } from "@webiny/tasks"; +import { EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK } from "./constants"; +import type { Context } from "~/types"; +import type { + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput +} from "~/tasks/domain/abstractions/ExportContentEntriesController"; + +export const createExportContentEntriesControllerTask = () => { + return createTaskDefinition< + Context, + IExportContentEntriesControllerInput, + IExportContentEntriesControllerOutput + >({ + id: EXPORT_CONTENT_ENTRIES_CONTROLLER_TASK, + title: "Export Content Entries and Assets Controller", + maxIterations: 100, + isPrivate: true, + description: "Export content entries and assets from a specific model - controller.", + async run(params) { + const { ExportContentEntriesController } = await import( + /* webpackChunkName: "ExportContentEntriesController" */ "./domain/ExportContentEntriesController" + ); + + try { + const controller = new ExportContentEntriesController(); + return await controller.run(params); + } catch (ex) { + return params.response.error(ex); + } + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/importFromUrlController.ts b/packages/api-headless-cms-import-export/src/tasks/importFromUrlController.ts new file mode 100644 index 00000000000..a601c8bbd4b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/importFromUrlController.ts @@ -0,0 +1,42 @@ +import { createTaskDefinition } from "@webiny/tasks"; +import { IMPORT_FROM_URL_CONTROLLER_TASK } from "./constants"; +import type { Context } from "~/types"; +import type { + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput +} from "~/tasks/domain/abstractions/ImportFromUrlController"; + +export const createImportFromUrlControllerTask = () => { + return createTaskDefinition< + Context, + IImportFromUrlControllerInput, + IImportFromUrlControllerOutput + >({ + id: IMPORT_FROM_URL_CONTROLLER_TASK, + title: "Import from URL List - Controller", + maxIterations: 500, + isPrivate: true, + description: "Imports the data from the given URL list - controller.", + async run(params) { + const { ImportFromUrlController } = await import( + /* webpackChunkName: "ImportFromUrlController" */ "./domain/ImportFromUrlController" + ); + + try { + const runner = new ImportFromUrlController(); + return await runner.run(params); + } catch (ex) { + return params.response.error(ex); + } + }, + async onDone({ task }) { + const { createDeleteFiles } = await import( + /* webpackChunkName: "DeleteFiles" */ "./utils/deleteFiles/DeleteFiles" + ); + + const deleteFiles = createDeleteFiles(); + + await deleteFiles.execute(task.output?.files); + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/importFromUrlDownload.ts b/packages/api-headless-cms-import-export/src/tasks/importFromUrlDownload.ts new file mode 100644 index 00000000000..5f50ce1521d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/importFromUrlDownload.ts @@ -0,0 +1,31 @@ +import { IMPORT_FROM_URL_DOWNLOAD_TASK } from "~/tasks/constants"; +import { createTaskDefinition } from "@webiny/tasks"; +import type { Context } from "~/types"; +import type { + IImportFromUrlDownloadInput, + IImportFromUrlDownloadOutput +} from "~/tasks/domain/abstractions/ImportFromUrlDownload"; + +export const createImportFromUrlDownloadTask = () => { + return createTaskDefinition( + { + id: IMPORT_FROM_URL_DOWNLOAD_TASK, + title: "Import from URL List - Download", + maxIterations: 500, + isPrivate: true, + description: "Downloads the files from external URL.", + async run(params) { + const { ImportFromUrlDownload } = await import( + /* webpackChunkName: "ImportFromUrlDownload" */ "./domain/ImportFromUrlDownload" + ); + + try { + const runner = new ImportFromUrlDownload(); + return await runner.run(params); + } catch (ex) { + return params.response.error(ex); + } + } + } + ); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/importFromUrlProcessAssets.ts b/packages/api-headless-cms-import-export/src/tasks/importFromUrlProcessAssets.ts new file mode 100644 index 00000000000..6bcf88cdf37 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/importFromUrlProcessAssets.ts @@ -0,0 +1,33 @@ +import { IMPORT_FROM_URL_PROCESS_ASSETS_TASK } from "~/tasks/constants"; +import { createTaskDefinition } from "@webiny/tasks"; +import type { Context } from "~/types"; +import type { + IImportFromUrlProcessAssetsInput, + IImportFromUrlProcessAssetsOutput +} from "./domain/importFromUrlProcessAssets/abstractions/ImportFromUrlProcessAssets"; + +export const createImportFromUrlProcessAssetsTask = () => { + return createTaskDefinition< + Context, + IImportFromUrlProcessAssetsInput, + IImportFromUrlProcessAssetsOutput + >({ + id: IMPORT_FROM_URL_PROCESS_ASSETS_TASK, + title: "Import from URL List - Process entries", + maxIterations: 10, + isPrivate: true, + description: "Process entries import.", + async run(params) { + const { createImportFromUrlProcessAssets } = await import( + /* webpackChunkName: "createImportFromUrlProcessAssets" */ "./domain/createImportFromUrlProcessAssets" + ); + + try { + const runner = createImportFromUrlProcessAssets(); + return await runner.run(params); + } catch (ex) { + return params.response.error(ex); + } + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/importFromUrlProcessEntries.ts b/packages/api-headless-cms-import-export/src/tasks/importFromUrlProcessEntries.ts new file mode 100644 index 00000000000..f375458078d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/importFromUrlProcessEntries.ts @@ -0,0 +1,33 @@ +import { IMPORT_FROM_URL_PROCESS_ENTRIES_TASK } from "~/tasks/constants"; +import { createTaskDefinition } from "@webiny/tasks"; +import type { Context } from "~/types"; +import type { + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesOutput +} from "./domain/importFromUrlProcessEntries/abstractions/ImportFromUrlProcessEntries"; + +export const createImportFromUrlProcessEntriesTask = () => { + return createTaskDefinition< + Context, + IImportFromUrlProcessEntriesInput, + IImportFromUrlProcessEntriesOutput + >({ + id: IMPORT_FROM_URL_PROCESS_ENTRIES_TASK, + title: "Import from URL List - Process entries", + maxIterations: 500, + isPrivate: true, + description: "Process entries import.", + async run(params) { + const { createImportFromUrlProcessEntries } = await import( + /* webpackChunkName: "createImportFromUrlProcessEntries" */ "./domain/createImportFromUrlProcessEntries" + ); + + try { + const runner = createImportFromUrlProcessEntries(); + return await runner.run(params); + } catch (ex) { + return params.response.error(ex); + } + } + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/index.ts b/packages/api-headless-cms-import-export/src/tasks/index.ts new file mode 100644 index 00000000000..20c1fc8547e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/index.ts @@ -0,0 +1,8 @@ +export * from "./exportContentEntriesController"; +export * from "./exportContentEntries"; +export * from "./exportContentAssets"; +export * from "./validateImportFromUrl"; +export * from "./importFromUrlController"; +export * from "./importFromUrlDownload"; +export * from "./importFromUrlProcessEntries"; +export * from "./importFromUrlProcessAssets"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/archiver/Archiver.ts b/packages/api-headless-cms-import-export/src/tasks/utils/archiver/Archiver.ts new file mode 100644 index 00000000000..fa0be4525c1 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/archiver/Archiver.ts @@ -0,0 +1,20 @@ +import type { IArchiver } from "./abstractions/Archiver"; +import type { Archiver as BaseArchiver, ArchiverOptions } from "archiver"; +import { create as baseCreateArchiver } from "archiver"; + +export interface IArchiverConfig { + format?: "zip"; + options: ArchiverOptions; +} + +export class Archiver implements IArchiver { + public readonly archiver: BaseArchiver; + + public constructor(config: IArchiverConfig) { + this.archiver = baseCreateArchiver(config.format || "zip", config.options); + } +} + +export const createArchiver = (config: IArchiverConfig): IArchiver => { + return new Archiver(config); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/archiver/abstractions/Archiver.ts b/packages/api-headless-cms-import-export/src/tasks/utils/archiver/abstractions/Archiver.ts new file mode 100644 index 00000000000..bcbcb6f709e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/archiver/abstractions/Archiver.ts @@ -0,0 +1,5 @@ +import type { Archiver } from "archiver"; + +export interface IArchiver { + archiver: Pick; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/archiver/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/archiver/index.ts new file mode 100644 index 00000000000..97cec1d6685 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/archiver/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/Archiver"; +export * from "./Archiver"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipper.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipper.ts new file mode 100644 index 00000000000..8ccb0b212ef --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipper.ts @@ -0,0 +1,276 @@ +import type { IZipper, IZipperDoneResult } from "~/tasks/utils/zipper"; +import type { IEntryAssets, IEntryAssetsResolver, IResolvedAsset } from "~/tasks/utils/entryAssets"; +import type { ICmsEntryFetcher } from "../cmsEntryFetcher"; +import type { + ICmsAssetsZipper, + ICmsAssetsZipperExecuteParams, + ICmsAssetsZipperExecuteResult +} from "./abstractions/CmsAssetsZipper"; +import { CmsAssetsZipperExecuteContinueResult } from "./CmsAssetsZipperExecuteContinueResult"; +import { CmsAssetsZipperExecuteDoneResult } from "./CmsAssetsZipperExecuteDoneResult"; +import type { IFileFetcher } from "../fileFetcher"; +import { CmsAssetsZipperExecuteContinueWithoutResult } from "./CmsAssetsZipperExecuteContinueWithoutResult"; +import { CmsAssetsZipperExecuteDoneWithoutResult } from "./CmsAssetsZipperExecuteDoneWithoutResult"; +import { PointerStore } from "~/tasks/utils/cmsAssetsZipper/PointerStore"; +import { UniqueResolver } from "../uniqueResolver/UniqueResolver"; +import type { CmsEntryMeta } from "@webiny/api-headless-cms/types"; +import { stripExportPath } from "~/tasks/utils/helpers/exportPath"; +import { MANIFEST_JSON } from "~/tasks/constants"; + +export interface ICmsAssetsZipperConfig { + zipper: IZipper; + entryFetcher: ICmsEntryFetcher; + createEntryAssets: () => IEntryAssets; + createEntryAssetsResolver: () => IEntryAssetsResolver; + fileFetcher: IFileFetcher; +} + +export class CmsAssetsZipper implements ICmsAssetsZipper { + private readonly zipper: IZipper; + private readonly entryFetcher: ICmsEntryFetcher; + private readonly createEntryAssets: () => IEntryAssets; + private readonly createEntryAssetsResolver: () => IEntryAssetsResolver; + private readonly fileFetcher: IFileFetcher; + + public constructor(params: ICmsAssetsZipperConfig) { + this.zipper = params.zipper; + this.entryFetcher = params.entryFetcher; + this.createEntryAssets = params.createEntryAssets; + this.createEntryAssetsResolver = params.createEntryAssetsResolver; + this.fileFetcher = params.fileFetcher; + } + + public async execute( + params: ICmsAssetsZipperExecuteParams + ): Promise { + const { + isCloseToTimeout, + isAborted, + entryAfter: inputEntryAfter, + fileAfter: inputFileAfter + } = params; + + const pointerStore = new PointerStore({ + entryMeta: { + cursor: inputEntryAfter || null + }, + fileCursor: inputFileAfter + }); + const entryAssets = this.createEntryAssets(); + const entryAssetsResolver = this.createEntryAssetsResolver(); + const uniqueLoadedAssetsResolver = new UniqueResolver(); + const allLoadedAssets: IResolvedAsset[] = []; + const assets: IResolvedAsset[] = []; + let nextMeta: CmsEntryMeta | undefined; + /** + * Note that this method should NEVER be awaited as it will be called recursively. + * It handles if the upload will be finalized or aborted. + */ + const fetchItems = async (): Promise => { + const hasMoreItems = pointerStore.getEntryHasMoreItems(); + const isStoredFiles = pointerStore.getIsStoredFiles(); + const currentCursor = pointerStore.getEntryCursor(); + pointerStore.setEntryMeta(nextMeta); + if (isAborted()) { + pointerStore.setTaskIsAborted(); + this.zipper.abort(); + return; + } + const closeToTimeout = isCloseToTimeout(); + if (isStoredFiles) { + await this.zipper.finalize(); + return; + } else if (!hasMoreItems || closeToTimeout) { + if (allLoadedAssets.length === 0) { + this.zipper.abort(); + return; + } + await this.zipper.add( + Buffer.from( + JSON.stringify({ + assets: allLoadedAssets, + size: allLoadedAssets.reduce((total, file) => { + const size = parseInt(file.size); + return total + size; + }, 0) + }) + ), + { + name: MANIFEST_JSON + } + ); + + pointerStore.setIsStoredFiles(); + return; + } + const { items, meta } = await this.entryFetcher(currentCursor); + /** + * If no items were found, we will throw an error via abort() call. + * This is internal from the lib we use. + */ + if (meta.totalCount === 0) { + console.log("No items found, aborting..."); + this.zipper.abort(); + return; + } + nextMeta = meta; + + /** + * Next we want to find all the assets, which were not already assigned. + * the assignAssets() method returns all newly found assets. + * + * Possibly no new assets found? Then just continue with the next batch of entries. + */ + const assigned = await entryAssets.assignAssets(items); + if (assigned.length === 0) { + fetchItems(); + return; + } + /** + * Then we want to load all the assets from the database. + * The matching will be done by alias or key of the file. + * + * Possibly no assets found? Then just continue with the next batch of entries. + */ + let loadedAssetList = await entryAssetsResolver.resolve(assigned); + const currentFileCursor = pointerStore.getFileCursor(); + pointerStore.resetFileCursor(); + if (loadedAssetList.length === 0) { + fetchItems(); + return; + } else if (currentFileCursor) { + const index = loadedAssetList.findIndex(asset => asset.id === currentFileCursor); + if (index === -1) { + fetchItems(); + return; + } + loadedAssetList = loadedAssetList.slice(index); + } + + const uniqueAssetsList = uniqueLoadedAssetsResolver.resolve(loadedAssetList, "id"); + if (uniqueAssetsList.length === 0) { + fetchItems(); + return; + } + /** + * If we have some new assets, we will push them into the assets array, which will be used in addAsset() function. + * + */ + assets.push(...uniqueAssetsList); + allLoadedAssets.push(...uniqueAssetsList); + + /** + * We proceed with adding the assets into the zip file. + */ + addAsset(); + }; + /** + * The addAsset() function will load a single asset from the storage and add it to the zipper. + * It calls itself while there are assets in the assets array. + * If there are no more assets, it will call fetchItems() to fetch more items and extract assets - and circle continues. + */ + const addAsset = async (): Promise => { + pointerStore.resetFileCursor(); + /** + * If there are no more assets, fetch more items and extract assets. + * fetchItems() method will check if there are more items to fetch or assets to add and + * will finish the zip if necessary. + */ + const asset = assets.shift(); + + if (!asset || isCloseToTimeout() || isAborted()) { + pointerStore.setFileCursor(asset?.id); + fetchItems(); + return; + } + + /** + * If there is an asset, load it from the storage and add it to the zipper. + */ + const file = await this.fileFetcher.stream(asset.key); + /** + * Possibly asset was not found on the storage? + * Then just continue with the next one. + */ + if (!file) { + addAsset(); + return; + } + this.zipper.add(file, { + name: asset.key + }); + }; + + this.zipper.on("error", error => { + console.error(error); + }); + /** + * Every time a file is added, add another one. + * If the getIsStoredFiles() flag is true, we will go through fetchItems() method for the last time, + * as will handle the upload finalization. + */ + this.zipper.on("entry", () => { + if (pointerStore.getIsStoredFiles()) { + fetchItems(); + return; + } + addAsset(); + }); + + /** + * Missing await on the fetchItems() is not an error. We do not want to await the function to be done. + * + * The zipper.done() is the method which we await because it will resolve when zipper.finalize() is called. + */ + setTimeout(() => { + fetchItems(); + }, 100); + + let result: IZipperDoneResult; + + try { + result = await this.zipper.done(); + } catch (ex) { + console.error(ex); + /** + * Possibly an error which is not an abort error? + * Abort error is thrown on .abort() method call. + */ + if (ex.message !== "Upload aborted." || pointerStore.getTaskIsAborted()) { + throw ex; + } + /** + * There was a possibility that no assets were found, but we need to continue through the next batch of entries. + * This happens on close to timeout. + */ + if (allLoadedAssets.length === 0 && pointerStore.getEntryCursor()) { + return new CmsAssetsZipperExecuteContinueWithoutResult({ + entryCursor: pointerStore.getEntryCursor(), + fileCursor: pointerStore.getFileCursor() + }); + } + /** + * An empty result set means that no assets were found and no more entries to fetch. + */ + return new CmsAssetsZipperExecuteDoneWithoutResult(); + } + + if (!result?.Key || !result.ETag) { + throw new Error("Failed to upload the file."); + } + + if (pointerStore.getEntryCursor() || pointerStore.getFileCursor()) { + return new CmsAssetsZipperExecuteContinueResult({ + key: stripExportPath(result.Key), + checksum: result.ETag.replaceAll('"', ""), + entryCursor: pointerStore.getEntryCursor(), + fileCursor: pointerStore.getFileCursor() + }); + } + + return new CmsAssetsZipperExecuteDoneResult({ + key: stripExportPath(result.Key), + checksum: result.ETag.replaceAll('"', "") + }); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteContinueResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteContinueResult.ts new file mode 100644 index 00000000000..24bd7b135a9 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteContinueResult.ts @@ -0,0 +1,15 @@ +import type { ICmsAssetsZipperExecuteContinueResult } from "./abstractions/CmsAssetsZipperExecuteContinueResult"; + +export class CmsAssetsZipperExecuteContinueResult implements ICmsAssetsZipperExecuteContinueResult { + public readonly key: string; + public readonly checksum: string; + public readonly entryCursor: string | undefined; + public readonly fileCursor: string | undefined; + + public constructor(params: ICmsAssetsZipperExecuteContinueResult) { + this.key = params.key; + this.checksum = params.checksum; + this.entryCursor = params.entryCursor; + this.fileCursor = params.fileCursor; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteContinueWithoutResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteContinueWithoutResult.ts new file mode 100644 index 00000000000..d3e1e80e7e0 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteContinueWithoutResult.ts @@ -0,0 +1,13 @@ +import type { ICmsAssetsZipperExecuteContinueWithoutResult } from "./abstractions/CmsAssetsZipperExecuteContinueWithoutResult"; + +export class CmsAssetsZipperExecuteContinueWithoutResult + implements ICmsAssetsZipperExecuteContinueWithoutResult +{ + public readonly entryCursor: string | undefined; + public readonly fileCursor: string | undefined; + + public constructor(params: ICmsAssetsZipperExecuteContinueWithoutResult) { + this.entryCursor = params.entryCursor; + this.fileCursor = params.fileCursor; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteDoneResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteDoneResult.ts new file mode 100644 index 00000000000..4e02961f7e1 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteDoneResult.ts @@ -0,0 +1,11 @@ +import type { ICmsAssetsZipperExecuteDoneResult } from "./abstractions/CmsAssetsZipperExecuteDoneResult"; + +export class CmsAssetsZipperExecuteDoneResult implements ICmsAssetsZipperExecuteDoneResult { + public readonly key: string; + public readonly checksum: string; + + constructor(params: ICmsAssetsZipperExecuteDoneResult) { + this.key = params.key; + this.checksum = params.checksum; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteDoneWithoutResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteDoneWithoutResult.ts new file mode 100644 index 00000000000..da48f736342 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/CmsAssetsZipperExecuteDoneWithoutResult.ts @@ -0,0 +1,8 @@ +import type { ICmsAssetsZipperExecuteDoneWithoutResult } from "./abstractions/CmsAssetsZipperExecuteDoneWithoutResult"; + +export class CmsAssetsZipperExecuteDoneWithoutResult + implements ICmsAssetsZipperExecuteDoneWithoutResult { + /** + * Just need an empty class + */ +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/PointerStore.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/PointerStore.ts new file mode 100644 index 00000000000..972353289b7 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/PointerStore.ts @@ -0,0 +1,77 @@ +import type { CmsEntryMeta } from "@webiny/api-headless-cms/types"; + +export interface IPointerStoreParams { + entryMeta: { + cursor: string | null | undefined; + }; + fileCursor?: string; +} + +export class PointerStore { + private isTaskAborted = false; + private isStoredFiles = false; + private entryMeta?: CmsEntryMeta; + private fileCursor?: string; + + public constructor(params: IPointerStoreParams) { + this.entryMeta = { + cursor: params.entryMeta.cursor || null, + hasMoreItems: true, + totalCount: 0 + }; + this.fileCursor = params.fileCursor; + } + + public setEntryMeta(meta?: CmsEntryMeta): void { + this.entryMeta = meta; + } + + public getEntryTotalItems(): number { + return this.entryMeta?.totalCount || 0; + } + + public getEntryHasMoreItems(): boolean { + return !!this.entryMeta?.hasMoreItems; + } + + public getEntryCursor(): string | undefined { + if (!this.entryMeta?.cursor || !this.entryMeta.hasMoreItems) { + return undefined; + } + return this.entryMeta.cursor; + } + + public setFileCursor(cursor?: string): void { + this.fileCursor = cursor; + } + + public getFileCursor(): string | undefined { + return this.fileCursor; + } + + public getIsStoredFiles(): boolean { + return this.isStoredFiles; + } + + public setIsStoredFiles(): void { + if (this.isStoredFiles) { + throw new Error(`The "setIsStoredFiles" method should be called only once.`); + } + this.isStoredFiles = true; + } + + public getTaskIsAborted(): boolean { + return this.isTaskAborted; + } + + public setTaskIsAborted(): void { + if (this.isTaskAborted) { + throw new Error(`The "setTaskIsAborted" method should be called only once.`); + } + this.isTaskAborted = true; + } + + public resetFileCursor(): void { + this.fileCursor = undefined; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipper.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipper.ts new file mode 100644 index 00000000000..6b24e4b7593 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipper.ts @@ -0,0 +1,22 @@ +import type { ICmsAssetsZipperExecuteContinueResult } from "./CmsAssetsZipperExecuteContinueResult"; +import type { ICmsAssetsZipperExecuteDoneResult } from "./CmsAssetsZipperExecuteDoneResult"; +import type { ICmsAssetsZipperExecuteDoneWithoutResult } from "./CmsAssetsZipperExecuteDoneWithoutResult"; +import type { ICmsAssetsZipperExecuteContinueWithoutResult } from "./CmsAssetsZipperExecuteContinueWithoutResult"; +import type { IIsCloseToTimeoutCallable } from "@webiny/tasks"; + +export interface ICmsAssetsZipperExecuteParams { + isCloseToTimeout: IIsCloseToTimeoutCallable; + isAborted(): boolean; + entryAfter: string | undefined; + fileAfter: string | undefined; +} + +export type ICmsAssetsZipperExecuteResult = + | ICmsAssetsZipperExecuteDoneResult + | ICmsAssetsZipperExecuteDoneWithoutResult + | ICmsAssetsZipperExecuteContinueResult + | ICmsAssetsZipperExecuteContinueWithoutResult; + +export interface ICmsAssetsZipper { + execute(params: ICmsAssetsZipperExecuteParams): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteContinueResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteContinueResult.ts new file mode 100644 index 00000000000..10e4632ea6f --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteContinueResult.ts @@ -0,0 +1,6 @@ +export interface ICmsAssetsZipperExecuteContinueResult { + key: string; + checksum: string; + entryCursor: string | undefined; + fileCursor: string | undefined; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteContinueWithoutResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteContinueWithoutResult.ts new file mode 100644 index 00000000000..812c577da5e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteContinueWithoutResult.ts @@ -0,0 +1,4 @@ +export interface ICmsAssetsZipperExecuteContinueWithoutResult { + entryCursor: string | undefined; + fileCursor: string | undefined; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteDoneResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteDoneResult.ts new file mode 100644 index 00000000000..f5edc31aad4 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteDoneResult.ts @@ -0,0 +1,4 @@ +export interface ICmsAssetsZipperExecuteDoneResult { + readonly key: string; + readonly checksum: string; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteDoneWithoutResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteDoneWithoutResult.ts new file mode 100644 index 00000000000..ad512f2ff6b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/abstractions/CmsAssetsZipperExecuteDoneWithoutResult.ts @@ -0,0 +1,3 @@ +import type { GenericRecord } from "@webiny/api/types"; + +export type ICmsAssetsZipperExecuteDoneWithoutResult = GenericRecord; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/index.ts new file mode 100644 index 00000000000..8f75e499d63 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsAssetsZipper/index.ts @@ -0,0 +1,10 @@ +export * from "./abstractions/CmsAssetsZipper"; +export * from "./abstractions/CmsAssetsZipperExecuteDoneResult"; +export * from "./abstractions/CmsAssetsZipperExecuteDoneWithoutResult"; +export * from "./abstractions/CmsAssetsZipperExecuteContinueResult"; +export * from "./abstractions/CmsAssetsZipperExecuteContinueWithoutResult"; +export * from "./CmsAssetsZipper"; +export * from "./CmsAssetsZipperExecuteDoneResult"; +export * from "./CmsAssetsZipperExecuteDoneWithoutResult"; +export * from "./CmsAssetsZipperExecuteContinueResult"; +export * from "./CmsAssetsZipperExecuteContinueWithoutResult"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/abstractions/CmsEntryFetcher.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/abstractions/CmsEntryFetcher.ts new file mode 100644 index 00000000000..58f7eb80749 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/abstractions/CmsEntryFetcher.ts @@ -0,0 +1,10 @@ +import type { CmsEntry, CmsEntryMeta } from "@webiny/api-headless-cms/types"; + +export interface ICmsEntryFetcherResult { + items: CmsEntry[]; + meta: CmsEntryMeta; +} + +export interface ICmsEntryFetcher { + (after?: string): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/createCmsEntryFetcher.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/createCmsEntryFetcher.ts new file mode 100644 index 00000000000..e57ea561865 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/createCmsEntryFetcher.ts @@ -0,0 +1,5 @@ +import type { ICmsEntryFetcher } from "./abstractions/CmsEntryFetcher"; + +export const createCmsEntryFetcher = (fetcher: ICmsEntryFetcher): ICmsEntryFetcher => { + return fetcher; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/index.ts new file mode 100644 index 00000000000..a139ba31805 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryFetcher/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/CmsEntryFetcher"; +export * from "./createCmsEntryFetcher"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipper.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipper.ts new file mode 100644 index 00000000000..fb326a4c751 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipper.ts @@ -0,0 +1,203 @@ +import type { CmsEntry } from "@webiny/api-headless-cms/types"; +import type { ICmsEntryEntriesJson, ICmsEntryManifestJson, IFileMeta } from "../types"; +import { CmsEntryZipperExecuteContinueResult } from "./CmsEntryZipperExecuteContinueResult"; +import { CmsEntryZipperExecuteDoneResult } from "./CmsEntryZipperExecuteDoneResult"; +import type { + ICmsEntryZipper, + ICmsEntryZipperExecuteParams, + ICmsEntryZipperExecuteResult +} from "./abstractions/CmsEntryZipper"; +import type { ICmsEntryFetcher } from "~/tasks/utils/cmsEntryFetcher/abstractions/CmsEntryFetcher"; +import type { IZipper } from "~/tasks/utils/zipper"; +import type { IAsset, IEntryAssets } from "~/tasks/utils/entryAssets"; +import type { IUniqueResolver } from "~/tasks/utils/uniqueResolver/abstractions/UniqueResolver"; +import { sanitizeModel } from "@webiny/api-headless-cms/export/crud/sanitize"; +import { stripExportPath } from "~/tasks/utils/helpers/exportPath"; +import { cleanChecksum } from "~/tasks/utils/helpers/cleanChecksum"; +import { MANIFEST_JSON } from "~/tasks/constants"; + +export interface ICmsEntryZipperConfig { + zipper: IZipper; + fetcher: ICmsEntryFetcher; + entryAssets: IEntryAssets; + uniqueAssetsResolver: IUniqueResolver; +} + +const createBufferData = (params: ICmsEntryEntriesJson) => { + const { items, meta, after } = params; + return Buffer.from( + JSON.stringify({ + items: items.map((item: Partial) => { + /** + * We will use the entryId as the ID of the entry. + */ + const id = item.entryId; + /** + * We need to remove some fields that are not needed in the export. + */ + delete item.tenant; + delete item.locale; + delete item.locked; + delete item.webinyVersion; + delete item.version; + delete item.entryId; + delete item.modelId; + + const values = item.values; + + delete item.values; + + return { + ...item, + ...values, + id + }; + }), + meta, + after + }) + ); +}; + +export class CmsEntryZipper implements ICmsEntryZipper { + private readonly zipper: IZipper; + private readonly fetcher: ICmsEntryFetcher; + private readonly entryAssets: IEntryAssets; + private readonly uniqueAssetsResolver: IUniqueResolver; + + public constructor(params: ICmsEntryZipperConfig) { + this.zipper = params.zipper; + this.fetcher = params.fetcher; + this.entryAssets = params.entryAssets; + this.uniqueAssetsResolver = params.uniqueAssetsResolver; + } + + public async execute( + params: ICmsEntryZipperExecuteParams + ): Promise { + const { isCloseToTimeout, isAborted, model, after: inputAfter, exportAssets } = params; + + const files: IFileMeta[] = []; + + let after = inputAfter; + + let hasMoreItems = true; + let storedFiles = false; + + let id = 1; + + let continueAfter: string | undefined = undefined; + + let assets: IAsset[] | undefined = undefined; + /** + * This function works as self invoking function, it will add items to the zipper until there are no more items to add. + * + * If the lambda is close to timeout, we will store the current state and continue from the last cursor in the next task run. + */ + const addItems = async () => { + if (isAborted()) { + this.zipper.abort(); + return; + } + const closeToTimeout = isCloseToTimeout(); + if (storedFiles) { + await this.zipper.finalize(); + return; + } else if (!hasMoreItems || closeToTimeout) { + if (closeToTimeout && hasMoreItems) { + continueAfter = after; + } + const output: ICmsEntryManifestJson = { + files, + assets, + model: sanitizeModel( + { + id: model.group.id + }, + model + ) + }; + await this.zipper.add(Buffer.from(JSON.stringify(output)), { + name: MANIFEST_JSON + }); + storedFiles = true; + return; + } + + const { items, meta } = await this.fetcher(after); + if (meta.totalCount === 0) { + console.log("No items found, aborting..."); + this.zipper.abort(); + return; + } + + const name = `entries${inputAfter ? `-${inputAfter}` : ""}-${id}.json`; + + files.push({ + id, + name, + after + }); + + await this.zipper.add(createBufferData({ items, meta, after }), { + name + }); + + hasMoreItems = meta.hasMoreItems; + after = meta.cursor || undefined; + id++; + /** + * We should not continue if assets are getting exported. + * There will be a new task triggered for exporting assets. + */ + if (exportAssets) { + return; + } else if (!assets) { + assets = []; + } + + const itemsAssets = await this.entryAssets.assignAssets(items); + + if (itemsAssets.length === 0) { + return; + } + const uniqueItemAssets = this.uniqueAssetsResolver.resolve(itemsAssets, "url"); + if (uniqueItemAssets.length === 0) { + return; + } + assets.push(...uniqueItemAssets); + }; + + this.zipper.on("error", error => { + console.error(error); + }); + + this.zipper.on("entry", () => { + addItems(); + }); + + addItems(); + + const result = await this.zipper.done(); + + if (!result.Key) { + throw new Error("Failed to upload the file."); + } + + const checksum = cleanChecksum(result.ETag || ""); + + const key = stripExportPath(result.Key); + if (continueAfter) { + return new CmsEntryZipperExecuteContinueResult({ + key, + checksum, + cursor: continueAfter + }); + } + + return new CmsEntryZipperExecuteDoneResult({ + key, + checksum + }); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipperExecuteContinueResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipperExecuteContinueResult.ts new file mode 100644 index 00000000000..fa5445fe73f --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipperExecuteContinueResult.ts @@ -0,0 +1,13 @@ +import type { ICmsEntryZipperExecuteContinueResult } from "./abstractions/CmsEntryZipperExecuteContinueResult"; + +export class CmsEntryZipperExecuteContinueResult implements ICmsEntryZipperExecuteContinueResult { + public readonly key: string; + public readonly cursor: string | null; + public readonly checksum: string; + + public constructor(params: ICmsEntryZipperExecuteContinueResult) { + this.key = params.key; + this.cursor = params.cursor; + this.checksum = params.checksum; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipperExecuteDoneResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipperExecuteDoneResult.ts new file mode 100644 index 00000000000..4dc0e659463 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/CmsEntryZipperExecuteDoneResult.ts @@ -0,0 +1,11 @@ +import type { ICmsEntryZipperExecuteDoneResult } from "./abstractions/CmsEntryZipperExecuteDoneResult"; + +export class CmsEntryZipperExecuteDoneResult implements ICmsEntryZipperExecuteDoneResult { + public readonly key: string; + public readonly checksum: string; + + constructor(params: ICmsEntryZipperExecuteDoneResult) { + this.key = params.key; + this.checksum = params.checksum; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipper.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipper.ts new file mode 100644 index 00000000000..cf437062c25 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipper.ts @@ -0,0 +1,20 @@ +import type { CmsModel } from "@webiny/api-headless-cms/types"; +import type { ICmsEntryZipperExecuteContinueResult } from "./CmsEntryZipperExecuteContinueResult"; +import type { ICmsEntryZipperExecuteDoneResult } from "./CmsEntryZipperExecuteDoneResult"; +import type { IIsCloseToTimeoutCallable } from "@webiny/tasks"; + +export interface ICmsEntryZipperExecuteParams { + isCloseToTimeout: IIsCloseToTimeoutCallable; + isAborted(): boolean; + model: CmsModel; + after: string | undefined; + exportAssets: boolean; +} + +export type ICmsEntryZipperExecuteResult = + | ICmsEntryZipperExecuteContinueResult + | ICmsEntryZipperExecuteDoneResult; + +export interface ICmsEntryZipper { + execute(params: ICmsEntryZipperExecuteParams): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipperExecuteContinueResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipperExecuteContinueResult.ts new file mode 100644 index 00000000000..6bcc5c37134 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipperExecuteContinueResult.ts @@ -0,0 +1,5 @@ +export interface ICmsEntryZipperExecuteContinueResult { + key: string; + checksum: string; + cursor: string | null; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipperExecuteDoneResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipperExecuteDoneResult.ts new file mode 100644 index 00000000000..3ca6b825089 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/abstractions/CmsEntryZipperExecuteDoneResult.ts @@ -0,0 +1,4 @@ +export interface ICmsEntryZipperExecuteDoneResult { + key: string; + checksum: string; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/index.ts new file mode 100644 index 00000000000..ae02cfcf82c --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/cmsEntryZipper/index.ts @@ -0,0 +1,6 @@ +export * from "./CmsEntryZipper"; +export * from "./CmsEntryZipperExecuteContinueResult"; +export * from "./CmsEntryZipperExecuteDoneResult"; +export * from "./abstractions/CmsEntryZipperExecuteDoneResult"; +export * from "./abstractions/CmsEntryZipperExecuteContinueResult"; +export * from "./abstractions/CmsEntryZipper"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/CompressedFileReader.ts b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/CompressedFileReader.ts new file mode 100644 index 00000000000..9b78c1e7d8e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/CompressedFileReader.ts @@ -0,0 +1,35 @@ +import { Open } from "unzipper"; +import type { S3Client } from "../helpers/s3Client"; +import type { IUnzipperFile } from "./abstractions/Decompressor"; +import type { ICompressedFileReader } from "./abstractions/CompressedFileReader"; + +export interface ICompressedFileReaderParams { + client: S3Client; + bucket: string; +} + +export class CompressedFileReader implements ICompressedFileReader { + private readonly client: S3Client; + private readonly bucket: string; + + public constructor(params: ICompressedFileReaderParams) { + this.client = params.client; + this.bucket = params.bucket; + } + + public async read(target: string): Promise { + const result = await Open.s3_v3(this.client, { + Bucket: this.bucket, + Key: target + }); + return result.files.filter(file => { + return file.type === "File"; + }); + } +} + +export const createCompressedFileReader = ( + params: ICompressedFileReaderParams +): ICompressedFileReader => { + return new CompressedFileReader(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/Decompressor.ts b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/Decompressor.ts new file mode 100644 index 00000000000..98ec6aae065 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/Decompressor.ts @@ -0,0 +1,121 @@ +import type { + IDecompressor, + IDecompressorDecompressParams, + IUnzipperFile +} from "./abstractions/Decompressor"; +import type { + IMultipartUploadFactory, + IMultipartUploadHandlerAddResult, + IUploadDoneResult +} from "~/tasks/utils/upload"; +import type { Entry } from "unzipper"; +import { PassThrough } from "stream"; + +export interface IDecompressorParamsUploadCreateFactory { + (filename: string): IMultipartUploadFactory; +} + +export interface IDecompressorParams { + createUploadFactory: IDecompressorParamsUploadCreateFactory; +} + +export class Decompressor implements IDecompressor { + private readonly createUploadFactory: IDecompressorParamsUploadCreateFactory; + + public constructor(params: IDecompressorParams) { + this.createUploadFactory = params.createUploadFactory; + } + /** + * Should not be used with large files (> 10/20MB) + */ + public async read(files: IUnzipperFile[], target: string): Promise { + const file = files.find(f => f.path === target); + if (!file) { + throw new Error(`File "${target}" not found in the compressed file.`); + } + + const buffer = await file.buffer(); + + return buffer.toString(); + } + + public async extract(params: IDecompressorDecompressParams): Promise { + const { source, target } = params; + + const factory = this.createUploadFactory(target); + + const multipartUpload = await factory.start(); + + const promises: Promise[] = []; + + const localStream = new PassThrough({ + autoDestroy: true + }) + .on("error", err => { + console.log("Decompressor Local Stream Error", err); + throw err; + }) + .on("data", data => { + const p = multipartUpload.add(data); + promises.push(p); + }); + + let stream: Entry; + try { + stream = source.stream(); + } catch (ex) { + console.error(`Failed to create stream for "${source.path}".`, ex); + throw ex; + } + return new Promise((resolve, reject) => { + let alreadyDone = false; + let alreadyError = false; + + const done = async (): Promise => { + if (alreadyDone) { + return; + } + alreadyDone = true; + try { + await Promise.all(promises); + const result = await multipartUpload.complete(); + resolve(result.result); + } catch (ex) { + console.error("Failed to upload file.", ex); + multipartUpload.abort(); + reject(ex); + } finally { + stream.destroy(); + } + }; + + const error = async (err: Error): Promise => { + if (alreadyError) { + return; + } + alreadyError = true; + try { + await multipartUpload.abort(); + reject(err); + } catch (ex) { + reject(ex); + } finally { + stream.destroy(); + } + }; + + stream + .pipe(localStream) + .on("finish", () => { + done(); + }) + .on("error", err => { + error(err); + }); + }); + } +} + +export const createDecompressor = (params: IDecompressorParams): IDecompressor => { + return new Decompressor(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/abstractions/CompressedFileReader.ts b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/abstractions/CompressedFileReader.ts new file mode 100644 index 00000000000..7959093cc41 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/abstractions/CompressedFileReader.ts @@ -0,0 +1,5 @@ +import type { IUnzipperFile } from "~/tasks/utils/decompressor"; + +export interface ICompressedFileReader { + read(target: string): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/abstractions/Decompressor.ts b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/abstractions/Decompressor.ts new file mode 100644 index 00000000000..04014b9d961 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/abstractions/Decompressor.ts @@ -0,0 +1,14 @@ +import type { IUploadDoneResult } from "~/tasks/utils/upload"; +import type { File as IUnzipperFile } from "unzipper"; + +export type { IUnzipperFile }; + +export interface IDecompressorDecompressParams { + source: IUnzipperFile; + target: string; +} + +export interface IDecompressor { + read(files: IUnzipperFile[], target: string): Promise; + extract(params: IDecompressorDecompressParams): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/index.ts new file mode 100644 index 00000000000..65962add385 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/decompressor/index.ts @@ -0,0 +1,4 @@ +export * from "./abstractions/CompressedFileReader"; +export * from "./abstractions/Decompressor"; +export * from "./CompressedFileReader"; +export * from "./Decompressor"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/deleteFiles/DeleteFiles.ts b/packages/api-headless-cms-import-export/src/tasks/utils/deleteFiles/DeleteFiles.ts new file mode 100644 index 00000000000..3c2d48015ef --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/deleteFiles/DeleteFiles.ts @@ -0,0 +1,57 @@ +import type { + IDeleteFiles, + IDeleteFilesExecuteInput +} from "~/tasks/utils/deleteFiles/abstractions/DeleteFiles"; +import { createS3Client } from "../helpers/s3Client"; +import { FileFetcher } from "~/tasks/utils/fileFetcher"; +import type { IFileFetcher } from "~/tasks/utils/fileFetcher"; +import { getBucket } from "../helpers/getBucket"; + +export interface IDeleteFilesParams { + fileFetcher: IFileFetcher; +} + +export class DeleteFiles implements IDeleteFiles { + private readonly fileFetcher: IFileFetcher; + + public constructor(params: IDeleteFilesParams) { + this.fileFetcher = params.fileFetcher; + } + + public async execute(input: IDeleteFilesExecuteInput): Promise { + if (!input) { + return; + } + const files = (Array.isArray(input) ? input : [input]).filter((file): file is string => { + return !!file; + }); + for (const file of files) { + const exists = await this.fileFetcher.exists(file); + if (!exists) { + continue; + } + try { + const result = await this.fileFetcher.delete(file); + if (!result.$metadata) { + continue; + } + if (result.$metadata.httpStatusCode !== 200) { + console.log(`Failed to delete file "${file}".`); + } + } catch (ex) { + console.log(`Failed to delete file "${file}".`, ex); + } + } + } +} + +export const createDeleteFiles = (): IDeleteFiles => { + const client = createS3Client(); + const bucket = getBucket(); + return new DeleteFiles({ + fileFetcher: new FileFetcher({ + client, + bucket + }) + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/deleteFiles/abstractions/DeleteFiles.ts b/packages/api-headless-cms-import-export/src/tasks/utils/deleteFiles/abstractions/DeleteFiles.ts new file mode 100644 index 00000000000..d3860881aa4 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/deleteFiles/abstractions/DeleteFiles.ts @@ -0,0 +1,9 @@ +export type IDeleteFilesExecuteInput = string | string[] | undefined | null; + +export interface IDeleteFiles { + /** + * This method should take a file or an array of file paths and delete them. + * It should log errors if any occur. + */ + execute(input: IDeleteFilesExecuteInput): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/EntryAssets.ts b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/EntryAssets.ts new file mode 100644 index 00000000000..9dbabe130e6 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/EntryAssets.ts @@ -0,0 +1,79 @@ +import type { IContentEntryTraverser } from "@webiny/api-headless-cms"; +import { matchKeyOrAlias } from "~/tasks/utils/helpers/matchKeyOrAlias"; +import type { IAsset, IAssignAssetsInput, IEntryAssets } from "./abstractions/EntryAssets"; +import type { GenericRecord } from "@webiny/api/types"; +import type { IUniqueResolver } from "~/tasks/utils/uniqueResolver/abstractions/UniqueResolver"; + +export interface IEntryAssetsParams { + traverser: IContentEntryTraverser; + uniqueResolver: IUniqueResolver; +} + +const fileTypes: string[] = ["file"]; + +export class EntryAssets implements IEntryAssets { + private readonly uniqueResolver: IUniqueResolver; + + private readonly traverser: IContentEntryTraverser; + + public constructor(params: IEntryAssetsParams) { + this.traverser = params.traverser; + this.uniqueResolver = params.uniqueResolver; + } + + public async assignAssets(input: IAssignAssetsInput): Promise { + const entries = Array.isArray(input) ? input : [input]; + if (entries.length === 0) { + return []; + } + + const assets: IAsset[] = []; + + for (const entry of entries) { + if (!entry?.values) { + continue; + } + await this.traverser.traverse(entry.values, ({ field, value }) => { + if (!value || fileTypes.includes(field.type) === false) { + return; + } + + assets.push(...this.assignAssetsToItems(value)); + }); + } + return assets; + } + + private parseAssetSrc(input?: string | unknown): IAsset | null { + if (!input || typeof input !== "string" || !input.trim()) { + return null; + } + + const result = matchKeyOrAlias(input); + if (!result) { + return null; + } + return { + ...result, + url: input + }; + } + + private assignAssetsToItems(input: string | string[] | unknown): IAsset[] { + const assets: GenericRecord = {}; + if (!input) { + return []; + } + const inputArray: string[] = Array.isArray(input) ? input : [input]; + for (const src of inputArray) { + const asset = this.parseAssetSrc(src); + if (!asset) { + continue; + } else if (assets[asset.url]) { + continue; + } + assets[asset.url] = asset; + } + return this.uniqueResolver.resolve(Object.values(assets), "url"); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/EntryAssetsResolver.ts b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/EntryAssetsResolver.ts new file mode 100644 index 00000000000..00d178baebf --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/EntryAssetsResolver.ts @@ -0,0 +1,109 @@ +import type { + File, + FileListMeta, + FileListWhereParams, + FilesListOpts +} from "@webiny/api-file-manager/types"; +import type { IEntryAssetsResolver, IResolvedAsset } from "./abstractions/EntryAssetsResolver"; +import type { IAsset } from "./abstractions/EntryAssets"; + +export interface IFetchFilesCbResult { + items: File[]; + meta: FileListMeta; +} + +export interface IFetchFilesCb { + (opts?: FilesListOpts): Promise; +} + +export interface IEntryAssetsResolverParams { + fetchFiles: IFetchFilesCb; +} + +const createResolvedAsset = (file: File): IResolvedAsset => { + const result: IResolvedAsset = { + ...file, + aliases: file.aliases || [] + }; + /** + * We need to remove unnecessary fields from the resolved assets. + * + * We cannot return specific fields, rather than deleting unnecessary ones, because a user can extend the file model + * so we would not know which fields to return. + */ + delete result.savedBy; + delete result.savedOn; + delete result.modifiedBy; + delete result.modifiedOn; + delete result.accessControl; + delete result.createdBy; + delete result.createdOn; + delete result.tenant; + delete result.locale; + delete result.webinyVersion; + + return result; +}; + +export class EntryAssetsResolver implements IEntryAssetsResolver { + private readonly fetchFiles: IFetchFilesCb; + + public constructor(params: IEntryAssetsResolverParams) { + this.fetchFiles = params.fetchFiles; + } + + public async resolve(input: IAsset[]): Promise { + const keys: string[] = []; + const aliases: string[] = []; + for (const asset of input) { + if (asset.key) { + keys.push(asset.key); + } else if (asset.alias) { + aliases.push(asset.alias); + } + } + + const assets: IResolvedAsset[] = []; + const where: FileListWhereParams = {}; + if (keys.length > 0 && aliases.length > 0) { + where.OR = [ + { + key_in: keys + }, + { + aliases_in: aliases + } + ]; + } else if (keys.length > 0) { + where.key_in = keys; + } else if (aliases.length > 0) { + where.aliases_in = aliases; + } else { + return assets; + } + + const fetch = async (after?: string) => { + return this.fetchFiles({ + where, + limit: 10000000, + sort: ["id_ASC"], + after + }); + }; + + let after: string | undefined = undefined; + while (true) { + /** + * Unfortunately we must cast the result, because TS is not able to infer the correct type. + */ + const { items, meta } = (await fetch(after)) as IFetchFilesCbResult; + for (const file of items) { + assets.push(createResolvedAsset(file)); + } + if (!meta.hasMoreItems) { + return assets; + } + after = meta.cursor || undefined; + } + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/abstractions/EntryAssets.ts b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/abstractions/EntryAssets.ts new file mode 100644 index 00000000000..c72e08dce4d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/abstractions/EntryAssets.ts @@ -0,0 +1,28 @@ +import type { CmsEntry } from "@webiny/api-headless-cms/types"; +import type { GenericRecord } from "@webiny/api/types"; + +export interface IKeyAsset { + key: string; + alias?: never; + url: string; +} + +export interface IAliasAsset { + key?: never; + alias: string; + url: string; +} + +export type IAsset = IKeyAsset | IAliasAsset; + +export type IAssets = GenericRecord; + +export type IAssignAssetsInput = Pick | Pick[]; + +export interface IEntryAssets { + /** + * The output of this method is a list of assets that were found in the given input. + * If there were any duplicates, they will not be included in the output. + */ + assignAssets(input: IAssignAssetsInput): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/abstractions/EntryAssetsResolver.ts b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/abstractions/EntryAssetsResolver.ts new file mode 100644 index 00000000000..d80a3869c1f --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/abstractions/EntryAssetsResolver.ts @@ -0,0 +1,8 @@ +import type { IAsset } from "./EntryAssets"; +import type { File } from "@webiny/api-file-manager/types/file"; + +export type IResolvedAsset = Omit; + +export interface IEntryAssetsResolver { + resolve(input: IAsset[]): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/index.ts new file mode 100644 index 00000000000..a65d10ddbfb --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/entryAssets/index.ts @@ -0,0 +1,4 @@ +export * from "./abstractions/EntryAssets"; +export * from "./abstractions/EntryAssetsResolver"; +export * from "./EntryAssets"; +export * from "./EntryAssetsResolver"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/ExternalFileFetcher.ts b/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/ExternalFileFetcher.ts new file mode 100644 index 00000000000..bdc067e4dd9 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/ExternalFileFetcher.ts @@ -0,0 +1,136 @@ +import type { + IExternalFileFetcher, + IExternalFileFetcherFetchResult, + IExternalFileFetcherHeadResult +} from "./abstractions/ExternalFileFetcher"; +import { getObjectProperties } from "@webiny/utils"; +import { WebinyError } from "@webiny/error"; + +export interface IGetChecksumHeaderCallable { + (headers: Headers): string | undefined | null; +} + +export interface IExternalFileFetcherParams { + fetcher: typeof fetch; + timeout?: number; + getChecksumHeader: IGetChecksumHeaderCallable; +} + +const defaultTimeout = 5; + +export class ExternalFileFetcher implements IExternalFileFetcher { + private readonly fetcher: typeof fetch; + private readonly timeout: number = defaultTimeout; + private readonly _getChecksumHeader: IGetChecksumHeaderCallable; + + public constructor(params: IExternalFileFetcherParams) { + this.fetcher = params.fetcher; + this.timeout = params.timeout || defaultTimeout; + this._getChecksumHeader = params.getChecksumHeader; + } + + public async fetch(url: string): Promise { + try { + const result = await this.fetcher(url, { + method: "GET" + }); + const contentType = result.headers.get("content-type"); + if (!contentType) { + throw new Error(`Content type not found for URL: ${url}`); + } + const contentLengthString = result.headers.get("content-length"); + const contentLength = contentLengthString ? parseInt(contentLengthString) : 0; + if (contentLength === 0) { + throw new Error(`Content length not found for URL: ${url}`); + } + const checksum = this.getChecksumHeader(result.headers); + if (!checksum) { + throw new Error(`ETag not found for URL: ${url}`); + } + if (!result.body) { + throw new Error(`Body not found for URL: ${url}`); + } + return { + file: { + name: url.split("/").pop() as string, + size: contentLength, + url, + contentType, + body: result.body, + checksum + } + }; + } catch (ex) { + const error = getObjectProperties(ex); + return { + error: { + ...error, + code: error.code || "GET_FETCH_ERROR", + data: { + ...error.data, + url + } + } + }; + } + } + + public async head(url: string): Promise { + const abort = new AbortController(); + try { + /** + * We will allow $timeout seconds for the HEAD request to complete. + */ + const tId = setTimeout(() => { + abort.abort("Timeout."); + }, this.timeout * 1000); + const result = await this.fetcher(url, { + method: "HEAD", + signal: abort.signal + }); + /** + * And clear timeout as soon as the request is completed. + */ + clearTimeout(tId); + if (result.status !== 200) { + throw new Error(`Failed to fetch URL: ${url}. Status: ${result.status}`); + } + const contentType = result.headers.get("content-type"); + if (!contentType) { + throw new Error(`Content type not found for URL: ${url}`); + } + const checksum = this.getChecksumHeader(result.headers); + if (!checksum) { + throw new Error(`ETag not found for URL: ${url}`); + } + const contentLength = result.headers.get("content-length"); + + return { + file: { + name: url.split("/").pop() as string, + size: parseInt(contentLength || "0"), + url, + contentType, + checksum + } + }; + } catch (ex) { + const error = getObjectProperties(ex); + console.error(error); + return { + error: { + ...error, + code: error.code || "HEAD_FETCH_ERROR", + data: { + ...error.data, + url + } + } + }; + } + } + + private getChecksumHeader(headers: Headers): string | undefined | null { + return this._getChecksumHeader(headers); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/abstractions/ExternalFileFetcher.ts b/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/abstractions/ExternalFileFetcher.ts new file mode 100644 index 00000000000..aa07a657910 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/abstractions/ExternalFileFetcher.ts @@ -0,0 +1,52 @@ +import type { GenericRecord } from "@webiny/api/types"; + +export interface IExternalFileFetcherHeadFile { + url: string; + name: string; + size: number; + contentType: string; + checksum: string; +} + +export interface IExternalFileFetcherFetchFile extends IExternalFileFetcherHeadFile { + body: ReadableStream; +} + +export interface IExternalFileFetcherError { + message: string; + code: string; + data: GenericRecord; +} + +export type IExternalFileFetcherHeadResult = + | { + file: IExternalFileFetcherHeadFile; + error?: never; + } + | { + file?: never; + error: IExternalFileFetcherError; + }; + +export type IExternalFileFetcherFetchResult = + | { + file: IExternalFileFetcherFetchFile; + error?: never; + } + | { + file?: never; + error: IExternalFileFetcherError; + }; + +export interface IExternalFileFetcherFetchCallable { + (key: string): Promise; +} + +export interface IExternalFileFetcherHeadCallable { + (key: string): Promise; +} + +export interface IExternalFileFetcher { + fetch: IExternalFileFetcherFetchCallable; + head: IExternalFileFetcherHeadCallable; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/index.ts new file mode 100644 index 00000000000..bed07ae556a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/externalFileFetcher/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/ExternalFileFetcher"; +export * from "./ExternalFileFetcher"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/FileFetcher.ts b/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/FileFetcher.ts new file mode 100644 index 00000000000..ce97efed325 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/FileFetcher.ts @@ -0,0 +1,146 @@ +import type { DeleteObjectCommandOutput, S3Client } from "@webiny/aws-sdk/client-s3"; +import { + DeleteObjectCommand, + GetObjectCommand, + HeadObjectCommand, + ListObjectsCommand +} from "@webiny/aws-sdk/client-s3"; +import { basename } from "path"; +import type { + IFileFetcher, + IFileFetcherFetchResult, + IFileFetcherFile, + IFileFetcherHeadResult, + IFileFetcherStream +} from "./abstractions/FileFetcher"; + +export interface IFileFetcherParams { + client: S3Client; + bucket: string; +} + +export class FileFetcher implements IFileFetcher { + public readonly client: S3Client; + public readonly bucket: string; + + public constructor(params: IFileFetcherParams) { + this.client = params.client; + this.bucket = params.bucket; + } + + public async head(key: string): Promise { + try { + const cmd = new HeadObjectCommand({ + Key: key, + Bucket: this.bucket + }); + + return await this.client.send(cmd); + } catch (ex) { + return null; + } + } + + public async exists(key: string): Promise { + try { + const result = await this.head(key); + if (!result) { + return false; + } + if (!result.$metadata) { + return false; + } + return result.$metadata?.httpStatusCode === 200; + } catch (ex) { + return false; + } + } + + public async list(key: string): Promise { + try { + const result = await this.client.send( + new ListObjectsCommand({ + Bucket: this.bucket, + Prefix: key + }) + ); + if (!Array.isArray(result.Contents)) { + return []; + } + + const items: IFileFetcherFile[] = []; + for (const item of result.Contents) { + if (!item.Key) { + continue; + } + items.push({ + name: basename(item.Key), + key: item.Key, + /** + * TODO: Is it really possible that there is no size of the file??? + */ + size: item.Size || 0 + }); + } + return items.sort((a, b) => a.key.localeCompare(b.key)); + } catch (ex) { + console.error(ex); + return []; + } + } + + public async fetch(key: string): Promise { + try { + return await this.client.send( + new GetObjectCommand({ + Bucket: this.bucket, + Key: key + }) + ); + } catch (ex) { + console.log(`Could not fetch file "${key}" from bucket "${this.bucket}".`); + console.error(ex); + return null; + } + } + + public async stream(key: string): Promise { + try { + const response = await this.fetch(key); + if (!response) { + return null; + } + /** + * We can safely cast because we are sure that the response will be readable. + * The method which is using the fetch() should handle the case when the response is null. + */ + return (response.Body || null) as IFileFetcherStream; + } catch (ex) { + console.log(`Could not fetch file "${key}" from bucket "${this.bucket}".`); + console.error(ex); + return null; + } + } + + public async read(key: string): Promise { + const response = await this.fetch(key); + if (!response?.Body) { + return null; + } + try { + return await response.Body.transformToString(); + } catch (ex) { + console.log(`Could not read file "${key}" from bucket "${this.bucket}".`); + console.error(ex); + return null; + } + } + + public async delete(key: string): Promise { + const cmd = new DeleteObjectCommand({ + Key: key, + Bucket: this.bucket + }); + return await this.client.send(cmd); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/abstractions/FileFetcher.ts b/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/abstractions/FileFetcher.ts new file mode 100644 index 00000000000..833141dd405 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/abstractions/FileFetcher.ts @@ -0,0 +1,55 @@ +import type { + DeleteObjectCommandOutput, + GetObjectCommandOutput, + HeadObjectCommandOutput +} from "@webiny/aws-sdk/client-s3"; +import type { Readable } from "stream"; + +export interface IFileFetcherFile { + key: string; + name: string; + size: number; +} + +export type IFileFetcherStream = Readable | null; +export type IFileFetcherFetchResult = GetObjectCommandOutput | null; + +export interface IFileFetcherExistsCallable { + (key: string): Promise; +} + +export type IFileFetcherHeadResult = HeadObjectCommandOutput | null; + +export interface IFileFetcherHeadCallable { + (key: string): Promise; +} + +export interface IFileFetcherListCallable { + (key: string): Promise; +} + +export interface IFileFetcherFetchCallable { + (key: string): Promise; +} + +export interface IFileFetcherStreamCallable { + (key: string): Promise; +} + +export interface IFileFetcherReadCallable { + (key: string): Promise; +} + +export interface IFileFetcherDeleteCallable { + (key: string): Promise; +} + +export interface IFileFetcher { + exists: IFileFetcherExistsCallable; + head: IFileFetcherHeadCallable; + list: IFileFetcherListCallable; + fetch: IFileFetcherFetchCallable; + stream: IFileFetcherStreamCallable; + read: IFileFetcherReadCallable; + delete: IFileFetcherDeleteCallable; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/index.ts new file mode 100644 index 00000000000..6ec8bc1cc4b --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/fileFetcher/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/FileFetcher"; +export * from "./FileFetcher"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/cleanChecksum.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/cleanChecksum.ts new file mode 100644 index 00000000000..ac08417c2be --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/cleanChecksum.ts @@ -0,0 +1,3 @@ +export const cleanChecksum = (input: string): string => { + return input.replaceAll('"', ""); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/exportPath.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/exportPath.ts new file mode 100644 index 00000000000..2a512cd243c --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/exportPath.ts @@ -0,0 +1,9 @@ +import { EXPORT_BASE_PATH } from "~/tasks/constants"; + +export const stripExportPath = (key: string): string => { + return key.replace(EXPORT_BASE_PATH, "").replace(/^\/+/, ""); +}; + +export const prependExportPath = (key: string): string => { + return `${EXPORT_BASE_PATH}/${key.replace(/^\/+/, "")}`; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getBackOffSeconds.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getBackOffSeconds.ts new file mode 100644 index 00000000000..84934481d1d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getBackOffSeconds.ts @@ -0,0 +1,9 @@ +export const min = 10; +export const max = 90; +export const step = 10; +/** + * This function will increase the backoff time exponentially with each iteration, after the minimum backoff time, ofc... + */ +export const getBackOffSeconds = (iterations: number) => { + return Math.min(max, Math.max(min, iterations * step)); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getBucket.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getBucket.ts new file mode 100644 index 00000000000..fe6fb6e9cda --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getBucket.ts @@ -0,0 +1,15 @@ +import { WebinyError } from "@webiny/error"; + +export const getBucket = (): string => { + const bucket = process.env.S3_BUCKET; + if ( + !bucket || + bucket === "undefined" || + bucket === "null" || + typeof bucket !== "string" || + !bucket.trim() + ) { + throw new WebinyError(`Missing S3_BUCKET environment variable.`, "S3_BUCKET_ERROR"); + } + return bucket; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getFilePath.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getFilePath.ts new file mode 100644 index 00000000000..07cacc7c1c9 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getFilePath.ts @@ -0,0 +1,19 @@ +export interface IGetFilePathResult { + path: string; + filename: string; +} + +export const getFilePath = (file: string): IGetFilePathResult => { + const parts = file.split("/").filter(Boolean); + if (parts.length === 1) { + return { + path: "", + filename: parts.join("/") + }; + } + const filename = parts.pop() as string; + return { + path: parts.join("/"), + filename + }; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getImportExportFileType.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getImportExportFileType.ts new file mode 100644 index 00000000000..463e5baa71d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/getImportExportFileType.ts @@ -0,0 +1,50 @@ +import { WEBINY_EXPORT_ASSETS_EXTENSION, WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; +import { CmsImportExportFileType } from "~/types"; + +interface SuccessResponse { + type: CmsImportExportFileType; + pathname: string; + error?: never; +} + +interface ErrorResponse { + type: string | undefined; + pathname: string; + error: true; +} + +export type Response = SuccessResponse | ErrorResponse; + +export const getImportExportFileType = (input: string): Response => { + const result = new URL(input); + const pathname = result.pathname; + if (pathname.endsWith(WEBINY_EXPORT_ENTRIES_EXTENSION)) { + return { + type: CmsImportExportFileType.ENTRIES, + pathname + }; + } else if (pathname.endsWith(WEBINY_EXPORT_ASSETS_EXTENSION)) { + return { + type: CmsImportExportFileType.ASSETS, + pathname + }; + } + + if (pathname.includes(".") === false) { + return { + type: undefined, + pathname, + error: true + }; + } + const extensions = pathname.split("."); + extensions.shift(); + + const type = extensions.join("."); + + return { + type, + pathname, + error: true + }; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/importPath.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/importPath.ts new file mode 100644 index 00000000000..a010dc03ddd --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/importPath.ts @@ -0,0 +1,9 @@ +import { IMPORT_BASE_PATH } from "~/tasks/constants"; + +export const stripImportPath = (key: string): string => { + return key.replace(IMPORT_BASE_PATH, "").replace(/^\/+/, ""); +}; + +export const prependImportPath = (key: string): string => { + return `${IMPORT_BASE_PATH}/${key.replace(/^\/+/, "")}`; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/matchKeyOrAlias.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/matchKeyOrAlias.ts new file mode 100644 index 00000000000..3e9cde85cfd --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/matchKeyOrAlias.ts @@ -0,0 +1,36 @@ +interface IMatchOutput { + alias?: never; + key: string; +} + +interface IMatchAliasOutput { + key?: never; + alias: string; +} + +export const matchKeyOrAlias = (input: string): IMatchAliasOutput | IMatchOutput | null => { + try { + const url = new URL(input); + const { pathname } = url; + const isFiles = pathname.startsWith("/files/"); + const isPrivate = pathname.startsWith("/private/"); + if (!isFiles && !isPrivate) { + return { + alias: pathname + }; + } else if (!isPrivate) { + return { + key: pathname.replace(/^\/files\//, "") + }; + } + return { + key: pathname.replace(/^\/private\//, "") + }; + } catch (ex) { + if (process.env.DEBUG === "true") { + console.error(`Url "${input}" is not valid.`); + console.error(ex); + } + return null; + } +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/s3Client.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/s3Client.ts new file mode 100644 index 00000000000..b93cff06b04 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/s3Client.ts @@ -0,0 +1,34 @@ +import { NodeHttpHandler } from "@smithy/node-http-handler"; +import type { S3Client, S3ClientConfig } from "@webiny/aws-sdk/client-s3"; +import { createS3Client as baseCreateS3Client } from "@webiny/aws-sdk/client-s3"; +import { Agent as HttpAgent } from "http"; +import { Agent as HttpsAgent } from "https"; + +export type { S3Client }; + +export const createS3Client = (params?: S3ClientConfig): S3Client => { + return baseCreateS3Client({ + requestHandler: new NodeHttpHandler({ + connectionTimeout: 0, + socketTimeout: 0, + requestTimeout: 0, + httpAgent: new HttpAgent({ + maxSockets: Infinity, + keepAlive: true, + maxFreeSockets: Infinity, + maxTotalSockets: Infinity, + keepAliveMsecs: 900000 // milliseconds / 15 minutes + }), + httpsAgent: new HttpsAgent({ + maxSockets: Infinity, + keepAlive: true, + sessionTimeout: 900, // seconds / 15 minutes + maxCachedSessions: 100000, + maxFreeSockets: Infinity, + maxTotalSockets: Infinity, + keepAliveMsecs: 900000 // milliseconds / 15 minutes + }) + }), + ...params + }); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/helpers/sizeSegments.ts b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/sizeSegments.ts new file mode 100644 index 00000000000..c34b71c3f5e --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/helpers/sizeSegments.ts @@ -0,0 +1,27 @@ +import bytes from "bytes"; + +export interface ISegment { + start: number; + end: number; +} + +export const createSizeSegments = ( + fileSize: number, + segmentSizeInput: `${number}${string}` | number +): ISegment[] => { + const segmentSize = + typeof segmentSizeInput === "number" ? segmentSizeInput : bytes.parse(segmentSizeInput); + + const segments: ISegment[] = []; + let segmentIndex = 0; + for (let start = 0; start < fileSize; start += segmentSize + 1) { + const end = start + segmentSize > fileSize ? fileSize : start + segmentSize; + segments[segmentIndex] = { + start, + end + }; + segmentIndex++; + } + + return segments; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/types.ts b/packages/api-headless-cms-import-export/src/tasks/utils/types.ts new file mode 100644 index 00000000000..8d8006fcb03 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/types.ts @@ -0,0 +1,27 @@ +import type { IAsset, IResolvedAsset } from "./entryAssets"; +import type { SanitizedCmsModel } from "@webiny/api-headless-cms/export/types"; +import type { GenericRecord, NonEmptyArray } from "@webiny/api/types"; +import type { CmsEntryMeta } from "@webiny/api-headless-cms/types"; + +export interface IFileMeta { + id: number; + name: string; + after?: string | null; +} + +export interface ICmsEntryManifestJson { + files: IFileMeta[]; + assets?: IAsset[]; + model: SanitizedCmsModel; +} + +export interface ICmsAssetsManifestJson { + assets: NonEmptyArray; + size: number; +} + +export interface ICmsEntryEntriesJson { + items: GenericRecord[]; + meta: CmsEntryMeta; + after?: string; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/uniqueResolver/UniqueResolver.ts b/packages/api-headless-cms-import-export/src/tasks/utils/uniqueResolver/UniqueResolver.ts new file mode 100644 index 00000000000..8f86911812d --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/uniqueResolver/UniqueResolver.ts @@ -0,0 +1,18 @@ +import type { IUniqueResolver } from "./abstractions/UniqueResolver"; +import type { GenericRecord } from "@webiny/api/types"; + +export class UniqueResolver implements IUniqueResolver { + private readonly resolved = new Set(); + + public resolve(input: T[], field: keyof T): T[] { + return input.reduce((assets, asset) => { + const value = asset[field]; + if (this.resolved.has(value)) { + return assets; + } + this.resolved.add(value); + assets.push(asset); + return assets; + }, []); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/uniqueResolver/abstractions/UniqueResolver.ts b/packages/api-headless-cms-import-export/src/tasks/utils/uniqueResolver/abstractions/UniqueResolver.ts new file mode 100644 index 00000000000..cf2987e54c8 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/uniqueResolver/abstractions/UniqueResolver.ts @@ -0,0 +1,5 @@ +import type { GenericRecord } from "@webiny/api/types"; + +export interface IUniqueResolver { + resolve(assets: T[], field: keyof T): T[]; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/MultipartUploadFactory.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/MultipartUploadFactory.ts new file mode 100644 index 00000000000..53a7c5f7747 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/MultipartUploadFactory.ts @@ -0,0 +1,144 @@ +import type { ListPartsCommandOutput, S3Client } from "@webiny/aws-sdk/client-s3"; +import { CreateMultipartUploadCommand, ListPartsCommand } from "@webiny/aws-sdk/client-s3"; +import { WebinyError } from "@webiny/error"; +import { + ICreateMultipartUploadHandler, + IMultipartUploadHandler, + IPart +} from "./abstractions/MultipartUploadHandler"; +import { + IMultipartUploadFactory, + IMultipartUploadFactoryContinueParams +} from "./abstractions/MultipartUploadFactory"; + +export interface IMultipartUploadFactoryParams { + client: S3Client; + bucket: string; + filename: string; + createHandler: ICreateMultipartUploadHandler; +} + +export class MultipartUploadFactory implements IMultipartUploadFactory { + private readonly client: S3Client; + private readonly bucket: string; + private readonly filename: string; + private readonly createHandler: ICreateMultipartUploadHandler; + + public constructor(params: IMultipartUploadFactoryParams) { + this.client = params.client; + this.bucket = params.bucket; + this.filename = params.filename; + this.createHandler = params.createHandler; + } + + public async start( + params?: IMultipartUploadFactoryContinueParams + ): Promise { + const resumeUploadId = params?.uploadId; + if (resumeUploadId !== undefined) { + return this.continue({ + uploadId: resumeUploadId + }); + } + const cmd = new CreateMultipartUploadCommand({ + Bucket: this.bucket, + Key: this.filename + }); + + const result = await this.client.send(cmd); + const uploadId = result.UploadId; + if (uploadId) { + return this.createHandler({ + uploadId, + client: this.client, + bucket: this.bucket, + filename: this.filename, + parts: undefined + }); + } + throw new WebinyError({ + message: "Could not initiate multipart upload.", + code: "S3_ERROR", + data: { + bucket: this.bucket, + filename: this.filename + } + }); + } + + private async continue( + params: Required + ): Promise { + const result = await this.listParts({ + uploadId: params.uploadId + }); + + const parts = result.Parts.map(part => { + if (!part.ETag || part.PartNumber === undefined) { + return null; + } + return { + tag: part.ETag.replaceAll('"', ""), + partNumber: part.PartNumber + }; + }) + .filter((part): part is IPart => !!part) + .sort((a, b) => { + return a.partNumber - b.partNumber; + }); + return this.createHandler({ + client: this.client, + bucket: this.bucket, + filename: this.filename, + uploadId: params.uploadId, + parts + }); + } + + private async listParts( + params: Required + ): Promise> { + const cmd = new ListPartsCommand({ + Bucket: this.bucket, + Key: this.filename, + UploadId: params.uploadId + }); + + let result: ListPartsCommandOutput; + try { + result = await this.client.send(cmd); + } catch (ex) { + throw new WebinyError({ + message: `Failed to list parts: ${ex.message}`, + code: "S3_ERROR", + data: { + metadata: ex.$metadata, + bucket: this.bucket, + filename: this.filename, + uploadId: params.uploadId + } + }); + } + + if (!result.UploadId || !result.Parts?.length) { + throw new WebinyError({ + message: "Could not find the upload.", + code: "S3_ERROR", + data: { + bucket: this.bucket, + filename: this.filename, + uploadId: params.uploadId + } + }); + } + return result as Required; + } +} + +export interface ICreateMultipartUploadFactoryCallable { + (params: IMultipartUploadFactoryParams): IMultipartUploadFactory; +} + +export const createMultipartUploadFactory: ICreateMultipartUploadFactoryCallable = params => { + return new MultipartUploadFactory(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/Upload.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/Upload.ts new file mode 100644 index 00000000000..aaec9ab7466 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/Upload.ts @@ -0,0 +1,108 @@ +import type { Options as BaseUploadOptions } from "@webiny/aws-sdk/lib-storage"; +import { Upload as BaseUpload } from "@webiny/aws-sdk/lib-storage"; +import type { Transform } from "stream"; +import { PassThrough } from "stream"; +import type { + CompleteMultipartUploadCommandOutput, + PutObjectCommandInput, + S3Client +} from "@webiny/aws-sdk/client-s3"; +import { IAwsUpload, IUpload, IUploadOnListener } from "./abstractions/Upload"; +import { getContentType } from "./getContentType"; + +export interface IUploadConfig { + client: S3Client; + stream: PassThrough; + bucket: string; + filename: string; + factory?(params: BaseUploadOptions): IAwsUpload; + queueSize?: number; +} + +const defaultFactory = (options: BaseUploadOptions): IAwsUpload => { + return new BaseUpload(options); +}; + +export class Upload implements IUpload { + public readonly stream: PassThrough; + public readonly upload: IAwsUpload; + public readonly client: S3Client; + + public constructor(input: IUploadConfig) { + this.client = input.client; + const factory = input?.factory || defaultFactory; + + const params: PutObjectCommandInput = { + ACL: "private", + Body: input.stream, + Bucket: input.bucket, + ContentType: getContentType(input.filename), + Key: input.filename + }; + + this.upload = factory({ + client: input.client, + params, + queueSize: input.queueSize || 1, + partSize: 1024 * 1024 * 5, + leavePartsOnError: false + }); + this.stream = input.stream; + } + + public async abort(): Promise { + await this.upload.abort(); + } + + public async done(): Promise { + try { + return await this.upload.done(); + } catch (ex) { + await this.abort(); + throw ex; + } + } + + public onProgress(listener: IUploadOnListener): void { + this.upload.on("httpUploadProgress", listener); + } +} + +export interface ICreateUploadFactoryParams { + client: S3Client; + bucket: string; +} + +export interface ICreateUploadCallable { + (filename: string, options?: ICreateUploadFactoryOptions): IUpload; +} + +export interface ICreateUploadFactoryOptions { + stream?: Transform; + client?: S3Client; + bucket?: string; +} + +export const createUploadFactory = (params: ICreateUploadFactoryParams): ICreateUploadCallable => { + return (filename, options) => { + const stream = + options?.stream || + new PassThrough({ + autoDestroy: true + }); + + if (stream.listenerCount("error") === 0) { + stream.on("error", ex => { + console.log("Upload Stream Error", ex); + throw ex; + }); + } + + return new Upload({ + client: options?.client || params.client, + bucket: options?.bucket || params.bucket, + stream, + filename + }); + }; +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/MultipartUploadFactory.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/MultipartUploadFactory.ts new file mode 100644 index 00000000000..16acdc41663 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/MultipartUploadFactory.ts @@ -0,0 +1,9 @@ +import type { IMultipartUploadHandler } from "./MultipartUploadHandler"; + +export interface IMultipartUploadFactoryContinueParams { + uploadId?: string; +} + +export interface IMultipartUploadFactory { + start(params?: IMultipartUploadFactoryContinueParams): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/MultipartUploadHandler.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/MultipartUploadHandler.ts new file mode 100644 index 00000000000..7fab7e145e1 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/MultipartUploadHandler.ts @@ -0,0 +1,68 @@ +import type { + AbortMultipartUploadCommandOutput, + CompleteMultipartUploadCommandOutput, + S3Client +} from "@webiny/aws-sdk/client-s3"; + +export type ITag = string; + +export interface IPart { + tag: ITag; + partNumber: number; +} + +export type IMultipartUploadHandlerParamsMinBufferSize = number | `${number}MB`; + +export interface IMultipartUploadHandlerParams { + uploadId: string; + client: S3Client; + bucket: string; + filename: string; + parts: IPart[] | undefined; + minBufferSize?: IMultipartUploadHandlerParamsMinBufferSize; +} + +export interface IMultipartUploadHandlerAddParams { + bufferLength: number; + body: Buffer; +} + +export interface IMultipartUploadHandlerPauseResult { + uploadId: string; + parts: IPart[]; +} + +export interface IMultipartUploadHandlerAddResult { + parts: IPart[]; + canBePaused(): boolean; + pause(): Promise; +} + +export interface IMultipartUploadHandlerCompleteResult { + result: CompleteMultipartUploadCommandOutput; + uploadId: string; + parts: IPart[]; +} + +export interface IMultipartUploadHandlerAbortResult { + result: AbortMultipartUploadCommandOutput; + uploadId: string; + parts: IPart[]; +} + +export interface IMultipartUploadHandlerGetBufferResult { + buffer: Buffer[]; + bufferLength: number; +} + +export interface IMultipartUploadHandler { + add(buffer: Buffer): Promise; + complete(): Promise; + abort(): Promise; + getBuffer(): IMultipartUploadHandlerGetBufferResult; + getUploadId(): string; +} + +export interface ICreateMultipartUploadHandler { + (params: IMultipartUploadHandlerParams): IMultipartUploadHandler; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/Upload.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/Upload.ts new file mode 100644 index 00000000000..ef8b2a52e40 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/abstractions/Upload.ts @@ -0,0 +1,23 @@ +import type { PassThrough } from "stream"; +import type { CompleteMultipartUploadCommandOutput, S3Client } from "@webiny/aws-sdk/client-s3"; +import type { Progress as BaseProgress, Upload as BaseUpload } from "@webiny/aws-sdk/lib-storage"; + +export type IAwsUpload = Pick; + +export type IUploadDoneResult = CompleteMultipartUploadCommandOutput; + +export type IUploadProgress = BaseProgress; + +export interface IUploadOnListener { + (progress: IUploadProgress): void; +} + +export interface IUpload { + client: S3Client; + stream: PassThrough; + upload: IAwsUpload; + + done(): Promise; + abort(): Promise; + onProgress(listener: IUploadOnListener): void; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/getContentType.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/getContentType.ts new file mode 100644 index 00000000000..c9e1e1feacb --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/getContentType.ts @@ -0,0 +1,27 @@ +import type { GenericRecord, NonEmptyArray } from "@webiny/api/types"; +import { WEBINY_EXPORT_ASSETS_EXTENSION, WEBINY_EXPORT_ENTRIES_EXTENSION } from "~/tasks/constants"; + +const allowedContentTypes: GenericRecord> = { + "application/zip": ["zip", WEBINY_EXPORT_ENTRIES_EXTENSION, WEBINY_EXPORT_ASSETS_EXTENSION], + "application/json": ["json"], + "text/plain": ["txt"] +}; + +export const getContentType = (filename: string): string => { + const ext = filename.split(".").pop(); + if (!ext) { + throw new Error( + `Could not determine the file extension from the provided filename: ${filename}` + ); + } + for (const type in allowedContentTypes) { + const extensions = allowedContentTypes[type]; + if (extensions.includes(ext)) { + return type; + } + } + + throw new Error( + `Could not determine the file content type from the provided extension: ${ext}.` + ); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/index.ts new file mode 100644 index 00000000000..4110307aede --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/index.ts @@ -0,0 +1,6 @@ +export * from "./abstractions/Upload"; +export * from "./abstractions/MultipartUploadHandler"; +export * from "./abstractions/MultipartUploadFactory"; +export * from "./Upload"; +export * from "./multipartUploadHandler"; +export * from "./MultipartUploadFactory"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandler.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandler.ts new file mode 100644 index 00000000000..09b0dedce72 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandler.ts @@ -0,0 +1,231 @@ +import type { S3Client } from "@webiny/aws-sdk/client-s3"; +import { + AbortMultipartUploadCommand, + CompleteMultipartUploadCommand, + UploadPartCommand +} from "@webiny/aws-sdk/client-s3"; +import { WebinyError } from "@webiny/error"; +import bytes from "bytes"; +import type { + ICreateMultipartUploadHandler, + IMultipartUploadHandler, + IMultipartUploadHandlerAbortResult, + IMultipartUploadHandlerAddParams, + IMultipartUploadHandlerAddResult, + IMultipartUploadHandlerCompleteResult, + IMultipartUploadHandlerGetBufferResult, + IMultipartUploadHandlerParams, + IMultipartUploadHandlerParamsMinBufferSize, + IMultipartUploadHandlerPauseResult, + IPart +} from "../abstractions/MultipartUploadHandler"; +import { createMultipartUploadHandlerPauseResult } from "./MultipartUploadHandlerPauseResult"; +import { createMultipartUploadHandlerAbortResult } from "./MultipartUploadHandlerAbortResult"; +import { createMultipartUploadHandlerCompleteResult } from "./MultipartUploadHandlerCompleteResult"; +import { createMultipartUploadHandlerAddResult } from "./MultipartUploadHandlerAddResult"; + +/** + * Minimum we can send into the S3 is 5MB. + * We can modify to have this value bigger if required. + */ +const MIN_BUFFER_SIZE = bytes.parse("5MB"); + +const getMinBufferSize = (minBufferSize?: IMultipartUploadHandlerParamsMinBufferSize): number => { + if (!minBufferSize) { + return MIN_BUFFER_SIZE; + } + const size = typeof minBufferSize === "number" ? minBufferSize : bytes.parse(minBufferSize); + if (size >= MIN_BUFFER_SIZE) { + return size; + } + return MIN_BUFFER_SIZE; +}; + +export class MultipartUploadHandler implements IMultipartUploadHandler { + private readonly uploadId: string; + private readonly client: S3Client; + private readonly bucket: string; + private readonly filename: string; + private readonly minBufferSize: IMultipartUploadHandlerParamsMinBufferSize; + private readonly parts: IPart[]; + + private buffer: Buffer[] = []; + private bufferLength = 0; + + public constructor(params: IMultipartUploadHandlerParams) { + if (!params.uploadId?.length) { + throw new WebinyError({ + message: `Missing "uploadId" in the multipart upload handler.`, + code: "MULTIPART_UPLOAD_ERROR" + }); + } else if (!params.filename?.length) { + throw new WebinyError({ + message: `Missing "filename" in the multipart upload handler.`, + code: "MULTIPART_UPLOAD_ERROR" + }); + } + this.uploadId = params.uploadId; + this.client = params.client; + this.bucket = params.bucket; + this.filename = params.filename; + this.parts = params.parts || []; + this.minBufferSize = getMinBufferSize(params.minBufferSize); + } + + public getBuffer(): IMultipartUploadHandlerGetBufferResult { + return { + buffer: this.buffer, + bufferLength: this.bufferLength + }; + } + + public async add(buffer: Buffer): Promise { + this.buffer.push(buffer); + this.bufferLength = this.bufferLength + buffer.length; + if (this.bufferLength < this.minBufferSize) { + return createMultipartUploadHandlerAddResult({ + parts: this.parts, + written: false, + pause: () => { + return this.pause(); + } + }); + } + const bufferLength = this.bufferLength; + + const body = Buffer.concat(this.buffer, bufferLength); + this.buffer = []; + this.bufferLength = 0; + await this.write({ + body, + bufferLength + }); + + return createMultipartUploadHandlerAddResult({ + parts: this.parts, + written: true, + pause: () => { + return this.pause(); + } + }); + } + + public async complete(): Promise { + const bufferLength = this.bufferLength; + const body = Buffer.concat(this.buffer, bufferLength); + this.buffer = []; + this.bufferLength = 0; + + await this.write({ + body, + bufferLength + }); + + if (this.parts.length === 0) { + throw new WebinyError({ + message: `Failed to complete the upload, no parts were uploaded.`, + code: "S3_ERROR" + }); + } + + const cmd = new CompleteMultipartUploadCommand({ + UploadId: this.uploadId, + Bucket: this.bucket, + Key: this.filename, + MultipartUpload: { + Parts: this.parts.map(part => { + return { + ETag: part.tag, + PartNumber: part.partNumber + }; + }) + } + }); + const result = await this.client.send(cmd); + + return createMultipartUploadHandlerCompleteResult({ + result, + uploadId: this.uploadId, + parts: this.parts + }); + } + + public async abort(): Promise { + const cmd = new AbortMultipartUploadCommand({ + UploadId: this.uploadId, + Bucket: this.bucket, + Key: this.filename + }); + const result = await this.client.send(cmd); + return createMultipartUploadHandlerAbortResult({ + result, + uploadId: this.uploadId, + parts: this.parts + }); + } + + private async pause(): Promise { + if (this.bufferLength > 0) { + throw new WebinyError({ + message: `Failed to pause the upload, buffer was not empty.`, + code: "S3_ERROR" + }); + } else if (this.parts.length === 0) { + throw new WebinyError({ + message: `Failed to pause the upload, no parts were uploaded.`, + code: "S3_ERROR" + }); + } + + return createMultipartUploadHandlerPauseResult({ + uploadId: this.uploadId, + parts: this.parts + }); + } + + private async write(params: IMultipartUploadHandlerAddParams): Promise { + if (params.bufferLength <= 0) { + return false; + } + + const nextPart = this.getNextPartNumber(); + const cmd = new UploadPartCommand({ + Bucket: this.bucket, + Key: this.filename, + UploadId: this.uploadId, + Body: params.body, + PartNumber: nextPart + }); + const result = await this.client.send(cmd); + if (!result.ETag) { + throw new WebinyError({ + message: `Failed to upload part: ${nextPart}`, + code: "S3_ERROR" + }); + } + this.parts.push({ + partNumber: nextPart, + tag: result.ETag.replaceAll('"', "") + }); + return true; + } + + public getUploadId(): string { + return this.uploadId; + } + + public getNextPartNumber(): number { + if (this.parts.length === 0) { + return 1; + } + const part = this.parts.at(-1); + if (!part) { + return 1; + } + return part.partNumber + 1; + } +} + +export const createMultipartUpload: ICreateMultipartUploadHandler = params => { + return new MultipartUploadHandler(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerAbortResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerAbortResult.ts new file mode 100644 index 00000000000..cfeb2c1ebf6 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerAbortResult.ts @@ -0,0 +1,23 @@ +import type { AbortMultipartUploadCommandOutput } from "@webiny/aws-sdk/client-s3"; +import type { + IMultipartUploadHandlerAbortResult, + IPart +} from "../abstractions/MultipartUploadHandler"; + +export class MultipartUploadHandlerAbortResult implements IMultipartUploadHandlerAbortResult { + public readonly result: AbortMultipartUploadCommandOutput; + public readonly uploadId: string; + public readonly parts: IPart[]; + + public constructor(params: IMultipartUploadHandlerAbortResult) { + this.result = params.result; + this.uploadId = params.uploadId; + this.parts = params.parts; + } +} + +export const createMultipartUploadHandlerAbortResult = ( + params: IMultipartUploadHandlerAbortResult +): IMultipartUploadHandlerAbortResult => { + return new MultipartUploadHandlerAbortResult(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerAddResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerAddResult.ts new file mode 100644 index 00000000000..622c8157a3a --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerAddResult.ts @@ -0,0 +1,36 @@ +import type { + IMultipartUploadHandlerAddResult, + IMultipartUploadHandlerPauseResult, + IPart +} from "../abstractions/MultipartUploadHandler"; + +export interface IMultipartUploadHandlerAddResultParams { + written: boolean; + parts: IPart[]; + pause: () => Promise; +} + +export class MultipartUploadHandlerAddResult implements IMultipartUploadHandlerAddResult { + public readonly parts: IPart[]; + private readonly written: boolean; + private readonly _pause: () => Promise; + + public constructor(params: IMultipartUploadHandlerAddResultParams) { + this.written = params.written; + this.parts = params.parts; + this._pause = params.pause; + } + + public canBePaused(): boolean { + return this.written; + } + public async pause(): Promise { + return this._pause(); + } +} + +export const createMultipartUploadHandlerAddResult = ( + params: IMultipartUploadHandlerAddResultParams +): IMultipartUploadHandlerAddResult => { + return new MultipartUploadHandlerAddResult(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerCompleteResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerCompleteResult.ts new file mode 100644 index 00000000000..a4b5596acd0 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerCompleteResult.ts @@ -0,0 +1,23 @@ +import type { CompleteMultipartUploadCommandOutput } from "@webiny/aws-sdk/client-s3"; +import type { + IMultipartUploadHandlerCompleteResult, + IPart +} from "../abstractions/MultipartUploadHandler"; + +export class MultipartUploadHandlerCompleteResult implements IMultipartUploadHandlerCompleteResult { + public readonly result: CompleteMultipartUploadCommandOutput; + public readonly uploadId: string; + public readonly parts: IPart[]; + + public constructor(params: IMultipartUploadHandlerCompleteResult) { + this.result = params.result; + this.uploadId = params.uploadId; + this.parts = params.parts; + } +} + +export const createMultipartUploadHandlerCompleteResult = ( + params: IMultipartUploadHandlerCompleteResult +): IMultipartUploadHandlerCompleteResult => { + return new MultipartUploadHandlerCompleteResult(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerPauseResult.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerPauseResult.ts new file mode 100644 index 00000000000..dbcedbd3ace --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/MultipartUploadHandlerPauseResult.ts @@ -0,0 +1,20 @@ +import type { + IMultipartUploadHandlerPauseResult, + IPart +} from "../abstractions/MultipartUploadHandler"; + +export class MultipartUploadHandlerPauseResult implements IMultipartUploadHandlerPauseResult { + uploadId: string; + parts: IPart[]; + + public constructor(params: IMultipartUploadHandlerPauseResult) { + this.uploadId = params.uploadId; + this.parts = params.parts; + } +} + +export const createMultipartUploadHandlerPauseResult = ( + params: IMultipartUploadHandlerPauseResult +): IMultipartUploadHandlerPauseResult => { + return new MultipartUploadHandlerPauseResult(params); +}; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/index.ts new file mode 100644 index 00000000000..9c9f2c7fa13 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/upload/multipartUploadHandler/index.ts @@ -0,0 +1,5 @@ +export * from "./MultipartUploadHandler"; +export * from "./MultipartUploadHandlerAbortResult"; +export * from "./MultipartUploadHandlerAddResult"; +export * from "./MultipartUploadHandlerCompleteResult"; +export * from "./MultipartUploadHandlerPauseResult"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/UrlSigner.ts b/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/UrlSigner.ts new file mode 100644 index 00000000000..d298c9a44a0 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/UrlSigner.ts @@ -0,0 +1,66 @@ +import { + GetObjectCommand, + getSignedUrl, + HeadObjectCommand, + S3Client +} from "@webiny/aws-sdk/client-s3"; +import type { + IUrlSigner, + IUrlSignerSignParams, + IUrlSignerSignResult +} from "./abstractions/UrlSigner"; + +const DEFAULT_TIMEOUT = 3600; // 1 hour + +export interface IUrlSignerParams { + client: S3Client; + bucket: string; +} + +export interface IObjectCommandConstructor { + new (params: { Bucket: string; Key: string }): GetObjectCommand | HeadObjectCommand; +} + +export class UrlSigner implements IUrlSigner { + private readonly client: S3Client; + private readonly bucket: string; + + public constructor(params: IUrlSignerParams) { + this.client = params.client; + this.bucket = params.bucket; + } + + public async get(params: IUrlSignerSignParams): Promise { + return this.sign(params, GetObjectCommand); + } + + public async head(params: IUrlSignerSignParams): Promise { + return this.sign(params, HeadObjectCommand); + } + + private async sign( + params: IUrlSignerSignParams, + command: IObjectCommandConstructor + ): Promise { + const expiresIn = params.timeout || DEFAULT_TIMEOUT; + const expiresOn = new Date(new Date().getTime() + expiresIn * 1000); + + const url = await getSignedUrl( + this.client, + new command({ + Bucket: this.bucket, + Key: params.key + }), + { + expiresIn + } + ); + + return { + url, + bucket: this.bucket, + key: params.key, + expiresOn + }; + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/abstractions/UrlSigner.ts b/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/abstractions/UrlSigner.ts new file mode 100644 index 00000000000..0e6e771a010 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/abstractions/UrlSigner.ts @@ -0,0 +1,16 @@ +export interface IUrlSignerSignResult { + url: string; + bucket: string; + key: string; + expiresOn: Date; +} + +export interface IUrlSignerSignParams { + key: string; + timeout?: number; +} + +export interface IUrlSigner { + get(params: IUrlSignerSignParams): Promise; + head(params: IUrlSignerSignParams): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/index.ts new file mode 100644 index 00000000000..af8427660b0 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/urlSigner/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/UrlSigner"; +export * from "./UrlSigner"; diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/zipper/Zipper.ts b/packages/api-headless-cms-import-export/src/tasks/utils/zipper/Zipper.ts new file mode 100644 index 00000000000..1547089a5f8 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/zipper/Zipper.ts @@ -0,0 +1,56 @@ +import type { IAddOptions, IZipper, IZipperDoneResult } from "./abstractions/Zipper"; +import type { Readable } from "stream"; +import type { ArchiverError, EntryData, ProgressData } from "archiver"; +import type { IUpload } from "~/tasks/utils/upload"; +import type { IArchiver } from "~/tasks/utils/archiver"; + +export interface IZipperConfig { + upload: IUpload; + archiver: IArchiver; +} + +export class Zipper implements IZipper { + private readonly upload: IUpload; + public readonly archiver: IArchiver; + + public constructor(config: IZipperConfig) { + this.upload = config.upload; + this.archiver = config.archiver; + + this.archiver.archiver.pipe(config.upload.stream); + } + + public async add(data: Buffer | Readable, options: IAddOptions): Promise { + this.archiver.archiver.append(data, options); + } + + public async finalize(): Promise { + /** + * Unfortunately we must wait a bit before finalizing the archive. + * Possibly it could work without this, but I've seen some issues with the archiver hanging if the finalize + * was called immediately after the last file was added. + */ + setTimeout(() => { + this.archiver.archiver.finalize(); + }, 200); + } + + public async abort(): Promise { + return this.upload.abort(); + } + + public async done(): Promise { + return this.upload.done(); + } + + public on(event: "error" | "warning", listener: (error: ArchiverError) => void): void; + public on(event: "data", listener: (data: Buffer) => void): void; + public on(event: "progress", listener: (progress: ProgressData) => void): void; + public on(event: "close" | "drain" | "finish", listener: () => void): void; + public on(event: "pipe" | "unpipe", listener: (src: Readable) => void): void; + public on(event: "entry", listener: (entry: EntryData) => void): void; + + public on(event: any, callback: any): void { + this.archiver.archiver.on(event, callback); + } +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/zipper/abstractions/Zipper.ts b/packages/api-headless-cms-import-export/src/tasks/utils/zipper/abstractions/Zipper.ts new file mode 100644 index 00000000000..3cc0baab0c5 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/zipper/abstractions/Zipper.ts @@ -0,0 +1,25 @@ +import type { ArchiverError, EntryData, ProgressData } from "archiver"; +import type { CompleteMultipartUploadCommandOutput } from "@webiny/aws-sdk/client-s3"; +import type { Readable } from "stream"; + +export interface IAddOptions extends Omit, Required> {} + +export interface IZipperOnCb { + (...args: any[]): void; +} + +export type IZipperDoneResult = CompleteMultipartUploadCommandOutput; + +export interface IZipper { + add(data: Buffer | Readable, options: IAddOptions): Promise; + on(event: string, cb: IZipperOnCb): void; + on(event: "error" | "warning", listener: (error: ArchiverError) => void): void; + on(event: "data", listener: (data: Buffer) => void): void; + on(event: "progress", listener: (progress: ProgressData) => void): void; + on(event: "close" | "drain" | "finish", listener: () => void): void; + on(event: "pipe" | "unpipe", listener: (src: Readable) => void): void; + on(event: "entry", listener: (entry: EntryData) => void): void; + finalize(): Promise; + abort(): Promise; + done(): Promise; +} diff --git a/packages/api-headless-cms-import-export/src/tasks/utils/zipper/index.ts b/packages/api-headless-cms-import-export/src/tasks/utils/zipper/index.ts new file mode 100644 index 00000000000..be3fe735963 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/utils/zipper/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/Zipper"; +export * from "./Zipper"; diff --git a/packages/api-headless-cms-import-export/src/tasks/validateImportFromUrl.ts b/packages/api-headless-cms-import-export/src/tasks/validateImportFromUrl.ts new file mode 100644 index 00000000000..094566f93d2 --- /dev/null +++ b/packages/api-headless-cms-import-export/src/tasks/validateImportFromUrl.ts @@ -0,0 +1,32 @@ +import { createTaskDefinition } from "@webiny/tasks"; +import { VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK } from "./constants"; +import type { Context } from "~/types"; +import type { + IValidateImportFromUrlInput, + IValidateImportFromUrlOutput +} from "~/tasks/domain/abstractions/ValidateImportFromUrl"; + +export const createValidateImportFromUrlTask = () => { + return createTaskDefinition( + { + id: VALIDATE_IMPORT_FROM_URL_INTEGRITY_TASK, + title: "Validate Import from URL Integrity", + maxIterations: 1, + isPrivate: true, + description: + "Validates given URLs to verify that they are what we need to import the data.", + async run(params) { + const { createValidateImportFromUrl } = await import( + /* webpackChunkName: "createValidateImportFromUrl" */ "./domain/createValidateImportFromUrl" + ); + + try { + const runner = createValidateImportFromUrl(); + return await runner.run(params); + } catch (ex) { + return params.response.error(ex); + } + } + } + ); +}; diff --git a/packages/api-headless-cms-import-export/src/types.ts b/packages/api-headless-cms-import-export/src/types.ts new file mode 100644 index 00000000000..b5182d0db3f --- /dev/null +++ b/packages/api-headless-cms-import-export/src/types.ts @@ -0,0 +1,169 @@ +import type { FileManagerContext } from "@webiny/api-file-manager/types"; +import type { Context as TasksContext, TaskDataStatus } from "@webiny/tasks/types"; +import type { ICmsImportExportRecord } from "./domain/abstractions/CmsImportExportRecord"; +import type { GenericRecord, NonEmptyArray } from "@webiny/api/types"; +import type { + CmsEntryListSort, + CmsEntryListWhere, + CmsEntryMeta +} from "@webiny/api-headless-cms/types"; + +export * from "./domain/abstractions/CmsImportExportRecord"; + +export enum CmsImportExportFileType { + ENTRIES = "entries", + ASSETS = "assets" +} + +export interface ICmsImportExportObjectGetExportParams { + id: string; +} + +export interface ICmsImportExportObjectStartExportParams { + modelId: string; + exportAssets: boolean; + limit?: number; + where?: CmsEntryListWhere; + sort?: CmsEntryListSort; +} + +export interface ICmsImportExportObjectAbortExportParams { + id: string; +} + +export interface ICmsImportExportObjectValidateImportFromUrlParams { + data: string | GenericRecord; +} + +export interface ICmsImportExportFile { + get: string; + head: string; + checksum: string; + key: string; + type: CmsImportExportFileType; + error?: ICmsImportExportValidatedFileError; +} + +export interface ICmsImportExportObjectValidateImportFromUrlResult { + files: NonEmptyArray; + modelId: string; + id: string; + status: TaskDataStatus; +} + +export interface ICmsImportExportObjectGetValidateImportFromUrlParams { + id: string; +} + +export interface ICmsImportExportValidatedFileError { + message: string; + code: string; + data?: GenericRecord; +} + +export interface ICmsImportExportValidatedValidFile { + get: string; + head: string; + key: string; + checksum: string; + type: CmsImportExportFileType | undefined; + size: number; + checked: boolean; + error?: never; +} + +export interface ICmsImportExportValidatedInvalidFile + extends Partial> { + checked: boolean; + error: ICmsImportExportValidatedFileError; +} + +export type ICmsImportExportValidatedFile = + | ICmsImportExportValidatedValidFile + | ICmsImportExportValidatedInvalidFile; + +export interface ICmsImportExportValidatedContentEntriesFile + extends ICmsImportExportValidatedValidFile { + size: number; + type: CmsImportExportFileType.ENTRIES; +} + +export interface ICmsImportExportValidatedAssetsFile extends ICmsImportExportValidatedValidFile { + size: number; + type: CmsImportExportFileType.ASSETS; +} + +export interface ICmsImportExportObjectGetValidateImportFromUrlResult { + id: string; + files: NonEmptyArray | undefined; + status: TaskDataStatus; + error?: GenericRecord; +} + +export interface ICmsImportExportObjectImportFromUrlParams { + id: string; + maxInsertErrors?: number; + overwrite?: boolean; +} + +export interface ICmsImportExportObjectAbortImportFromUrlParams { + id: string; +} + +export interface ICmsImportExportObjectGetImportFromUrlParams { + id: string; +} + +export interface ICmsImportExportObjectImportFromUrlResult { + id: string; + done: string[]; + failed: string[]; + aborted: string[]; + invalid: string[]; + status: TaskDataStatus; + error?: GenericRecord; +} + +export interface IListExportContentEntriesParams { + limit?: number; + after?: string; +} + +export interface IListExportContentEntriesResult { + items: Omit[]; + meta: CmsEntryMeta; +} + +export interface CmsImportExportObject { + getExportContentEntries( + params: ICmsImportExportObjectGetExportParams + ): Promise; + listExportContentEntries( + params?: IListExportContentEntriesParams + ): Promise; + exportContentEntries( + params: ICmsImportExportObjectStartExportParams + ): Promise; + abortExportContentEntries( + params: ICmsImportExportObjectAbortExportParams + ): Promise; + validateImportFromUrl( + params: ICmsImportExportObjectValidateImportFromUrlParams + ): Promise; + getValidateImportFromUrl( + params: ICmsImportExportObjectGetValidateImportFromUrlParams + ): Promise; + importFromUrl( + params: ICmsImportExportObjectImportFromUrlParams + ): Promise; + abortImportFromUrl( + params: ICmsImportExportObjectAbortImportFromUrlParams + ): Promise; + getImportFromUrl( + params: ICmsImportExportObjectGetImportFromUrlParams + ): Promise; +} + +export interface Context extends FileManagerContext, TasksContext { + cmsImportExport: CmsImportExportObject; +} diff --git a/packages/api-headless-cms-import-export/tsconfig.build.json b/packages/api-headless-cms-import-export/tsconfig.build.json new file mode 100644 index 00000000000..ca7d39c3d0f --- /dev/null +++ b/packages/api-headless-cms-import-export/tsconfig.build.json @@ -0,0 +1,30 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "references": [ + { "path": "../api-file-manager/tsconfig.build.json" }, + { "path": "../api-headless-cms/tsconfig.build.json" }, + { "path": "../aws-sdk/tsconfig.build.json" }, + { "path": "../error/tsconfig.build.json" }, + { "path": "../handler-graphql/tsconfig.build.json" }, + { "path": "../plugins/tsconfig.build.json" }, + { "path": "../tasks/tsconfig.build.json" }, + { "path": "../utils/tsconfig.build.json" }, + { "path": "../api/tsconfig.build.json" }, + { "path": "../api-admin-users/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/tsconfig.build.json" }, + { "path": "../handler-aws/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-import-export/tsconfig.json b/packages/api-headless-cms-import-export/tsconfig.json new file mode 100644 index 00000000000..233f4c8e737 --- /dev/null +++ b/packages/api-headless-cms-import-export/tsconfig.json @@ -0,0 +1,67 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "__tests__"], + "references": [ + { "path": "../api-file-manager" }, + { "path": "../api-headless-cms" }, + { "path": "../aws-sdk" }, + { "path": "../error" }, + { "path": "../handler-graphql" }, + { "path": "../plugins" }, + { "path": "../tasks" }, + { "path": "../utils" }, + { "path": "../api" }, + { "path": "../api-admin-users" }, + { "path": "../api-i18n" }, + { "path": "../api-security" }, + { "path": "../api-tenancy" }, + { "path": "../api-wcp" }, + { "path": "../handler" }, + { "path": "../handler-aws" }, + { "path": "../wcp" } + ], + "compilerOptions": { + "rootDirs": ["./src", "./__tests__"], + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { + "~/*": ["./src/*"], + "~tests/*": ["./__tests__/*"], + "@webiny/api-file-manager/*": ["../api-file-manager/src/*"], + "@webiny/api-file-manager": ["../api-file-manager/src"], + "@webiny/api-headless-cms/*": ["../api-headless-cms/src/*"], + "@webiny/api-headless-cms": ["../api-headless-cms/src"], + "@webiny/aws-sdk/*": ["../aws-sdk/src/*"], + "@webiny/aws-sdk": ["../aws-sdk/src"], + "@webiny/error/*": ["../error/src/*"], + "@webiny/error": ["../error/src"], + "@webiny/handler-graphql/*": ["../handler-graphql/src/*"], + "@webiny/handler-graphql": ["../handler-graphql/src"], + "@webiny/plugins/*": ["../plugins/src/*"], + "@webiny/plugins": ["../plugins/src"], + "@webiny/tasks/*": ["../tasks/src/*"], + "@webiny/tasks": ["../tasks/src"], + "@webiny/utils/*": ["../utils/src/*"], + "@webiny/utils": ["../utils/src"], + "@webiny/api/*": ["../api/src/*"], + "@webiny/api": ["../api/src"], + "@webiny/api-admin-users/*": ["../api-admin-users/src/*"], + "@webiny/api-admin-users": ["../api-admin-users/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/*": ["../handler/src/*"], + "@webiny/handler": ["../handler/src"], + "@webiny/handler-aws/*": ["../handler-aws/src/*"], + "@webiny/handler-aws": ["../handler-aws/src"], + "@webiny/wcp/*": ["../wcp/src/*"], + "@webiny/wcp": ["../wcp/src"] + }, + "baseUrl": "." + } +} diff --git a/packages/api-headless-cms-import-export/webiny.config.js b/packages/api-headless-cms-import-export/webiny.config.js new file mode 100644 index 00000000000..6dff86766c9 --- /dev/null +++ b/packages/api-headless-cms-import-export/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-tasks/package.json b/packages/api-headless-cms-tasks/package.json index e635f732884..c8c4d715755 100644 --- a/packages/api-headless-cms-tasks/package.json +++ b/packages/api-headless-cms-tasks/package.json @@ -13,7 +13,8 @@ }, "license": "MIT", "dependencies": { - "@webiny/api-headless-cms-bulk-actions": "0.0.0" + "@webiny/api-headless-cms-bulk-actions": "0.0.0", + "@webiny/api-headless-cms-import-export": "0.0.0" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/api-headless-cms-tasks/src/index.ts b/packages/api-headless-cms-tasks/src/index.ts index e8e266af68c..13dc12ab4c1 100644 --- a/packages/api-headless-cms-tasks/src/index.ts +++ b/packages/api-headless-cms-tasks/src/index.ts @@ -1,3 +1,4 @@ import { createHcmsBulkActions } from "@webiny/api-headless-cms-bulk-actions"; +import { createHeadlessCmsImportExport } from "@webiny/api-headless-cms-import-export"; -export const createHcmsTasks = () => [createHcmsBulkActions()]; +export const createHcmsTasks = () => [createHcmsBulkActions(), createHeadlessCmsImportExport()]; diff --git a/packages/api-headless-cms-tasks/tsconfig.build.json b/packages/api-headless-cms-tasks/tsconfig.build.json index 66d667fef63..4ecdb594134 100644 --- a/packages/api-headless-cms-tasks/tsconfig.build.json +++ b/packages/api-headless-cms-tasks/tsconfig.build.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.build.json", "include": ["src"], - "references": [{ "path": "../api-headless-cms-bulk-actions/tsconfig.build.json" }], + "references": [ + { "path": "../api-headless-cms-bulk-actions/tsconfig.build.json" }, + { "path": "../api-headless-cms-import-export/tsconfig.build.json" } + ], "compilerOptions": { "rootDir": "./src", "outDir": "./dist", diff --git a/packages/api-headless-cms-tasks/tsconfig.json b/packages/api-headless-cms-tasks/tsconfig.json index 9f8143ecba3..bc71a530f8b 100644 --- a/packages/api-headless-cms-tasks/tsconfig.json +++ b/packages/api-headless-cms-tasks/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.json", "include": ["src", "__tests__"], - "references": [{ "path": "../api-headless-cms-bulk-actions" }], + "references": [ + { "path": "../api-headless-cms-bulk-actions" }, + { "path": "../api-headless-cms-import-export" } + ], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], "outDir": "./dist", @@ -10,7 +13,9 @@ "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], "@webiny/api-headless-cms-bulk-actions/*": ["../api-headless-cms-bulk-actions/src/*"], - "@webiny/api-headless-cms-bulk-actions": ["../api-headless-cms-bulk-actions/src"] + "@webiny/api-headless-cms-bulk-actions": ["../api-headless-cms-bulk-actions/src"], + "@webiny/api-headless-cms-import-export/*": ["../api-headless-cms-import-export/src/*"], + "@webiny/api-headless-cms-import-export": ["../api-headless-cms-import-export/src"] }, "baseUrl": "." } diff --git a/packages/api-headless-cms/__tests__/contentAPI/mocks/contentModels.ts b/packages/api-headless-cms/__tests__/contentAPI/mocks/contentModels.ts index 94b5bab7d55..229b660d139 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/mocks/contentModels.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/mocks/contentModels.ts @@ -1,6 +1,6 @@ import { createContentModelGroup } from "./contentModelGroup"; import { CmsModel } from "~/types"; -import { CmsModelInput, createCmsGroup, createCmsModel } from "~/plugins"; +import { CmsModelInput, createCmsGroupPlugin, createCmsModelPlugin } from "~/plugins"; const { version: webinyVersion } = require("@webiny/cli/package.json"); @@ -1839,7 +1839,7 @@ export const getCmsModel = (modelId: string) => { export const createModelPlugins = (targets: string[]) => { return [ - createCmsGroup({ + createCmsGroupPlugin({ ...contentModelGroup }), ...targets.map(modelId => { @@ -1851,7 +1851,7 @@ export const createModelPlugins = (targets: string[]) => { ...(model as Omit), noValidate: true }; - return createCmsModel(newModel); + return createCmsModelPlugin(newModel); }) ]; }; diff --git a/packages/api-headless-cms/__tests__/utils/modelFieldTraverser.ts.ts b/packages/api-headless-cms/__tests__/utils/modelFieldTraverser.ts.ts new file mode 100644 index 00000000000..b6a177466a2 --- /dev/null +++ b/packages/api-headless-cms/__tests__/utils/modelFieldTraverser.ts.ts @@ -0,0 +1,140 @@ +import { useHandler } from "~tests/testHelpers/useHandler"; +import models, { createModelPlugins } from "~tests/contentAPI/mocks/contentModels"; +import { ModelFieldTraverser } from "~/utils"; +import { CmsContext } from "~/types"; +import { pageModel } from "~tests/contentAPI/mocks/pageWithDynamicZonesModel"; +import { CmsModelToAstConverter } from "~/utils/contentModelAst"; +import { CmsModelInput, createCmsModelPlugin } from "~/plugins"; + +describe("model field traverser", () => { + const { handler } = useHandler({ + plugins: [ + ...createModelPlugins(models.map(model => model.modelId)), + createCmsModelPlugin(pageModel as unknown as CmsModelInput) + ] + }); + + let context: CmsContext; + let converter: CmsModelToAstConverter; + + beforeEach(async () => { + context = await handler({ + path: "/cms/manage/en-US", + headers: { + "x-tenant": "root" + } + }); + converter = context.cms.getModelToAstConverter(); + }); + + it("should properly traverse through model fields - product", async () => { + const model = await context.cms.getModel("product"); + const ast = converter.toAst(model); + const traverser = new ModelFieldTraverser(); + + const result: string[] = []; + + traverser.traverse(ast, ({ field, path }) => { + const ref = field.settings?.models + ? `#R#${field.settings.models + .map(m => m.modelId) + .sort() + .join(",")}` + : ""; + result.push( + `${field.type}@${path.join(".")}#${field.multipleValues ? "m" : "s"}${ref}` + ); + }); + + expect(result).toEqual([ + "text@title#s", + "ref@category#s#R#category", + "number@price#s", + "boolean@inStock#s", + "number@itemsInStock#s", + "datetime@availableOn#s", + "text@color#s", + "text@availableSizes#m", + "file@image#s", + "rich-text@richText#s", + "object@variant#s", + "text@variant.name#s", + "number@variant.price#s", + "file@variant.images#m", + "ref@variant.category#s#R#category", + "object@variant.options#m", + "text@variant.options.name#s", + "number@variant.options.price#s", + "file@variant.options.image#s", + "ref@variant.options.category#s#R#category", + "ref@variant.options.categories#m#R#category", + "long-text@variant.options.longText#m", + "object@fieldsObject#s", + "text@fieldsObject.text#s" + ]); + }); + + it("should properly traverse through model fields - page builder", async () => { + const model = await context.cms.getModel(pageModel.modelId); + const ast = converter.toAst(model); + const traverser = new ModelFieldTraverser(); + + const result: string[] = []; + + traverser.traverse(ast, ({ field, path }) => { + const ref = field.settings?.models + ? `#R#${field.settings.models + .map(m => m.modelId) + .sort() + .join(",")}` + : ""; + result.push( + `${field.type}@${path.join(".")}#${field.multipleValues ? "m" : "s"}${ref}` + ); + }); + + expect(result.sort()).toEqual([ + "dynamicZone@content#m", + "dynamicZone@content.content#m", + "dynamicZone@content.content#m", + "dynamicZone@content.content#m", + "dynamicZone@content.content#m", + "dynamicZone@content.content.dynamicZone#s", + "dynamicZone@content.content.dynamicZone.dynamicZone#s", + "dynamicZone@content.content.emptyDynamicZone#s", + "dynamicZone@ghostObject.emptyDynamicZone#s", + "dynamicZone@header#s", + "dynamicZone@header.header#s", + "dynamicZone@header.header#s", + "dynamicZone@objective#s", + "dynamicZone@objective.objective#s", + "dynamicZone@reference#s", + "dynamicZone@reference.reference#s", + "dynamicZone@references1#s", + "dynamicZone@references1.references1#s", + "dynamicZone@references2#m", + "dynamicZone@references2.references2#m", + "file@header.header.image#s", + "long-text@content.content.text#s", + "object@content.content.nestedObject#s", + "object@content.content.nestedObject.objectNestedObject#m", + "object@ghostObject#s", + "object@objective.objective.nestedObject#s", + "object@objective.objective.nestedObject.objectNestedObject#m", + "ref@content.content.author#s#R#author", + "ref@content.content.authors#m#R#author", + "ref@content.content.dynamicZone.dynamicZone.authors#m#R#author", + "ref@reference.reference.author#s#R#author", + "ref@references1.references1.authors#m#R#author", + "ref@references2.references2.author#s#R#author", + "rich-text@objective.objective.nestedObject.objectBody#s", + "text@content.content.nestedObject.objectNestedObject.nestedObjectNestedTitle#s", + "text@content.content.nestedObject.objectTitle#s", + "text@content.content.title#s", + "text@header.header.title#s", + "text@header.header.title#s", + "text@objective.objective.nestedObject.objectNestedObject.nestedObjectNestedTitle#s", + "text@objective.objective.nestedObject.objectTitle#s" + ]); + }); +}); diff --git a/packages/api-headless-cms/package.json b/packages/api-headless-cms/package.json index 939da13d342..902fd0f2e92 100644 --- a/packages/api-headless-cms/package.json +++ b/packages/api-headless-cms/package.json @@ -45,7 +45,7 @@ "pluralize": "^8.0.0", "semver": "^7.3.5", "slugify": "^1.4.0", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/api-headless-cms/src/export/crud/sanitize.ts b/packages/api-headless-cms/src/export/crud/sanitize.ts index b0d607ac39a..d7f1dd09f92 100644 --- a/packages/api-headless-cms/src/export/crud/sanitize.ts +++ b/packages/api-headless-cms/src/export/crud/sanitize.ts @@ -11,10 +11,7 @@ export const sanitizeGroup = (group: CmsGroup): SanitizedCmsGroup => { }; }; -export const sanitizeModel = ( - group: Pick, - model: CmsModel -): SanitizedCmsModel => { +export const sanitizeModel = (group: Pick, model: CmsModel): SanitizedCmsModel => { return { modelId: model.modelId, name: model.name, @@ -27,6 +24,7 @@ export const sanitizeModel = ( layout: model.layout, titleFieldId: model.titleFieldId, descriptionFieldId: model.descriptionFieldId, - imageFieldId: model.imageFieldId + imageFieldId: model.imageFieldId, + tags: model.tags }; }; diff --git a/packages/api-headless-cms/src/export/types.ts b/packages/api-headless-cms/src/export/types.ts index 62d7613ed24..22e3a705c8b 100644 --- a/packages/api-headless-cms/src/export/types.ts +++ b/packages/api-headless-cms/src/export/types.ts @@ -37,6 +37,7 @@ export interface SanitizedCmsModel | "pluralApiName" | "name" | "description" + | "tags" > { group: string; } diff --git a/packages/api-headless-cms/src/index.ts b/packages/api-headless-cms/src/index.ts index 86f635b82ce..c21c4d40901 100644 --- a/packages/api-headless-cms/src/index.ts +++ b/packages/api-headless-cms/src/index.ts @@ -61,4 +61,6 @@ export * from "~/plugins"; export * from "~/utils/incrementEntryIdVersion"; export * from "~/utils/RichTextRenderer"; export * from "./graphql/handleRequest"; +export * from "./utils/contentEntryTraverser/ContentEntryTraverser"; +export * from "./utils/contentModelAst"; export { entryToStorageTransform, entryFieldFromStorageTransform, entryFromStorageTransform }; diff --git a/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts b/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts index 480a1eca3cd..1be069e43a7 100644 --- a/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts +++ b/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts @@ -224,7 +224,7 @@ export class CmsModelPlugin extends Plugin { */ if (fieldIdList.includes(fieldId)) { throw new WebinyError( - `Field's "fieldId" is not unique in the content model "${model.modelId}".`, + `Field's "fieldId" (id: ${input.id}) is not unique in the content model "${model.modelId}".`, "FIELD_ID_NOT_UNIQUE_ERROR", { model, diff --git a/packages/api-headless-cms/src/types/modelField.ts b/packages/api-headless-cms/src/types/modelField.ts index 66bce079b23..0dd2333699d 100644 --- a/packages/api-headless-cms/src/types/modelField.ts +++ b/packages/api-headless-cms/src/types/modelField.ts @@ -15,6 +15,7 @@ export type CmsModelFieldType = | "dynamicZone" | string; +export type ICmsModelFieldStorageId = `${string}@${string}` | string; /** * A definition for content model field. This type exists on the app side as well. * @@ -44,7 +45,7 @@ export interface CmsModelField { * * This is used as path for the entry value. */ - storageId: `${string}@${string}` | string; + storageId: ICmsModelFieldStorageId; /** * Field identifier for the model field that will be available to the outside world. * `storageId` is used as path (or column) to store the data. diff --git a/packages/api-headless-cms/src/types/types.ts b/packages/api-headless-cms/src/types/types.ts index c0f81e72d61..9911dfe4d5a 100644 --- a/packages/api-headless-cms/src/types/types.ts +++ b/packages/api-headless-cms/src/types/types.ts @@ -727,6 +727,8 @@ export interface CmsModelManager { delete(id: string): Promise; } +export type ICmsEntryManager = CmsModelManager; + /** * Create */ @@ -834,7 +836,7 @@ export interface CmsModelContext { * * @throws NotFoundError */ - getModel: (modelId: string) => Promise; + getModel(modelId: string): Promise; /** * Get model to AST converter. */ @@ -878,12 +880,12 @@ export interface CmsModelContext { */ getEntryManager( model: CmsModel | string - ): Promise>; + ): Promise>; /** * Get all content model managers mapped by modelId. * @see CmsModelManager */ - getEntryManagers(): Map; + getEntryManagers(): Map; /** * Clear all the model caches. */ @@ -1068,7 +1070,9 @@ export interface CmsEntryListWhere { * @category CmsEntry * @category GraphQL params */ -export type CmsEntryListSort = string[]; +export type CmsEntryListSortAsc = `${string}_ASC`; +export type CmsEntryListSortDesc = `${string}_DESC`; +export type CmsEntryListSort = (CmsEntryListSortAsc | CmsEntryListSortDesc)[]; /** * Get entry GraphQL resolver params. diff --git a/packages/api-headless-cms/src/utils/contentEntryTraverser/ContentEntryTraverser.ts b/packages/api-headless-cms/src/utils/contentEntryTraverser/ContentEntryTraverser.ts index d96e674b617..784310606d0 100644 --- a/packages/api-headless-cms/src/utils/contentEntryTraverser/ContentEntryTraverser.ts +++ b/packages/api-headless-cms/src/utils/contentEntryTraverser/ContentEntryTraverser.ts @@ -28,7 +28,11 @@ const childrenAreCollections = (node: CmsModelFieldAstNode): node is NodeWithCol const emptyValues = [null, undefined]; -export class ContentEntryTraverser { +export interface IContentEntryTraverser { + traverse(values: CmsEntryValues, visitor: ContentEntryValueVisitor): Promise; +} + +export class ContentEntryTraverser implements IContentEntryTraverser { private readonly modelAst: CmsModelAst; constructor(modelAst: CmsModelAst) { diff --git a/packages/api-headless-cms/src/utils/contentModelAst/CmsModelToAstConverter.ts b/packages/api-headless-cms/src/utils/contentModelAst/CmsModelToAstConverter.ts index 6d4203f5f63..f867310e640 100644 --- a/packages/api-headless-cms/src/utils/contentModelAst/CmsModelToAstConverter.ts +++ b/packages/api-headless-cms/src/utils/contentModelAst/CmsModelToAstConverter.ts @@ -9,11 +9,11 @@ import { CmsModel, CmsModelAst, CmsModelFieldAstNode, ICmsModelFieldToAst } from export class CmsModelToAstConverter { private readonly fieldToAstConverter: ICmsModelFieldToAst; - constructor(fieldToAstConverter: ICmsModelFieldToAst) { + public constructor(fieldToAstConverter: ICmsModelFieldToAst) { this.fieldToAstConverter = fieldToAstConverter; } - toAst(model: CmsModel): CmsModelAst { + public toAst(model: Pick): CmsModelAst { return { type: "root", children: model.fields.reduce((ast, field) => { diff --git a/packages/api-headless-cms/src/utils/index.ts b/packages/api-headless-cms/src/utils/index.ts index d9121b6ae0d..50955f7b6cf 100644 --- a/packages/api-headless-cms/src/utils/index.ts +++ b/packages/api-headless-cms/src/utils/index.ts @@ -1 +1,2 @@ export * from "./caching"; +export * from "./modelFieldTraverser"; diff --git a/packages/api-headless-cms/src/utils/modelFieldTraverser/ModelFieldTraverser.ts b/packages/api-headless-cms/src/utils/modelFieldTraverser/ModelFieldTraverser.ts new file mode 100644 index 00000000000..afe3ab8a7fc --- /dev/null +++ b/packages/api-headless-cms/src/utils/modelFieldTraverser/ModelFieldTraverser.ts @@ -0,0 +1,62 @@ +import { + CmsModelAst, + CmsModelField, + CmsModelFieldAstNode, + CmsModelFieldAstNodeField +} from "~/types"; + +const nodeHasChildren = (node: CmsModelFieldAstNode) => { + return node.children.length > 0; +}; + +type IParentNode = CmsModelAst | CmsModelFieldAstNode | null; + +interface IVisitorContext { + node: CmsModelFieldAstNode; + parent: IParentNode; +} + +export interface IModelFieldTraverserTraverseOnFieldCallableParams { + field: CmsModelField; + path: string[]; +} + +export interface IModelFieldTraverserTraverseOnFieldCallable { + (params: IModelFieldTraverserTraverseOnFieldCallableParams): void; +} + +export interface IModelFieldTraverser { + traverse(modelAst: CmsModelAst, onField: IModelFieldTraverserTraverseOnFieldCallable): void; +} + +export class ModelFieldTraverser implements IModelFieldTraverser { + public traverse(modelAst: CmsModelAst, onField: IModelFieldTraverserTraverseOnFieldCallable) { + this.execute(modelAst, [], onField); + } + + private execute( + root: CmsModelAst | CmsModelFieldAstNode, + path: string[], + onField: IModelFieldTraverserTraverseOnFieldCallable + ) { + for (const node of root.children) { + const field = this.getFieldFromNode({ node, parent: root }); + onField({ + field, + path: [...path, field.fieldId] + }); + + if (nodeHasChildren(node)) { + this.execute(node, [...path, field.fieldId], onField); + } + } + } + + private getFieldFromNode({ node, parent }: IVisitorContext) { + if (node.type === "collection") { + return (parent as CmsModelFieldAstNodeField).field; + } + + return (node as CmsModelFieldAstNodeField).field; + } +} diff --git a/packages/api-headless-cms/src/utils/modelFieldTraverser/index.ts b/packages/api-headless-cms/src/utils/modelFieldTraverser/index.ts new file mode 100644 index 00000000000..493625acf97 --- /dev/null +++ b/packages/api-headless-cms/src/utils/modelFieldTraverser/index.ts @@ -0,0 +1 @@ +export * from "./ModelFieldTraverser"; diff --git a/packages/api-mailer/package.json b/packages/api-mailer/package.json index e4e1b699b17..e685e1cd6d8 100644 --- a/packages/api-mailer/package.json +++ b/packages/api-mailer/package.json @@ -24,7 +24,7 @@ "crypto-js": "^4.2.0", "lodash": "^4.17.21", "nodemailer": "^6.9.12", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/api-page-builder-import-export/package.json b/packages/api-page-builder-import-export/package.json index 0f19f5b2474..a67fc211152 100644 --- a/packages/api-page-builder-import-export/package.json +++ b/packages/api-page-builder-import-export/package.json @@ -39,7 +39,7 @@ "lodash": "^4.17.21", "node-fetch": "^2.6.13", "stream": "^0.0.2", - "uniqid": "^5.2.0", + "uniqid": "^5.4.0", "yauzl": "^2.10.0" }, "devDependencies": { diff --git a/packages/api-page-builder-import-export/src/export/pages/ExportPagesCleanup.ts b/packages/api-page-builder-import-export/src/export/pages/ExportPagesCleanup.ts index 911347f36d9..ea034c6754f 100644 --- a/packages/api-page-builder-import-export/src/export/pages/ExportPagesCleanup.ts +++ b/packages/api-page-builder-import-export/src/export/pages/ExportPagesCleanup.ts @@ -5,7 +5,7 @@ import { PageExportTask } from "~/export/pages/types"; import { ITaskResponseResult } from "@webiny/tasks"; -import { createS3Client } from "@webiny/aws-sdk/client-s3"; +import { createS3 } from "@webiny/aws-sdk/client-s3"; import lodashChunk from "lodash/chunk"; export class ExportPagesCleanup { @@ -50,7 +50,7 @@ export class ExportPagesCleanup { return collection; }, []); - const s3 = createS3Client({ + const s3 = createS3({ region: process.env.AWS_REGION }); diff --git a/packages/api-page-builder-import-export/src/export/process/exporters/PageExporter.ts b/packages/api-page-builder-import-export/src/export/process/exporters/PageExporter.ts index 9c6fbfe119c..434b5856ade 100644 --- a/packages/api-page-builder-import-export/src/export/process/exporters/PageExporter.ts +++ b/packages/api-page-builder-import-export/src/export/process/exporters/PageExporter.ts @@ -52,7 +52,12 @@ export class PageExporter { // Get file data for all images const imageFilesData: File[] = []; if (fileIds.length > 0) { - const [filesData] = await this.fileManager.listFiles({ where: { id_in: fileIds } }); + const [filesData] = await this.fileManager.listFiles({ + where: { + id_in: fileIds + }, + limit: 10000 + }); imageFilesData.push(...filesData); } diff --git a/packages/api-page-builder-import-export/src/utils/ZipFiles.ts b/packages/api-page-builder-import-export/src/utils/ZipFiles.ts index 82cda90b8c9..3c8b053209c 100644 --- a/packages/api-page-builder-import-export/src/utils/ZipFiles.ts +++ b/packages/api-page-builder-import-export/src/utils/ZipFiles.ts @@ -1,7 +1,7 @@ import { ArchiverError, create as createArchiver } from "archiver"; import { CompleteMultipartUploadOutput, - createS3Client, + createS3, GetObjectCommand } from "@webiny/aws-sdk/client-s3"; import path from "path"; @@ -28,7 +28,7 @@ export class ZipFiles { files: string[] ): Promise { const fileNames = Array.from(files); - const s3Client = createS3Client({ + const s3Client = createS3({ requestHandler: new NodeHttpHandler({ connectionTimeout: 0, httpAgent: new HttpAgent({ diff --git a/packages/api-page-builder/package.json b/packages/api-page-builder/package.json index d0b73fb7cb5..8abb75f0b8c 100644 --- a/packages/api-page-builder/package.json +++ b/packages/api-page-builder/package.json @@ -39,8 +39,8 @@ "lodash": "^4.17.21", "node-fetch": "^2.6.13", "stream": "^0.0.2", - "uniqid": "^5.2.0", - "zod": "^3.22.4" + "uniqid": "^5.4.0", + "zod": "^3.23.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/api-websockets/package.json b/packages/api-websockets/package.json index d947b440bca..d98c219bf46 100644 --- a/packages/api-websockets/package.json +++ b/packages/api-websockets/package.json @@ -25,7 +25,7 @@ "@webiny/plugins": "0.0.0", "@webiny/utils": "0.0.0", "type-fest": "^2.19.0", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index ad905e5d1d7..a48881572f6 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,8 +1,8 @@ import { PluginsContainer } from "@webiny/plugins"; -export type GenericRecordKey = string | number | symbol; +export type GenericRecord = Record; -export type GenericRecord = Record; +export type NonEmptyArray = [T, ...T[]]; export type BenchmarkRuns = GenericRecord; diff --git a/packages/app-aco/package.json b/packages/app-aco/package.json index 074f29f6967..5fba1507424 100644 --- a/packages/app-aco/package.json +++ b/packages/app-aco/package.json @@ -41,7 +41,7 @@ "react-hotkeyz": "^1.0.4", "slugify": "^1.2.9", "store": "^2.0.12", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/app-file-manager/package.json b/packages/app-file-manager/package.json index b3d01855549..789f948915c 100644 --- a/packages/app-file-manager/package.json +++ b/packages/app-file-manager/package.json @@ -56,7 +56,7 @@ "react-dom": "18.2.0", "react-hotkeyz": "^1.0.4", "react-lazy-load": "^3.1.14", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/app-file-manager/tsconfig.build.json b/packages/app-file-manager/tsconfig.build.json index 74817e22279..217862e1934 100644 --- a/packages/app-file-manager/tsconfig.build.json +++ b/packages/app-file-manager/tsconfig.build.json @@ -5,9 +5,9 @@ { "path": "../app/tsconfig.build.json" }, { "path": "../app-aco/tsconfig.build.json" }, { "path": "../app-admin/tsconfig.build.json" }, - { "path": "../app-i18n/tsconfig.build.json" }, { "path": "../app-headless-cms/tsconfig.build.json" }, { "path": "../app-headless-cms-common/tsconfig.build.json" }, + { "path": "../app-i18n/tsconfig.build.json" }, { "path": "../app-security/tsconfig.build.json" }, { "path": "../app-tenancy/tsconfig.build.json" }, { "path": "../error/tsconfig.build.json" }, diff --git a/packages/app-file-manager/tsconfig.json b/packages/app-file-manager/tsconfig.json index df85064dc1e..c6f1dd20495 100644 --- a/packages/app-file-manager/tsconfig.json +++ b/packages/app-file-manager/tsconfig.json @@ -5,9 +5,9 @@ { "path": "../app" }, { "path": "../app-aco" }, { "path": "../app-admin" }, - { "path": "../app-i18n" }, { "path": "../app-headless-cms" }, { "path": "../app-headless-cms-common" }, + { "path": "../app-i18n" }, { "path": "../app-security" }, { "path": "../app-tenancy" }, { "path": "../error" }, @@ -32,12 +32,12 @@ "@webiny/app-aco": ["../app-aco/src"], "@webiny/app-admin/*": ["../app-admin/src/*"], "@webiny/app-admin": ["../app-admin/src"], - "@webiny/app-i18n/*": ["../app-i18n/src/*"], - "@webiny/app-i18n": ["../app-i18n/src"], "@webiny/app-headless-cms/*": ["../app-headless-cms/src/*"], "@webiny/app-headless-cms": ["../app-headless-cms/src"], "@webiny/app-headless-cms-common/*": ["../app-headless-cms-common/src/*"], "@webiny/app-headless-cms-common": ["../app-headless-cms-common/src"], + "@webiny/app-i18n/*": ["../app-i18n/src/*"], + "@webiny/app-i18n": ["../app-i18n/src"], "@webiny/app-security/*": ["../app-security/src/*"], "@webiny/app-security": ["../app-security/src"], "@webiny/app-tenancy/*": ["../app-tenancy/src/*"], diff --git a/packages/app-page-builder/package.json b/packages/app-page-builder/package.json index 8e9770b3d3e..b77bc2de637 100644 --- a/packages/app-page-builder/package.json +++ b/packages/app-page-builder/package.json @@ -82,7 +82,7 @@ "slugify": "^1.2.9", "store": "^2.0.12", "swiper": "^9.3.2", - "uniqid": "^5.0.3" + "uniqid": "^5.4.0" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/aws-sdk/package.json b/packages/aws-sdk/package.json index f9a1bf85b86..d8ebe29852a 100644 --- a/packages/aws-sdk/package.json +++ b/packages/aws-sdk/package.json @@ -6,27 +6,27 @@ "license": "MIT", "author": "Webiny Ltd.", "dependencies": { - "@aws-sdk/client-apigatewaymanagementapi": "^3.621.0", - "@aws-sdk/client-cloudfront": "^3.621.0", - "@aws-sdk/client-cloudwatch-events": "^3.621.0", - "@aws-sdk/client-cloudwatch-logs": "^3.621.0", - "@aws-sdk/client-cognito-identity-provider": "^3.621.0", - "@aws-sdk/client-dynamodb": "^3.621.0", - "@aws-sdk/client-dynamodb-streams": "^3.621.0", - "@aws-sdk/client-eventbridge": "^3.621.0", - "@aws-sdk/client-iam": "^3.621.0", - "@aws-sdk/client-iot": "^3.621.0", - "@aws-sdk/client-lambda": "^3.621.0", - "@aws-sdk/client-s3": "^3.621.0", - "@aws-sdk/client-sfn": "^3.621.0", - "@aws-sdk/client-sqs": "^3.621.0", - "@aws-sdk/client-sts": "^3.621.0", - "@aws-sdk/credential-providers": "^3.621.0", - "@aws-sdk/lib-dynamodb": "^3.621.0", - "@aws-sdk/lib-storage": "^3.621.0", - "@aws-sdk/s3-presigned-post": "^3.621.0", - "@aws-sdk/s3-request-presigner": "^3.621.0", - "@aws-sdk/util-dynamodb": "^3.621.0", + "@aws-sdk/client-apigatewaymanagementapi": "^3.654.0", + "@aws-sdk/client-cloudfront": "^3.654.0", + "@aws-sdk/client-cloudwatch-events": "^3.654.0", + "@aws-sdk/client-cloudwatch-logs": "^3.654.0", + "@aws-sdk/client-cognito-identity-provider": "^3.654.0", + "@aws-sdk/client-dynamodb": "^3.654.0", + "@aws-sdk/client-dynamodb-streams": "^3.654.0", + "@aws-sdk/client-eventbridge": "^3.654.0", + "@aws-sdk/client-iam": "^3.654.0", + "@aws-sdk/client-iot": "^3.654.0", + "@aws-sdk/client-lambda": "^3.654.0", + "@aws-sdk/client-s3": "^3.654.0", + "@aws-sdk/client-sfn": "^3.654.0", + "@aws-sdk/client-sqs": "^3.654.0", + "@aws-sdk/client-sts": "^3.654.0", + "@aws-sdk/credential-providers": "^3.654.0", + "@aws-sdk/lib-dynamodb": "^3.654.0", + "@aws-sdk/lib-storage": "^3.654.0", + "@aws-sdk/s3-presigned-post": "^3.654.0", + "@aws-sdk/s3-request-presigner": "^3.654.0", + "@aws-sdk/util-dynamodb": "^3.654.0", "@webiny/utils": "0.0.0" }, "devDependencies": { diff --git a/packages/aws-sdk/src/client-s3/index.ts b/packages/aws-sdk/src/client-s3/index.ts index 123685cf94e..8c14cc48fde 100644 --- a/packages/aws-sdk/src/client-s3/index.ts +++ b/packages/aws-sdk/src/client-s3/index.ts @@ -1,30 +1,41 @@ -import { S3, S3ClientConfig } from "@aws-sdk/client-s3"; +import { S3, S3Client, S3ClientConfig as BaseS3ClientConfig } from "@aws-sdk/client-s3"; import { createCacheKey } from "@webiny/utils"; export { + GetObjectCommand, + HeadObjectCommand, + ListObjectsCommand, + ListObjectsV2Command, + ListPartsCommand, + ObjectCannedACL, + Part, + DeleteObjectCommand, + PutObjectCommand, + PutObjectCommandInput, + PutObjectRequest, + UploadPartCommand, + AbortMultipartUploadCommand, CompleteMultipartUploadCommand, + CreateMultipartUploadCommand, + S3, + S3Client +} from "@aws-sdk/client-s3"; + +export type { CompleteMultipartUploadCommandOutput, AbortMultipartUploadCommandOutput, CompleteMultipartUploadOutput, DeleteObjectOutput, - GetObjectCommand, GetObjectOutput, - HeadObjectCommand, + GetObjectCommandOutput, HeadObjectOutput, + HeadObjectCommandOutput, + DeleteObjectCommandOutput, ListObjectsOutput, - ListObjectsV2Command, - ListPartsCommand, ListPartsCommandOutput, ListPartsOutput, - ObjectCannedACL, - Part, - PutObjectCommand, - PutObjectCommandInput, PutObjectCommandOutput, - PutObjectRequest, - S3, - S3Client, - UploadPartCommand + UploadPartCommandOutput } from "@aws-sdk/client-s3"; export { createPresignedPost } from "@aws-sdk/s3-presigned-post"; @@ -32,19 +43,57 @@ export { PresignedPost, PresignedPostOptions } from "@aws-sdk/s3-presigned-post" export { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -const clients = new Map(); +const s3ClientsCache = new Map(); -export const createS3Client = (initial?: S3ClientConfig): S3 => { - const options = { +export interface S3ClientConfig extends BaseS3ClientConfig { + cache?: boolean; +} + +export const createS3Client = (initial?: S3ClientConfig): S3Client => { + const options: S3ClientConfig = { region: process.env.AWS_REGION, ...initial }; + const skipCache = options.cache === false; + delete options.cache; + if (skipCache) { + return new S3Client({ + ...options + }); + } + const key = createCacheKey(options); - if (clients.has(key)) { - return clients.get(key) as S3; + if (s3ClientsCache.has(key)) { + return s3ClientsCache.get(key) as S3Client; + } + + const instance = new S3Client({ + ...options + }); + s3ClientsCache.set(key, instance); + + return instance; +}; + +const s3Cache = new Map(); + +export const createS3 = (initial?: S3ClientConfig): S3 => { + const options: S3ClientConfig = { + region: process.env.AWS_REGION, + ...initial + }; + const skipCache = options.cache === false; + delete options.cache; + if (skipCache) { + return new S3(options); + } + const key = createCacheKey(options); + if (s3Cache.has(key)) { + return s3Cache.get(key) as S3; } const instance = new S3(options); - clients.set(key, instance); + + s3Cache.set(key, instance); return instance; }; diff --git a/packages/aws-sdk/src/client-sfn/index.ts b/packages/aws-sdk/src/client-sfn/index.ts index 3fc8f003a84..309281fa0f6 100644 --- a/packages/aws-sdk/src/client-sfn/index.ts +++ b/packages/aws-sdk/src/client-sfn/index.ts @@ -1,11 +1,41 @@ +import type { + DescribeExecutionCommandInput, + DescribeExecutionCommandOutput, + ListExecutionsCommandInput, + ListExecutionsCommandOutput, + SFNClientConfig as BaseSFNClientConfig, + StartExecutionCommandInput, + StartExecutionCommandOutput +} from "@aws-sdk/client-sfn"; import { + DescribeExecutionCommand, + ListExecutionsCommand, SFNClient, - SFNClientConfig, - StartExecutionCommand, - StartExecutionCommandInput + SFNServiceException, + StartExecutionCommand } from "@aws-sdk/client-sfn"; +import { createCacheKey } from "@webiny/utils"; + +export { + SFNClient, + DescribeExecutionCommand, + SFNServiceException, + StartExecutionCommand, + ListExecutionsCommand +}; + +export type { + DescribeExecutionCommandInput, + DescribeExecutionCommandOutput, + StartExecutionCommandInput, + StartExecutionCommandOutput, + ListExecutionsCommandInput, + ListExecutionsCommandOutput +}; -export { SFNClient, StartExecutionCommand, SFNServiceException } from "@aws-sdk/client-sfn"; +export interface SFNClientConfig extends BaseSFNClientConfig { + cache?: boolean; +} export type GenericData = string | number | boolean | null | undefined; @@ -19,21 +49,36 @@ export interface TriggerStepFunctionParams< input: T; } -const getClient = (config: SFNClient | SFNClientConfig): SFNClient => { - if (config instanceof SFNClient) { - return config; +const stepFunctionClientsCache = new Map(); + +const getClient = (initial?: SFNClientConfig): SFNClient => { + const config: SFNClientConfig = { + region: process.env.AWS_REGION, + ...initial + }; + const skipCache = config.cache === false; + delete config.cache; + if (skipCache) { + return new SFNClient({ + ...config + }); + } + + const key = createCacheKey(config); + if (stepFunctionClientsCache.has(key)) { + return stepFunctionClientsCache.get(key) as SFNClient; } + return new SFNClient({ - ...config, - region: config.region || process.env.AWS_REGION + ...config }); }; -export const triggerStepFunctionFactory = (config: SFNClient | SFNClientConfig) => { +export const triggerStepFunctionFactory = (config?: SFNClientConfig) => { const client = getClient(config); return async ( params: TriggerStepFunctionParams - ) => { + ): Promise => { const cmd = new StartExecutionCommand({ ...params, stateMachineArn: params.stateMachineArn || process.env.BG_TASK_SFN_ARN, @@ -43,3 +88,24 @@ export const triggerStepFunctionFactory = (config: SFNClient | SFNClientConfig) return await client.send(cmd); }; }; + +export const listExecutionsFactory = (config?: SFNClientConfig) => { + const client = getClient(config); + return async (params: ListExecutionsCommandInput): Promise => { + const cmd = new ListExecutionsCommand({ + ...params, + stateMachineArn: params.stateMachineArn || process.env.BG_TASK_SFN_ARN + }); + return await client.send(cmd); + }; +}; + +export const describeExecutionFactory = (config?: SFNClientConfig) => { + const client = getClient(config); + return async ( + params: DescribeExecutionCommandInput + ): Promise => { + const cmd = new DescribeExecutionCommand(params); + return await client.send(cmd); + }; +}; diff --git a/packages/cli-plugin-scaffold-extensions/tsconfig.json b/packages/cli-plugin-scaffold-extensions/tsconfig.json index 5cbebb5ff09..0a46a2edd0a 100644 --- a/packages/cli-plugin-scaffold-extensions/tsconfig.json +++ b/packages/cli-plugin-scaffold-extensions/tsconfig.json @@ -2,15 +2,9 @@ "extends": "../../tsconfig.json", "include": ["src", "__tests__"], "references": [ - { - "path": "../aws-sdk" - }, - { - "path": "../cli-plugin-scaffold" - }, - { - "path": "../error" - } + { "path": "../aws-sdk" }, + { "path": "../cli-plugin-scaffold" }, + { "path": "../error" } ], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], diff --git a/packages/cli/package.json b/packages/cli/package.json index ee1b036cc16..669c0bdef68 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,7 +32,7 @@ "semver": "^7.3.5", "ts-morph": "^11.0.0", "typescript": "4.9.5", - "uniqid": "5.4.0", + "uniqid": "^5.4.0", "yargs": "^17.4.0" }, "license": "MIT", diff --git a/packages/db-dynamodb/src/plugins/definitions/ValueFilterPlugin.ts b/packages/db-dynamodb/src/plugins/definitions/ValueFilterPlugin.ts index 6c2f375b065..366da8f954e 100644 --- a/packages/db-dynamodb/src/plugins/definitions/ValueFilterPlugin.ts +++ b/packages/db-dynamodb/src/plugins/definitions/ValueFilterPlugin.ts @@ -15,7 +15,7 @@ export interface ValueFilterPluginParams { canUse?: (params: ValueFilterPluginParamsMatchesParams) => boolean; matches: ValueFilterPluginParamsMatches; } -export class ValueFilterPlugin extends Plugin { +export class ValueFilterPlugin extends Plugin { public static override readonly type: string = "dynamodb.value.filter"; private readonly _params: ValueFilterPluginParams; @@ -28,7 +28,7 @@ export class ValueFilterPlugin extends Plugin { this._params = params; } - public canUse(params: ValueFilterPluginParamsMatchesParams): boolean { + public canUse(params: ValueFilterPluginParamsMatchesParams): boolean { if (!this._params.canUse) { return true; } diff --git a/packages/handler-graphql/src/index.ts b/packages/handler-graphql/src/index.ts index 3bfa473d497..e6c2952f91c 100644 --- a/packages/handler-graphql/src/index.ts +++ b/packages/handler-graphql/src/index.ts @@ -3,6 +3,7 @@ import createGraphQLHandler from "./createGraphQLHandler"; export * from "./errors"; export * from "./responses"; +export * from "./utils"; export * from "./plugins"; export * from "./processRequestBody"; diff --git a/packages/handler-graphql/src/utils/index.ts b/packages/handler-graphql/src/utils/index.ts new file mode 100644 index 00000000000..b3f16557468 --- /dev/null +++ b/packages/handler-graphql/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./resolve"; diff --git a/packages/handler-graphql/src/utils/resolve.ts b/packages/handler-graphql/src/utils/resolve.ts new file mode 100644 index 00000000000..529f87e5fee --- /dev/null +++ b/packages/handler-graphql/src/utils/resolve.ts @@ -0,0 +1,40 @@ +import { ErrorResponse, ListErrorResponse, ListResponse, Response } from "~/responses"; +import { GenericRecord } from "@webiny/api/types"; + +export interface Meta { + totalCount: number; + hasMoreItems: boolean; + cursor: string | null; +} + +export const emptyResolver = () => ({}); + +interface ResolveCallable { + (): Promise; +} + +export const resolve = async (fn: ResolveCallable) => { + try { + return new Response(await fn()); + } catch (ex) { + return new ErrorResponse(ex); + } +}; + +interface ResolveListCallable { + (): Promise>; +} + +interface IListResult { + items: T[]; + meta: Meta; +} + +export const resolveList = async (fn: ResolveListCallable) => { + try { + const result = (await fn()) as IListResult; + return new ListResponse(result.items, result.meta); + } catch (ex) { + return new ListErrorResponse(ex); + } +}; diff --git a/packages/ioc/package.json b/packages/ioc/package.json index eaa07b091a2..d0b2bc7758f 100644 --- a/packages/ioc/package.json +++ b/packages/ioc/package.json @@ -18,7 +18,7 @@ "@webiny/project-utils": "0.0.0", "ttypescript": "^1.5.13", "typescript": "4.9.5", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "publishConfig": { "access": "public", diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 2f94d7debdd..6a28e2a83be 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -14,7 +14,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.0", - "uniqid": "^5.2.0" + "uniqid": "^5.4.0" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/project-utils/testing/tasks/runner.ts b/packages/project-utils/testing/tasks/runner.ts index 614f5c480b8..3461e4ec5ea 100644 --- a/packages/project-utils/testing/tasks/runner.ts +++ b/packages/project-utils/testing/tasks/runner.ts @@ -1,5 +1,7 @@ import { Context, + IResponseContinueResult, + IResponseResult, ITaskDataInput, ITaskDefinition, ITaskEvent, @@ -8,9 +10,20 @@ import { import { TaskRunner } from "@webiny/tasks/runner"; import { timerFactory } from "@webiny/handler-aws/utils"; import { TaskEventValidation } from "@webiny/tasks/runner/TaskEventValidation"; +import { ResponseContinueResult } from "@webiny/tasks/response/ResponseContinueResult"; import { createMockTaskTriggerTransportPlugin } from "./mockTaskTriggerTransportPlugin"; -export interface CreateRunnerParams< +export interface ICreateRunnerParamsOnContinueCallableParams { + taskId: string; + iteration: number; + result: IResponseContinueResult; +} + +export interface ICreateRunnerParamsOnContinueCallable { + (params: ICreateRunnerParamsOnContinueCallableParams): Promise; +} + +export interface ICreateRunnerParams< C extends Context = Context, I = ITaskDataInput, O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput @@ -18,14 +31,22 @@ export interface CreateRunnerParams< context: Context; task: ITaskDefinition; getRemainingTimeInMills?: () => number; + /** + * If provided, this function will be called every time the task continues. + * If the task is not supposed to continue, this function will not be called. + */ + onContinue?: ICreateRunnerParamsOnContinueCallable; } +export type IExecuteEvent = Pick & + Partial>; + export const createRunner = < C extends Context = Context, I = ITaskDataInput, O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput >( - params: CreateRunnerParams + params: ICreateRunnerParams ) => { params.context.plugins.register(createMockTaskTriggerTransportPlugin()); const runner = new TaskRunner( @@ -41,10 +62,8 @@ export const createRunner = < new TaskEventValidation() ); - return ( - event: Pick & Partial> - ) => { - return runner.run({ + const execute = async (event: IExecuteEvent) => { + return await runner.run({ tenant: "root", locale: "en-US", ...event, @@ -54,4 +73,23 @@ export const createRunner = < executionName: "aMockExecutionName" }); }; + + return async (event: IExecuteEvent) => { + let iteration = 1; + let result: IResponseResult; + while ((result = await execute(event))) { + if (result instanceof ResponseContinueResult && params.onContinue) { + await params.onContinue({ + taskId: event.webinyTaskId, + iteration, + result + }); + iteration++; + console.debug(`Continuing task ${params.task.id} #${iteration}.`); + continue; + } + return result; + } + return result; + }; }; diff --git a/packages/pulumi-aws/src/apps/api/backgroundTask/definition.ts b/packages/pulumi-aws/src/apps/api/backgroundTask/definition.ts index b66d982c349..fd62f5e64ea 100644 --- a/packages/pulumi-aws/src/apps/api/backgroundTask/definition.ts +++ b/packages/pulumi-aws/src/apps/api/backgroundTask/definition.ts @@ -77,6 +77,11 @@ export const createBackgroundTaskDefinition = ( * This means that task will wait for the specified time and then continue. * It can be used to handle waiting for child tasks or some resource to be created. */ + { + Variable: "$", + IsNull: true, + Next: "UnknownError" + }, { And: [ { diff --git a/packages/pulumi-aws/src/apps/api/backgroundTask/types.ts b/packages/pulumi-aws/src/apps/api/backgroundTask/types.ts index 67697d40032..194f900a064 100644 --- a/packages/pulumi-aws/src/apps/api/backgroundTask/types.ts +++ b/packages/pulumi-aws/src/apps/api/backgroundTask/types.ts @@ -24,6 +24,7 @@ export interface StepFunctionDefinitionStatesChoiceBase { StringEquals?: string; StringMatches?: string; IsPresent?: boolean; + IsNull?: boolean; } export interface StepFunctionDefinitionStatesChoiceAndItem { diff --git a/packages/pulumi-aws/tsconfig.build.json b/packages/pulumi-aws/tsconfig.build.json index 95d2ea4294f..88d73a8f7d3 100644 --- a/packages/pulumi-aws/tsconfig.build.json +++ b/packages/pulumi-aws/tsconfig.build.json @@ -3,9 +3,9 @@ "include": ["src"], "references": [ { "path": "../aws-sdk/tsconfig.build.json" }, - { "path": "../feature-flags/tsconfig.build.json" }, { "path": "../pulumi/tsconfig.build.json" }, - { "path": "../api-page-builder/tsconfig.build.json" } + { "path": "../api-page-builder/tsconfig.build.json" }, + { "path": "../feature-flags/tsconfig.build.json" } ], "compilerOptions": { "rootDir": "./src", diff --git a/packages/pulumi-aws/tsconfig.json b/packages/pulumi-aws/tsconfig.json index 5d3c7a0055d..ce4d5c491fb 100644 --- a/packages/pulumi-aws/tsconfig.json +++ b/packages/pulumi-aws/tsconfig.json @@ -3,9 +3,9 @@ "include": ["src", "__tests__"], "references": [ { "path": "../aws-sdk" }, - { "path": "../feature-flags" }, { "path": "../pulumi" }, - { "path": "../api-page-builder" } + { "path": "../api-page-builder" }, + { "path": "../feature-flags" } ], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], @@ -16,12 +16,12 @@ "~tests/*": ["./__tests__/*"], "@webiny/aws-sdk/*": ["../aws-sdk/src/*"], "@webiny/aws-sdk": ["../aws-sdk/src"], - "@webiny/feature-flags/*": ["../feature-flags/src/*"], - "@webiny/feature-flags": ["../feature-flags/src"], "@webiny/pulumi/*": ["../pulumi/src/*"], "@webiny/pulumi": ["../pulumi/src"], "@webiny/api-page-builder/*": ["../api-page-builder/src/*"], - "@webiny/api-page-builder": ["../api-page-builder/src"] + "@webiny/api-page-builder": ["../api-page-builder/src"], + "@webiny/feature-flags/*": ["../feature-flags/src/*"], + "@webiny/feature-flags": ["../feature-flags/src"] }, "baseUrl": "." } diff --git a/packages/tasks/__tests__/live/runner.ts b/packages/tasks/__tests__/live/runner.ts index 3aff454d4c6..8265560431f 100644 --- a/packages/tasks/__tests__/live/runner.ts +++ b/packages/tasks/__tests__/live/runner.ts @@ -2,6 +2,8 @@ import { createLiveContext, CreateLiveContextParams } from "./context"; import { TaskRunner } from "~/runner"; import { Context as LambdaContext } from "aws-lambda/handler"; import { Context } from "~tests/types"; +import { TaskEventValidation } from "~/runner/TaskEventValidation"; +import { timerFactory } from "@webiny/handler-aws"; const defaultLambdaContext: Pick = { getRemainingTimeInMillis: () => { @@ -20,7 +22,11 @@ export const createLiveRunner = async ( ) => { const context = params?.context || (await createLiveContext(params)); - const runner = new TaskRunner(params?.lambdaContext || defaultLambdaContext, context); + const runner = new TaskRunner( + context, + timerFactory(params?.lambdaContext || defaultLambdaContext), + new TaskEventValidation() + ); return { runner, context }; }; diff --git a/packages/tasks/__tests__/live/task.ts b/packages/tasks/__tests__/live/task.ts deleted file mode 100644 index 480c36f83e9..00000000000 --- a/packages/tasks/__tests__/live/task.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Context } from "~tests/types"; -import { ITaskCreateData, ITaskUpdateData } from "~/types"; - -export interface CreateLiveTaskParams { - context: C; - data: ITaskCreateData & Partial; - taskLog?: boolean; -} - -export const createLiveTask = async ( - params: CreateLiveTaskParams -) => { - const { context, data } = params; - const task = await context.tasks.createTask(data); - if (params.taskLog) { - return { - task - }; - } - const taskLog = await context.tasks.createLog(task, { - iteration: 1, - executionName: task.executionName || data.executionName || "unknownExecutionName" - }); - - return { - task, - taskLog - }; -}; diff --git a/packages/tasks/__tests__/live/taskManager.ts b/packages/tasks/__tests__/live/taskManager.ts deleted file mode 100644 index 7c80c2361ed..00000000000 --- a/packages/tasks/__tests__/live/taskManager.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TaskManager } from "~/runner/TaskManager"; -import { createLiveRunner, CreateLiveRunnerParams } from "./runner"; -import { Response, TaskResponse } from "~/response"; -import { ITaskEvent } from "~/handler/types"; -import { createLiveStore, CreateLiveStoreParams } from "./store"; - -export interface CreateLiveTaskManagerParams extends CreateLiveRunnerParams, CreateLiveStoreParams { - event: ITaskEvent; -} - -export const createLiveTaskManager = async (params: CreateLiveTaskManagerParams) => { - const response = new Response(params.event); - const taskResponse = new TaskResponse(response); - const { runner, context } = await createLiveRunner(params); - const { store } = await createLiveStore({ - context, - ...params - }); - - const taskManager = new TaskManager(runner, context, response, taskResponse, store); - - return { - runner, - store, - context, - taskManager, - response, - taskResponse - }; -}; diff --git a/packages/tasks/__tests__/live/taskResponse.ts b/packages/tasks/__tests__/live/taskResponse.ts deleted file mode 100644 index 2c11ba77cee..00000000000 --- a/packages/tasks/__tests__/live/taskResponse.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Response, TaskResponse } from "~/response"; -import { IResponse } from "~/types"; -import { ITaskEvent } from "~/handler/types"; - -export interface CreateLiveTaskResponseWithResponse { - response: IResponse; - event?: never; -} -export interface CreateLiveTaskResponseWithEvent { - event: ITaskEvent; - response?: never; -} - -export type CreateLiveTaskResponse = - | CreateLiveTaskResponseWithResponse - | CreateLiveTaskResponseWithEvent; - -export const createLiveTaskResponse = (params: CreateLiveTaskResponse) => { - const response = params?.response || new Response(params.event); - - return new TaskResponse(response); -}; diff --git a/packages/tasks/__tests__/response/response.test.ts b/packages/tasks/__tests__/response/response.test.ts new file mode 100644 index 00000000000..70d11a9b94d --- /dev/null +++ b/packages/tasks/__tests__/response/response.test.ts @@ -0,0 +1,268 @@ +import { + Response, + ResponseAbortedResult, + ResponseContinueResult, + ResponseDoneResult, + ResponseErrorResult +} from "~/response"; +import { createMockEvent } from "~tests/mocks"; +import { ITaskEvent } from "~/handler/types"; +import { TaskResponseStatus } from "~/types"; +import { WebinyError } from "@webiny/error"; + +describe("response", () => { + let event: ITaskEvent; + + beforeEach(() => { + event = createMockEvent(); + }); + + it("should output continue response", async () => { + const response = new Response(event); + + const message = "Some continue message"; + + const result = response.continue({ + message, + input: { + aInput: true + }, + wait: 30, + locale: event.locale, + tenant: event.tenant, + webinyTaskId: event.webinyTaskId + }); + + expect(result).toBeInstanceOf(ResponseContinueResult); + + expect(result).toEqual({ + message, + input: { + aInput: true + }, + status: TaskResponseStatus.CONTINUE, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale, + wait: 30, + delay: -1 + }); + }); + + it("should output done response", async () => { + const response = new Response(event); + + const message = "Some done message"; + + const result = response.done({ + message, + output: { + aDoneOutput: true + }, + locale: event.locale, + tenant: event.tenant, + webinyTaskId: event.webinyTaskId + }); + + expect(result).toBeInstanceOf(ResponseDoneResult); + + expect(result).toEqual({ + message, + output: { + aDoneOutput: true + }, + status: TaskResponseStatus.DONE, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale + }); + }); + + it("should output error response", async () => { + const response = new Response(event); + + const message = "Some error message"; + + const result = response.error({ + error: new WebinyError({ + message, + code: "SOME_ERROR_CODE", + data: { + someData: true + } + }), + webinyTaskId: event.webinyTaskId, + tenant: event.tenant, + locale: event.locale + }); + + expect(result).toBeInstanceOf(ResponseErrorResult); + + expect(result).toEqual({ + error: { + message, + code: "SOME_ERROR_CODE", + data: { + someData: true + } + }, + status: TaskResponseStatus.ERROR, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale + }); + }); + + it("should output aborted response", async () => { + const response = new Response(event); + + const result = response.aborted(); + + expect(result).toBeInstanceOf(ResponseAbortedResult); + + expect(result).toEqual({ + status: TaskResponseStatus.ABORTED, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale + }); + }); + + it("should create done response via from method", async () => { + const response = new Response(event); + + const message = "Some done message"; + + const done = response.from({ + message, + output: { + aDoneOutput: true + }, + locale: event.locale, + tenant: event.tenant, + webinyTaskId: event.webinyTaskId, + status: TaskResponseStatus.DONE, + webinyTaskDefinitionId: event.webinyTaskDefinitionId + }); + + expect(done).toBeInstanceOf(ResponseDoneResult); + expect(done).toEqual({ + message, + output: { + aDoneOutput: true + }, + status: TaskResponseStatus.DONE, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale + }); + }); + + it("should create continue response via from method", async () => { + const response = new Response(event); + + const message = "Some continue message"; + + const done = response.from({ + message, + input: { + aInput: true + }, + wait: 30, + locale: event.locale, + tenant: event.tenant, + webinyTaskId: event.webinyTaskId, + status: TaskResponseStatus.CONTINUE, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + delay: -1 + }); + + expect(done).toBeInstanceOf(ResponseContinueResult); + expect(done).toEqual({ + message, + input: { + aInput: true + }, + status: TaskResponseStatus.CONTINUE, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale, + wait: 30, + delay: -1 + }); + }); + + it("should create error response via from method", async () => { + const response = new Response(event); + + const message = "Some error message"; + + const done = response.from({ + error: new WebinyError({ + message, + code: "SOME_ERROR_CODE", + data: { + someData: true + } + }), + webinyTaskId: event.webinyTaskId, + tenant: event.tenant, + locale: event.locale, + status: TaskResponseStatus.ERROR, + webinyTaskDefinitionId: event.webinyTaskDefinitionId + }); + + expect(done).toBeInstanceOf(ResponseErrorResult); + expect(done).toEqual({ + error: { + message, + code: "SOME_ERROR_CODE", + data: { + someData: true + } + }, + status: TaskResponseStatus.ERROR, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale + }); + }); + + it("should truncate output because it is too large", async () => { + const response = new Response(event); + + const message = "Some done message"; + const result = response.done({ + message, + output: { + aDoneOutput: true, + aLargeOutput: new Array(8000).fill("123456789011121314151617181920").join("") + }, + locale: event.locale, + tenant: event.tenant, + webinyTaskId: event.webinyTaskId + }); + + expect(result).toBeInstanceOf(ResponseDoneResult); + expect(result).toEqual({ + message, + output: { + message: `Output size exceeds the maximum allowed size.`, + size: 240038, + max: 232 * 1024 + }, + status: TaskResponseStatus.DONE, + webinyTaskId: event.webinyTaskId, + webinyTaskDefinitionId: event.webinyTaskDefinitionId, + tenant: event.tenant, + locale: event.locale + }); + }); +}); diff --git a/packages/tasks/__tests__/runner/taskRunnerTaskNotFound.test.ts b/packages/tasks/__tests__/runner/taskRunnerTaskNotFound.test.ts index bcc5da7b309..74abe641cbe 100644 --- a/packages/tasks/__tests__/runner/taskRunnerTaskNotFound.test.ts +++ b/packages/tasks/__tests__/runner/taskRunnerTaskNotFound.test.ts @@ -29,15 +29,8 @@ describe("task runner task not found", () => { tenant: "root", locale: "en-US", error: { - status: "error", - webinyTaskId: "unknownTaskId", - webinyTaskDefinitionId: "myCustomTaskDefinition", - tenant: "root", - locale: "en-US", - error: { - message: 'Task "unknownTaskId" cannot be executed because it does not exist.', - code: "TASK_NOT_FOUND" - } + message: 'Task "unknownTaskId" cannot be executed because it does not exist.', + code: "TASK_NOT_FOUND" } }); }); diff --git a/packages/tasks/package.json b/packages/tasks/package.json index ad7825cc2bc..a6b350a9cfb 100644 --- a/packages/tasks/package.json +++ b/packages/tasks/package.json @@ -27,7 +27,8 @@ "deep-equal": "^2.2.3", "lodash": "^4.17.21", "object-merge-advanced": "^12.1.0", - "zod": "^3.22.4" + "object-sizeof": "^2.6.4", + "zod": "^3.23.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/tasks/src/crud/trigger.tasks.ts b/packages/tasks/src/crud/trigger.tasks.ts index c77ad943c6b..5688f9c6d80 100644 --- a/packages/tasks/src/crud/trigger.tasks.ts +++ b/packages/tasks/src/crud/trigger.tasks.ts @@ -8,6 +8,7 @@ import { ITaskDataInput, ITaskLog, ITaskLogItemType, + ITaskResponseDoneResultOutput, ITasksContextTriggerObject, ITaskTriggerParams, PutEventsCommandOutput, @@ -53,7 +54,12 @@ export const createTriggerTasksCrud = ( }); return { - trigger: async (params: ITaskTriggerParams): Promise> => { + trigger: async < + T = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput + >( + params: ITaskTriggerParams + ): Promise> => { const { definition: id, input: inputValues, name, parent, delay = 0 } = params; const definition = context.tasks.getDefinition(id); if (!definition) { @@ -105,13 +111,18 @@ export const createTriggerTasksCrud = ( eventResponse: event }); }, - abort: async (params: ITaskAbortParams): Promise => { - const task = await context.tasks.getTask(params.id); + abort: async < + T = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput + >( + params: ITaskAbortParams + ): Promise> => { + const task = await context.tasks.getTask(params.id); if (!task) { throw new NotFoundError(`Task "${params.id}" was not found!`); } - const definition = context.tasks.getDefinition(task.definitionId); + const definition = context.tasks.getDefinition(task.definitionId); if (!definition) { throw new WebinyError(`Task definition was not found!`, "TASK_DEFINITION_ERROR", { id: task.id @@ -143,7 +154,7 @@ export const createTriggerTasksCrud = ( }); } try { - const updatedTask = await context.tasks.updateTask(task.id, { + const updatedTask = await context.tasks.updateTask(task.id, { taskStatus: TaskDataStatus.ABORTED }); await context.tasks.updateLog(taskLog.id, { diff --git a/packages/tasks/src/handler/index.ts b/packages/tasks/src/handler/index.ts index ad104c3eb11..088cf32114b 100644 --- a/packages/tasks/src/handler/index.ts +++ b/packages/tasks/src/handler/index.ts @@ -1,11 +1,10 @@ import { createHandler as createBaseHandler } from "@webiny/handler"; import { registerDefaultPlugins } from "@webiny/handler-aws/plugins"; import { execute } from "@webiny/handler-aws/execute"; -import { HandlerFactoryParams } from "@webiny/handler-aws/types"; import { APIGatewayProxyResult } from "aws-lambda"; import { Context as LambdaContext } from "aws-lambda/handler"; import { Context, TaskResponseStatus } from "~/types"; -import { ITaskRawEvent } from "~/handler/types"; +import { HandlerParams, ITaskRawEvent } from "~/handler/types"; import { TaskRunner } from "~/runner"; import WebinyError from "@webiny/error"; import { timerFactory } from "@webiny/handler-aws/utils"; @@ -15,8 +14,6 @@ export interface HandlerCallable { (event: ITaskRawEvent, context: LambdaContext): Promise; } -export type HandlerParams = HandlerFactoryParams; - const url = "/webiny-background-task-event"; export const createHandler = (params: HandlerParams): HandlerCallable => { diff --git a/packages/tasks/src/handler/register.ts b/packages/tasks/src/handler/register.ts index e4e40d3398c..eaeb39572fd 100644 --- a/packages/tasks/src/handler/register.ts +++ b/packages/tasks/src/handler/register.ts @@ -1,7 +1,6 @@ import { registry } from "@webiny/handler-aws/registry"; -import { createHandler, HandlerParams } from "./index"; import { createSourceHandler } from "@webiny/handler-aws"; -import { IIncomingEvent, ITaskEvent } from "./types"; +import { HandlerParams, IIncomingEvent, ITaskEvent } from "./types"; const handler = createSourceHandler, HandlerParams>({ name: "handler-webiny-background-task", @@ -9,6 +8,10 @@ const handler = createSourceHandler, HandlerParams>({ return !!event.payload?.webinyTaskId; }, handle: async ({ params, event, context }) => { + const { createHandler } = await import( + /* webpackChunkName: "tasks.handler.createHandler" */ + "./index" + ); return createHandler(params)(event.payload, context); } }); diff --git a/packages/tasks/src/handler/types.ts b/packages/tasks/src/handler/types.ts index 2e48944e872..cac87aa8c0d 100644 --- a/packages/tasks/src/handler/types.ts +++ b/packages/tasks/src/handler/types.ts @@ -1,3 +1,7 @@ +import { HandlerFactoryParams } from "@webiny/handler-aws/types"; + +export type HandlerParams = HandlerFactoryParams; + export interface IIncomingEvent { name: string; payload: TEvent; diff --git a/packages/tasks/src/response/Response.ts b/packages/tasks/src/response/Response.ts index 2ae2704a782..3a691687373 100644 --- a/packages/tasks/src/response/Response.ts +++ b/packages/tasks/src/response/Response.ts @@ -1,3 +1,5 @@ +import sizeOfObject from "object-sizeof"; + import { ITaskEvent } from "~/handler/types"; import { TaskResponseStatus } from "~/types"; import { @@ -19,6 +21,54 @@ import { ResponseErrorResult } from "~/response/ResponseErrorResult"; import { ResponseAbortedResult } from "./ResponseAbortedResult"; import { getErrorProperties } from "~/utils/getErrorProperties"; +/** + * Step Functions has a limit of 256KB for the output size. + * We will set the max output to be 232KB to leave some room for the rest of the data. + */ +const MAX_SIZE_BYTES: number = 232 * 1024; + +interface ICreateMaxSizeOutputParams { + size: number; +} + +const createMaxSizeOutput = ({ + size +}: ICreateMaxSizeOutputParams): O => { + return { + message: `Output size exceeds the maximum allowed size.`, + size, + max: MAX_SIZE_BYTES + } as unknown as O; +}; +/** + * Figure out the size of the output object and remove the stack trace if the size exceeds the maximum allowed size. + * If the size is still greater than the maximum allowed size, just return the message that the output size exceeds the maximum allowed size. + */ +const getOutput = (output?: O): O | undefined => { + if (!output || Object.keys(output).length === 0) { + return undefined; + } + let size = sizeOfObject(output); + if (size > MAX_SIZE_BYTES) { + if (output.stack) { + delete output.stack; + size = sizeOfObject(output); + if (size <= MAX_SIZE_BYTES) { + return output; + } + } + if (output.error?.stack) { + delete output.error.stack; + size = sizeOfObject(output); + if (size <= MAX_SIZE_BYTES) { + return output; + } + } + return createMaxSizeOutput({ size }); + } + return output; +}; + export class Response implements IResponse { private _event: ITaskEvent; @@ -47,6 +97,7 @@ export class Response implements IResponse { public continue(params: IResponseContinueParams): IResponseContinueResult { return new ResponseContinueResult({ + message: params.message, input: params.input, webinyTaskId: params?.webinyTaskId || this.event.webinyTaskId, webinyTaskDefinitionId: this.event.webinyTaskDefinitionId, @@ -65,7 +116,7 @@ export class Response implements IResponse { tenant: params?.tenant || this.event.tenant, locale: params?.locale || this.event.locale, message: params?.message, - output: params?.output + output: getOutput(params?.output) }); } diff --git a/packages/tasks/src/response/ResponseDoneResult.ts b/packages/tasks/src/response/ResponseDoneResult.ts index 6ed20bd24b0..c12a4727046 100644 --- a/packages/tasks/src/response/ResponseDoneResult.ts +++ b/packages/tasks/src/response/ResponseDoneResult.ts @@ -19,6 +19,6 @@ export class ResponseDoneResult< this.webinyTaskDefinitionId = params.webinyTaskDefinitionId; this.tenant = params.tenant; this.locale = params.locale; - this.output = params.output; + this.output = Object.keys(params.output || {}).length > 0 ? params.output : undefined; } } diff --git a/packages/tasks/src/response/TaskResponse.ts b/packages/tasks/src/response/TaskResponse.ts index 2dd82c2b169..e6034ad016a 100644 --- a/packages/tasks/src/response/TaskResponse.ts +++ b/packages/tasks/src/response/TaskResponse.ts @@ -91,8 +91,8 @@ export class TaskResponse implements ITaskResponse { if (error instanceof Error) { return getErrorProperties(error); } else if (typeof error === "string") { - return new Error(error); + return getErrorProperties(new Error(error)); } - return error; + return getErrorProperties(error); } } diff --git a/packages/tasks/src/response/abstractions/ResponseContinueResult.ts b/packages/tasks/src/response/abstractions/ResponseContinueResult.ts index 98f5db3731e..778a429d8cf 100644 --- a/packages/tasks/src/response/abstractions/ResponseContinueResult.ts +++ b/packages/tasks/src/response/abstractions/ResponseContinueResult.ts @@ -7,6 +7,7 @@ import { IResponseBaseResult } from "./ResponseBaseResult"; */ export interface IResponseContinueParams { + message?: string; tenant?: string; locale?: string; webinyTaskId?: string; diff --git a/packages/tasks/src/response/abstractions/ResponseErrorResult.ts b/packages/tasks/src/response/abstractions/ResponseErrorResult.ts index 5a6ca23e3e5..7cf611ee523 100644 --- a/packages/tasks/src/response/abstractions/ResponseErrorResult.ts +++ b/packages/tasks/src/response/abstractions/ResponseErrorResult.ts @@ -1,10 +1,11 @@ import { TaskResponseStatus } from "~/types"; import { IResponseBaseResult } from "./ResponseBaseResult"; +import { GenericRecord } from "@webiny/api/types"; export interface IResponseError { message: string; - code?: string; - data?: Record; + code?: string | null; + data?: GenericRecord | null; stack?: string; } diff --git a/packages/tasks/src/response/abstractions/TaskResponse.ts b/packages/tasks/src/response/abstractions/TaskResponse.ts index 87c9d66c3dd..5a9e65e2864 100644 --- a/packages/tasks/src/response/abstractions/TaskResponse.ts +++ b/packages/tasks/src/response/abstractions/TaskResponse.ts @@ -55,19 +55,13 @@ export type ITaskResponseContinueOptions = | ITaskResponseContinueOptionsUntil | ITaskResponseContinueOptionsSeconds; -export interface ITaskResponseDoneCallable< - O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput -> { - (output?: O): ITaskResponseDoneResult; - (message?: string, output?: O): ITaskResponseDoneResult; -} - export interface ITaskResponse< T = ITaskDataInput, O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput > { - done: ITaskResponseDoneCallable; - continue: (data: T, options?: ITaskResponseContinueOptions) => ITaskResponseContinueResult; - error: (error: IResponseError | Error | string) => ITaskResponseErrorResult; - aborted: () => ITaskResponseAbortedResult; + done(output?: O): ITaskResponseDoneResult; + done(message?: string, output?: O): ITaskResponseDoneResult; + continue(data: T, options?: ITaskResponseContinueOptions): ITaskResponseContinueResult; + error(error: IResponseError | Error | string): ITaskResponseErrorResult; + aborted(): ITaskResponseAbortedResult; } diff --git a/packages/tasks/src/runner/TaskControl.ts b/packages/tasks/src/runner/TaskControl.ts index 094b80d2229..72ad94e5950 100644 --- a/packages/tasks/src/runner/TaskControl.ts +++ b/packages/tasks/src/runner/TaskControl.ts @@ -40,8 +40,12 @@ export class TaskControl implements ITaskControl { task = await this.getTask(taskId); this.context.security.setIdentity(task.createdBy); } catch (error) { + /** + * TODO Refactor error handling. + */ + // @ts-expect-error return this.response.error({ - error + ...getErrorProperties(error) }); } /** diff --git a/packages/tasks/src/runner/TaskManager.ts b/packages/tasks/src/runner/TaskManager.ts index a320b496ec2..0cb6732b7ed 100644 --- a/packages/tasks/src/runner/TaskManager.ts +++ b/packages/tasks/src/runner/TaskManager.ts @@ -120,7 +120,7 @@ export class TaskManager implements ITaskManager { input, context: this.context, response: this.taskResponse, - isCloseToTimeout: (seconds?: number) => { + isCloseToTimeout: seconds => { return this.runner.isCloseToTimeout(seconds); }, isAborted: () => { diff --git a/packages/tasks/src/runner/TaskManagerStore.ts b/packages/tasks/src/runner/TaskManagerStore.ts index 80e0a196f68..c7f3e21f92e 100644 --- a/packages/tasks/src/runner/TaskManagerStore.ts +++ b/packages/tasks/src/runner/TaskManagerStore.ts @@ -1,4 +1,5 @@ import { + IListTaskParamsWhere, ITask, ITaskDataInput, ITaskLog, @@ -25,6 +26,7 @@ import { import deepEqual from "deep-equal"; import { getObjectProperties } from "~/utils/getObjectProperties"; import { ObjectUpdater } from "~/utils/ObjectUpdater"; +import { GenericRecord } from "@webiny/api/types"; const getInput = ( originalInput: T, @@ -40,7 +42,7 @@ const getInput = ( }; export interface TaskManagerStoreContext { - tasks: Pick; + tasks: Pick; } export interface ITaskManagerStoreParams { @@ -78,6 +80,24 @@ export class TaskManagerStore< return this.task as ITask; } + public async listChildTasks< + I = GenericRecord, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput + >(definitionId?: string): Promise[]> { + const where: IListTaskParamsWhere = { + parentId: this.task.id + }; + if (definitionId) { + where.definitionId = definitionId; + } + const result = await this.context.tasks.listTasks({ + where, + sort: ["createdOn_ASC"], + limit: 1000000 + }); + return result.items; + } + public async updateTask( param: ITaskManagerStoreUpdateTaskParams, options?: ITaskManagerStoreUpdateTaskOptions diff --git a/packages/tasks/src/runner/abstractions/TaskManagerStore.ts b/packages/tasks/src/runner/abstractions/TaskManagerStore.ts index 1bf6029599b..a4de2cbeb97 100644 --- a/packages/tasks/src/runner/abstractions/TaskManagerStore.ts +++ b/packages/tasks/src/runner/abstractions/TaskManagerStore.ts @@ -7,6 +7,7 @@ import { ITaskUpdateData, TaskDataStatus } from "~/types"; +import { GenericRecord } from "@webiny/api/types"; export type ITaskManagerStoreUpdateTaskValues = T; @@ -87,6 +88,16 @@ export interface ITaskManagerStorePrivate< params: ITaskManagerStoreUpdateTaskParams, options?: ITaskManagerStoreUpdateTaskOptions ): Promise; + /** + * List all child tasks of the current task. + * If definitionId is provided, filter by that parameter. + */ + listChildTasks< + T = GenericRecord, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput + >( + definitionId?: string + ): Promise[]>; /** * Update the task input, which are used to store custom user data. * You can send partial input, and it will be merged with the existing input. diff --git a/packages/tasks/src/runner/abstractions/TaskRunner.ts b/packages/tasks/src/runner/abstractions/TaskRunner.ts index 71eda56f6c5..da4f8bca9fb 100644 --- a/packages/tasks/src/runner/abstractions/TaskRunner.ts +++ b/packages/tasks/src/runner/abstractions/TaskRunner.ts @@ -2,8 +2,12 @@ import { Context } from "~/types"; import { ITaskEvent } from "~/handler/types"; import { IResponseResult } from "~/response/abstractions"; +export interface IIsCloseToTimeoutCallable { + (seconds?: number): boolean; +} + export interface ITaskRunner { context: C; - isCloseToTimeout(seconds?: number): boolean; + isCloseToTimeout: IIsCloseToTimeoutCallable; run(event: ITaskEvent): Promise; } diff --git a/packages/tasks/src/types.ts b/packages/tasks/src/types.ts index ada089ca584..ae3e2314a31 100644 --- a/packages/tasks/src/types.ts +++ b/packages/tasks/src/types.ts @@ -13,7 +13,7 @@ import { ITaskResponseDoneResultOutput, ITaskResponseResult } from "~/response/abstractions"; -import { ITaskManagerStore } from "./runner/abstractions"; +import { IIsCloseToTimeoutCallable, ITaskManagerStore } from "./runner/abstractions"; import { PutEventsCommandOutput } from "@webiny/aws-sdk/client-eventbridge"; import { SecurityPermission } from "@webiny/api-security/types"; import { GenericRecord } from "@webiny/api/types"; @@ -85,7 +85,7 @@ export interface ITaskIdentity { } export interface ITask< - T = any, + T = GenericRecord, O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput > { /** @@ -136,34 +136,39 @@ export type IUpdateTaskResponse< > = ITask; export type IDeleteTaskResponse = boolean; +export interface IListTaskParamsWhere extends CmsEntryListWhere { + parentId?: string; + parentId_not?: string; + parentId_in?: string[]; + parentId_not_in?: string[]; + definitionId?: string; + definitionId_not?: string; + definitionId_in?: string[]; + definitionId_not_in?: string[]; + taskStatus?: string; + taskStatus_not?: string; + taskStatus_in?: string[]; + taskStatus_not_in?: string[]; +} + export interface IListTaskParams extends Omit { - where?: CmsEntryListWhere & { - parentId?: string; - parentId_not?: string; - parentId_in?: string[]; - parentId_not_in?: string[]; - definitionId?: string; - definitionId_not?: string; - definitionId_in?: string[]; - definitionId_not_in?: string[]; - taskStatus?: string; - taskStatus_not?: string; - taskStatus_in?: string[]; - taskStatus_not_in?: string[]; - }; + where?: IListTaskParamsWhere; } + +export interface IListTaskLogParamsWhere extends CmsEntryListWhere { + task?: string; + task_in?: string[]; + task_not?: string; + iteration?: number; + iteration_not?: number; + iteration_gte?: number; + iteration_gt?: number; + iteration_lte?: number; + iteration_lt?: number; +} + export interface IListTaskLogParams extends Omit { - where?: CmsEntryListWhere & { - task?: string; - task_in?: string[]; - task_not?: string; - iteration?: number; - iteration_not?: number; - iteration_gte?: number; - iteration_gt?: number; - iteration_lte?: number; - iteration_lt?: number; - }; + where?: IListTaskLogParamsWhere; } export interface ITaskCreateData { @@ -273,12 +278,18 @@ export interface ITasksContextConfigObject { } export interface ITasksContextDefinitionObject { - getDefinition: (id: string) => ITaskDefinition | null; + getDefinition: < + C extends Context = Context, + I = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput + >( + id: string + ) => ITaskDefinition | null; listDefinitions: () => ITaskDefinition[]; } export interface ITaskTriggerParams { - parent?: ITask; + parent?: Pick; definition: string; name?: string; input?: I; @@ -291,8 +302,18 @@ export interface ITaskAbortParams { } export interface ITasksContextTriggerObject { - trigger: (params: ITaskTriggerParams) => Promise>; - abort: (params: ITaskAbortParams) => Promise>; + trigger: < + T = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput + >( + params: ITaskTriggerParams + ) => Promise>; + abort: < + T = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput + >( + params: ITaskAbortParams + ) => Promise>; } export interface ITasksContextObject @@ -312,7 +333,7 @@ export interface ITaskRunParams< > { context: C; response: ITaskResponse; - isCloseToTimeout(seconds?: number): boolean; + isCloseToTimeout: IIsCloseToTimeoutCallable; isAborted(): boolean; input: I; store: ITaskManagerStore; @@ -321,19 +342,31 @@ export interface ITaskRunParams< ): Promise>; } -export interface ITaskOnSuccessParams { +export interface ITaskOnSuccessParams< + C extends Context, + I = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput +> { context: C; - task: ITask; + task: ITask; } -export interface ITaskOnErrorParams { +export interface ITaskOnErrorParams< + C extends Context, + I = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput +> { context: C; - task: ITask; + task: ITask; } -export interface ITaskOnAbortParams { +export interface ITaskOnAbortParams< + C extends Context, + I = ITaskDataInput, + O extends ITaskResponseDoneResultOutput = ITaskResponseDoneResultOutput +> { context: C; - task: ITask; + task: ITask; } export interface ITaskOnMaxIterationsParams { @@ -410,7 +443,7 @@ export interface ITaskDefinition< * When task successfully finishes, this method will be called. * This will be called during the run time of the task. */ - onDone?(params: ITaskOnSuccessParams): Promise; + onDone?(params: ITaskOnSuccessParams): Promise; /** * When task fails, this method will be called. * This will be called during the run time of the task. @@ -420,7 +453,7 @@ export interface ITaskDefinition< * When task is aborted, this method will be called. * This method will be called when user aborts the task. */ - onAbort?(params: ITaskOnAbortParams): Promise; + onAbort?(params: ITaskOnAbortParams): Promise; /** * When task hits max iterations, this method will be called. * This will be called during the run time of the task. diff --git a/packages/tasks/src/utils/getErrorProperties.ts b/packages/tasks/src/utils/getErrorProperties.ts index 9226045c436..c44b522dd87 100644 --- a/packages/tasks/src/utils/getErrorProperties.ts +++ b/packages/tasks/src/utils/getErrorProperties.ts @@ -1,6 +1,10 @@ import { IResponseError } from "~/response/abstractions"; import { getObjectProperties } from "~/utils/getObjectProperties"; -export const getErrorProperties = (error: Error): IResponseError => { - return getObjectProperties(error); +export const getErrorProperties = (error: Error | IResponseError): IResponseError => { + const value = getObjectProperties(error); + + delete value.stack; + + return value; }; diff --git a/packages/tasks/src/utils/index.ts b/packages/tasks/src/utils/index.ts new file mode 100644 index 00000000000..4aea56724b5 --- /dev/null +++ b/packages/tasks/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./getErrorProperties"; +export * from "./getObjectProperties"; +export * from "./ObjectUpdater"; diff --git a/yarn.lock b/yarn.lock index 4c7c7b384d7..5bfff7f2472 100644 --- a/yarn.lock +++ b/yarn.lock @@ -291,153 +291,153 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-apigatewaymanagementapi@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-apigatewaymanagementapi@npm:3.621.0" +"@aws-sdk/client-apigatewaymanagementapi@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-apigatewaymanagementapi@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 38b8069a494ee2122f51709d1896aca90f2672e32577724839355d5de2e17f47149d124c19796c097fdd528358087fc4bd95ad74f6eef29346e1acf3056b650a + checksum: ce8659c8986d204fd0149eb080d16a377d73b375e5c0710a4dc235fa869e99489b3257057ffd0781eab8f64f0e65e6415dbd846e1c9399646ee9171b3043f0ae languageName: node linkType: hard -"@aws-sdk/client-cloudfront@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-cloudfront@npm:3.621.0" +"@aws-sdk/client-cloudfront@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-cloudfront@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@aws-sdk/xml-builder": 3.609.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@aws-sdk/xml-builder": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 - "@smithy/util-stream": ^3.1.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 + "@smithy/util-stream": ^3.1.6 "@smithy/util-utf8": ^3.0.0 - "@smithy/util-waiter": ^3.1.2 + "@smithy/util-waiter": ^3.1.5 tslib: ^2.6.2 - checksum: 5904ed02402989c4fd7e78fb4c7d00895baf629ee86d0efd1b28bbb17c83cc3986f28d011ae692a760ed0ca7c2712d9ab4784f7fae7f9eb1f66dcf1cb8c40b18 + checksum: 10846912585b6ae99c2882d7b09865f538b50ba265101a60c8e096db5bddbb254027c989521bc5f0b9b4c5383480c4ba4d578d3ecdeb8ed46e01dfc0fdbc1b4d languageName: node linkType: hard -"@aws-sdk/client-cloudwatch-events@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-cloudwatch-events@npm:3.621.0" +"@aws-sdk/client-cloudwatch-events@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-cloudwatch-events@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: a217d09b3b8210c0042382cfd10f20a8f3c6970b30b6dac5b97595be497a68f842d24f341e9b3657f8353c6b9b9104cbad920c3fefc2af1e7e0487ca350f5f8c + checksum: aaaca4a8a21e0e892775d1ead486dd5d63de40e4d95e5aea3c9c4655c76daa3411b51fa56e799843b4b92be7a66cfe3adc7d72f8cf9e153afa798c3292a75ab3 languageName: node linkType: hard @@ -480,105 +480,105 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cloudwatch-logs@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-cloudwatch-logs@npm:3.621.0" +"@aws-sdk/client-cloudwatch-logs@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-cloudwatch-logs@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/eventstream-serde-browser": ^3.0.5 - "@smithy/eventstream-serde-config-resolver": ^3.0.3 - "@smithy/eventstream-serde-node": ^3.0.4 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/eventstream-serde-browser": ^3.0.9 + "@smithy/eventstream-serde-config-resolver": ^3.0.6 + "@smithy/eventstream-serde-node": ^3.0.8 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: 776975213422902f7c052c696b4e9103efbad6b601fde6f5b16ef2665f0bd4287e38f2c208b779454624c6158a58b459f239febafdbb8a64f71fbe1ad1797fa3 + checksum: 55d7eb78a136f44e78f6848badeaa2b8a7916fd16f7b9117d2813ff78b84d3523eee68fd099ea9f9663f0094cdab4ed2369ee6ddbdb2ebe7b23e9ba33ad0b069 languageName: node linkType: hard -"@aws-sdk/client-cognito-identity-provider@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-cognito-identity-provider@npm:3.621.0" +"@aws-sdk/client-cognito-identity-provider@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-cognito-identity-provider@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 16e3cad6b491897a539eee042737d7e8ff5e08c64e1eb231b560b6b5fdc2617d23c608356f73f6f9235317fbb51f4a41e967e5b8e96c47567a5554937f122204 + checksum: 19bfb8b8f09751ec04f6ab9f4576ae6b92d3ae5188d75e7ffbd6fa3779a3f84950938963c2a82c01f42e2480c6caa9d3580d6a4921bef0adefbc9b66e3941ee3 languageName: node linkType: hard @@ -621,666 +621,667 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.621.0" +"@aws-sdk/client-cognito-identity@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 227fb03620396c3d5f9ca36fd463fc2e3e2e04909f5dea59b90e25a89d110d3a869e89e870237b808b494156bba06fb966406d2dc192a8ad6a072b7abdd4d81c + checksum: 43b437cee74689b36b216646f9ad23c0c57b07674de076d5aaef17fee98d895a47287589e37bb3ae0ac7b3abe3ef8596b6fadd1cd2f2ecfb434d4e2183f7dffd languageName: node linkType: hard -"@aws-sdk/client-dynamodb-streams@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-dynamodb-streams@npm:3.621.0" +"@aws-sdk/client-dynamodb-streams@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-dynamodb-streams@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 8d76384a65cca4238dcfc0c778c596338a0814880e0ab013d1e6eea8ee188f9e26eb26f0da84f7349eee4aa1600ab71a1ad3a53fcae51b9559d26594bcf22e56 + checksum: 2ce093c8ab7d443032f49cf9b30f9bbbda715526ae82894845bf58408200ffa5c0622c43730d30c219e06b3c41fa67ab13f6ba4e42d95ebcb8b399a5a4ccdc17 languageName: node linkType: hard -"@aws-sdk/client-dynamodb@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-dynamodb@npm:3.621.0" +"@aws-sdk/client-dynamodb@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-dynamodb@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-endpoint-discovery": 3.620.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-endpoint-discovery": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 - "@smithy/util-waiter": ^3.1.2 + "@smithy/util-waiter": ^3.1.5 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: f2535fd1a8cdf8a0fba422b5d03bee11b072dd3d1edd26fb923a97c350dcae52dab3b246d25f809e1513526661f3b9fc0cb9ce08ba42b7b4ead69145cfa67c6d + checksum: f85ddd44ffffb2d76aeaf66aee0b3167e2338a1899d846d96014740e5ccd1d8fc7d24d3d09454eed08cb416106e16afc01408db6f46adad78852fad461775e23 languageName: node linkType: hard -"@aws-sdk/client-eventbridge@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-eventbridge@npm:3.621.0" +"@aws-sdk/client-eventbridge@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-eventbridge@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-signing": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/signature-v4-multi-region": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/signature-v4-multi-region": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: bb176c075593af5fe4b916b22de01ff1f565538eacaa86c0fc663fd06bd8830959089ee7daaf522f31594670e80b88fe564c5c552f9ccbeffc778e8cbc001db2 + checksum: eb1ff81c81350b78b86d7ab32ba1c8a0515cd4ff03008cb81d04a3f28b710b9d5844dc16e90fa8baed2050ef98b1ea2694720cf45500c9a298f9b3b9f46286e0 languageName: node linkType: hard -"@aws-sdk/client-iam@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-iam@npm:3.621.0" +"@aws-sdk/client-iam@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-iam@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 - "@smithy/util-waiter": ^3.1.2 + "@smithy/util-waiter": ^3.1.5 tslib: ^2.6.2 - checksum: 4f211e8a6decba1a86c05acdb52eb678c09145e164a8a3962c3973c4e4ec6426f8931215df846644aa477c8425ce4cf133e01caebf12027b2a78dd0a23e2704e + checksum: 28fe8dabcd599e9240d40b43deaf2378797d154df85172da765167b80fba1657d766718ac035704317e8b66f19e08de6d16186233feaede185faa5b4fd9d71a5 languageName: node linkType: hard -"@aws-sdk/client-iot@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-iot@npm:3.621.0" +"@aws-sdk/client-iot@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-iot@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: b7730e46a532eedd286dd068e0d1e4f01a341c95eae36e26acf93fed257d563ff9cf99aa5297c1812601586191442a68ef82400a44058294cff814c78958ee29 + checksum: 713ec44c4daa11498bb83c40075f327fe30457b33c9a21e7c0c1634eb9385da689b87c701f179786e55ce355d4703fa2cbb1311b8e0871624b916f9e3461c5a8 languageName: node linkType: hard -"@aws-sdk/client-lambda@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-lambda@npm:3.621.0" +"@aws-sdk/client-lambda@npm:^3.654.0": + version: 3.655.0 + resolution: "@aws-sdk/client-lambda@npm:3.655.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/eventstream-serde-browser": ^3.0.5 - "@smithy/eventstream-serde-config-resolver": ^3.0.3 - "@smithy/eventstream-serde-node": ^3.0.4 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/eventstream-serde-browser": ^3.0.9 + "@smithy/eventstream-serde-config-resolver": ^3.0.6 + "@smithy/eventstream-serde-node": ^3.0.8 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 - "@smithy/util-stream": ^3.1.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 + "@smithy/util-stream": ^3.1.6 "@smithy/util-utf8": ^3.0.0 - "@smithy/util-waiter": ^3.1.2 + "@smithy/util-waiter": ^3.1.5 tslib: ^2.6.2 - checksum: d3e87d606a89f17e66176b47d4bc6c41876182b68be7a8747f2dcadbe2ef481ded0076a8d95d5f2cfb4b75d37b39f59665e557f9b0d71f61eb767dba676cc331 + checksum: 3d71a3badcdc7212abcf3d02703e0e507a7b2734eff9500c0a293e968d7295deeb20f49f5815360c77861fcd43dab7866f17d6e539d346978f5812a589c49be3 languageName: node linkType: hard -"@aws-sdk/client-s3@npm:3.621.0, @aws-sdk/client-s3@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-s3@npm:3.621.0" +"@aws-sdk/client-s3@npm:3.654.0, @aws-sdk/client-s3@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-s3@npm:3.654.0" dependencies: "@aws-crypto/sha1-browser": 5.2.0 "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-bucket-endpoint": 3.620.0 - "@aws-sdk/middleware-expect-continue": 3.620.0 - "@aws-sdk/middleware-flexible-checksums": 3.620.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-location-constraint": 3.609.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-sdk-s3": 3.621.0 - "@aws-sdk/middleware-signing": 3.620.0 - "@aws-sdk/middleware-ssec": 3.609.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/signature-v4-multi-region": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@aws-sdk/xml-builder": 3.609.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/eventstream-serde-browser": ^3.0.5 - "@smithy/eventstream-serde-config-resolver": ^3.0.3 - "@smithy/eventstream-serde-node": ^3.0.4 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-blob-browser": ^3.1.2 - "@smithy/hash-node": ^3.0.3 - "@smithy/hash-stream-node": ^3.1.2 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/md5-js": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-bucket-endpoint": 3.654.0 + "@aws-sdk/middleware-expect-continue": 3.654.0 + "@aws-sdk/middleware-flexible-checksums": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-location-constraint": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-sdk-s3": 3.654.0 + "@aws-sdk/middleware-ssec": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/signature-v4-multi-region": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@aws-sdk/xml-builder": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/eventstream-serde-browser": ^3.0.9 + "@smithy/eventstream-serde-config-resolver": ^3.0.6 + "@smithy/eventstream-serde-node": ^3.0.8 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-blob-browser": ^3.1.5 + "@smithy/hash-node": ^3.0.6 + "@smithy/hash-stream-node": ^3.1.5 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/md5-js": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-retry": ^3.0.3 - "@smithy/util-stream": ^3.1.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 + "@smithy/util-stream": ^3.1.6 "@smithy/util-utf8": ^3.0.0 - "@smithy/util-waiter": ^3.1.2 + "@smithy/util-waiter": ^3.1.5 tslib: ^2.6.2 - checksum: 120bfaa142a61a8dbc2f82d6fe5e36cb2ec0acdb12390795d15326dfad4f67b0d2e95515afb04da34b3f686fef0368f7a79a20c85735b28df654cbc5bdb1f04c + checksum: 39da200413335ef4fbb64cffa94ffbb5d2dfbb09841d7c10e19c6f7e50790edd72f8fe0bc87b82950c9e65bf02ea24786f67ccc068e6064ced96cbe44baa0a2b languageName: node linkType: hard -"@aws-sdk/client-sfn@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-sfn@npm:3.621.0" +"@aws-sdk/client-sfn@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-sfn@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: 06c7e12214dd4c04a5c1b4fd2d88d810c5c9142650e1d58c01ffe705153349a7f741d958aaf10a09e217e87377747eded6f59188c5d058dee4252e75ce5557a4 + checksum: 6a0a6773d014d8169a6ea43659dd7a3153212637a51d7d2383b0b2c5602e3b12019f62ab024994c36d15a833956d79fc341fe98b6bf7c370a2684f2f02641d94 languageName: node linkType: hard -"@aws-sdk/client-sqs@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-sqs@npm:3.621.0" +"@aws-sdk/client-sqs@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-sqs@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-sdk-sqs": 3.621.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/md5-js": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-sdk-sqs": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/md5-js": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 293d3164570623e6228fc9e14c803b20e7ca5e562ce470f6f8ee3adf2b9a4cffd886ab69859d60afa1cf46f2df8cd9e0190a081335c1c2be21fee0739f21ce9f + checksum: 2cd7b9f4075bfcd82d22bfe45e793827d9012d0ced50014e584bae4dde334eff9645fbbbdbdb105bc791eff2a454a2dfe5709ee6d3e55f00b80c58a50c7b8eb0 languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.621.0" +"@aws-sdk/client-sso-oidc@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-sts": ^3.621.0 - checksum: 12aa50a56f2498a9202b877b0b789fa95c038707b648dc3c9e93976c209ed3d4518868279707ee8fe12f42858eba7181098bdbf68556341c9017bd5e31f89135 + "@aws-sdk/client-sts": ^3.654.0 + checksum: d4a4b978629c290f1c26e1e50e48171dfa58bce938e97fd6c4ad605cc3c40b7a3c51e3a5925816b5be09cfec5cb0c7f5bf4abc9e0244544dd4cc7bc94a8865db languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-sso@npm:3.621.0" +"@aws-sdk/client-sso@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-sso@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: fe72875685c07b9cf42b7fcde06502d42a5398161b37de87c5360f10a0d28cf7b32fc95f5eecdf782cff04c8109bd7a20d9644bfacc78e3521b6a7670ed76b40 + checksum: beb1ef4bfd8ed34d35b7eca962a989d1d560a86eb652d55175a68304f17a2c2975ac95f5e920d661caaafecf8eb5195c809279e6d7d776a37a90d329988419c3 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.621.0, @aws-sdk/client-sts@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/client-sts@npm:3.621.0" +"@aws-sdk/client-sts@npm:3.654.0, @aws-sdk/client-sts@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/client-sts@npm:3.654.0" dependencies: "@aws-crypto/sha256-browser": 5.2.0 "@aws-crypto/sha256-js": 5.2.0 - "@aws-sdk/client-sso-oidc": 3.621.0 - "@aws-sdk/core": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/middleware-host-header": 3.620.0 - "@aws-sdk/middleware-logger": 3.609.0 - "@aws-sdk/middleware-recursion-detection": 3.620.0 - "@aws-sdk/middleware-user-agent": 3.620.0 - "@aws-sdk/region-config-resolver": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@aws-sdk/util-user-agent-browser": 3.609.0 - "@aws-sdk/util-user-agent-node": 3.614.0 - "@smithy/config-resolver": ^3.0.5 - "@smithy/core": ^2.3.1 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/hash-node": ^3.0.3 - "@smithy/invalid-dependency": ^3.0.3 - "@smithy/middleware-content-length": ^3.0.5 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@aws-sdk/client-sso-oidc": 3.654.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/middleware-host-header": 3.654.0 + "@aws-sdk/middleware-logger": 3.654.0 + "@aws-sdk/middleware-recursion-detection": 3.654.0 + "@aws-sdk/middleware-user-agent": 3.654.0 + "@aws-sdk/region-config-resolver": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@aws-sdk/util-user-agent-browser": 3.654.0 + "@aws-sdk/util-user-agent-node": 3.654.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/core": ^2.4.3 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/hash-node": ^3.0.6 + "@smithy/invalid-dependency": ^3.0.6 + "@smithy/middleware-content-length": ^3.0.8 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 "@smithy/util-base64": ^3.0.0 "@smithy/util-body-length-browser": ^3.0.0 "@smithy/util-body-length-node": ^3.0.0 - "@smithy/util-defaults-mode-browser": ^3.0.13 - "@smithy/util-defaults-mode-node": ^3.0.13 - "@smithy/util-endpoints": ^2.0.5 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/util-defaults-mode-browser": ^3.0.18 + "@smithy/util-defaults-mode-node": ^3.0.18 + "@smithy/util-endpoints": ^2.1.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 35f543e1c4b419dc598489b4ce4dfbce22ad726ffe8f807876de13c0e8b5db10684835f7b95841e988d4f16aeb921935133e5775d35b47186c18cced6715a82a + checksum: 27c793be26d637f304bc80acb8d763d59c2ea4618d6adead95f36bb5f0bdf5ee3f00222497dd7114d23b7f3320b15171c317e7b588b6e6a91f07ef618f9e5478 languageName: node linkType: hard @@ -1295,20 +1296,21 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/core@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/core@npm:3.621.0" +"@aws-sdk/core@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/core@npm:3.654.0" dependencies: - "@smithy/core": ^2.3.1 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/signature-v4": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/util-middleware": ^3.0.3 + "@smithy/core": ^2.4.3 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/property-provider": ^3.1.6 + "@smithy/protocol-http": ^4.1.3 + "@smithy/signature-v4": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/util-middleware": ^3.0.6 fast-xml-parser: 4.4.1 tslib: ^2.6.2 - checksum: 789409227f2aa0f3c735e0f1bebf1d900b359e589bad50324e2fbec20ddb30435e1dcf9132501dad7aaf793829530980d1670257208c5e407c78be5f831e796d + checksum: 9e4b92d0e36aee68856afe09ca11f4f246eb325b3b087da6c2f70d3ef4e268f0fa76e2e77b6fa39021955a3ad933379aa89f48ce3b9b782762d4252f7f61d441 languageName: node linkType: hard @@ -1324,16 +1326,16 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.621.0" +"@aws-sdk/credential-provider-cognito-identity@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.654.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/types": ^3.3.0 + "@aws-sdk/client-cognito-identity": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: e8a913d61ac4e772e07cd1a6144ef8680e630894c2192aed81d06f36fb9607c4de3b95386155d52f050b99d8b4d78ceb69eb7fa0987d339631765cc1dc8c9588 + checksum: 525f049682a8209a899ae7efb7fd53f33e938343969758c57de49bf107748b01aafe8a66b60459d11062c2927734bf2785b64156362633154569ed7a81434b83 languageName: node linkType: hard @@ -1348,32 +1350,32 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.620.1": - version: 3.620.1 - resolution: "@aws-sdk/credential-provider-env@npm:3.620.1" +"@aws-sdk/credential-provider-env@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 3e05eeb6c6490e7759d03b7b688ef321d5e6c97929f21226e912b01aa926815312043febd505e8b43b54010b5ae2717559aba6deeea93bda7d6cf0d8a0b9fc76 + checksum: ca43bfe589dc4db0f156855c0ec6c5859533000a37f75eb701747cda374bf9e62a0e11358c5c4ad41c574d63e714a4be20087579ff24fd22dd0507c2c1645ab5 languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.621.0" +"@aws-sdk/credential-provider-http@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/property-provider": ^3.1.3 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/util-stream": ^3.1.3 + "@aws-sdk/types": 3.654.0 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/property-provider": ^3.1.6 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/util-stream": ^3.1.6 tslib: ^2.6.2 - checksum: bc45dfe7d0a6868978d71fa73731784af1839f822c09ef13b5a08ad74fa57c008dd874dd1765317df61ab022f9470bf509df38a70dd0aa5262d1d2234cc5eda2 + checksum: 9eac0812e89afd063d66b1d984ca36ff04e53332eda36d12b2f20784f64e0fe0fd60370ab127c4b4a6187fe88cf5b9779b3013b4a3bce7b23ab67da20a3d82a4 languageName: node linkType: hard @@ -1400,24 +1402,24 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.621.0" +"@aws-sdk/credential-provider-ini@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.654.0" dependencies: - "@aws-sdk/credential-provider-env": 3.620.1 - "@aws-sdk/credential-provider-http": 3.621.0 - "@aws-sdk/credential-provider-process": 3.620.1 - "@aws-sdk/credential-provider-sso": 3.621.0 - "@aws-sdk/credential-provider-web-identity": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@smithy/credential-provider-imds": ^3.2.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/shared-ini-file-loader": ^3.1.4 - "@smithy/types": ^3.3.0 + "@aws-sdk/credential-provider-env": 3.654.0 + "@aws-sdk/credential-provider-http": 3.654.0 + "@aws-sdk/credential-provider-process": 3.654.0 + "@aws-sdk/credential-provider-sso": 3.654.0 + "@aws-sdk/credential-provider-web-identity": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@smithy/credential-provider-imds": ^3.2.3 + "@smithy/property-provider": ^3.1.6 + "@smithy/shared-ini-file-loader": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-sts": ^3.621.0 - checksum: a3db43adfbdb2ea1d6d80b1ca6789e48b729fce2499039f99721f504322cba325e4b48ed842bd26ae6f49ca771e5817016b75cda31cf247fd3e2bd0ac235b332 + "@aws-sdk/client-sts": ^3.654.0 + checksum: a227a95e2fb08937ad7a58edeaf831c79ae38f995bdac2358c8496b3950dea3b77ada198014bd3dd98a9b26ae734067553a7589d1e8c4caad87fd8d159e53a81 languageName: node linkType: hard @@ -1437,23 +1439,23 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.621.0" +"@aws-sdk/credential-provider-node@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.654.0" dependencies: - "@aws-sdk/credential-provider-env": 3.620.1 - "@aws-sdk/credential-provider-http": 3.621.0 - "@aws-sdk/credential-provider-ini": 3.621.0 - "@aws-sdk/credential-provider-process": 3.620.1 - "@aws-sdk/credential-provider-sso": 3.621.0 - "@aws-sdk/credential-provider-web-identity": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@smithy/credential-provider-imds": ^3.2.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/shared-ini-file-loader": ^3.1.4 - "@smithy/types": ^3.3.0 + "@aws-sdk/credential-provider-env": 3.654.0 + "@aws-sdk/credential-provider-http": 3.654.0 + "@aws-sdk/credential-provider-ini": 3.654.0 + "@aws-sdk/credential-provider-process": 3.654.0 + "@aws-sdk/credential-provider-sso": 3.654.0 + "@aws-sdk/credential-provider-web-identity": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@smithy/credential-provider-imds": ^3.2.3 + "@smithy/property-provider": ^3.1.6 + "@smithy/shared-ini-file-loader": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: aada1f0624caee0e00b9ba287e66120031c8f5095acbcd80723b2ab6b5e4bffcd662665fffe62b7f2091bfb89d33ea13212a82893318005699914f13fbed70c3 + checksum: e87ab967059c28a8114eac718ac0567b4f1c1f04e502f38e4103a5b2d46836a16f38efbe73f745b428631257329302f4087abce0fe8bead01919d06151743a3d languageName: node linkType: hard @@ -1470,69 +1472,69 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:3.620.1": - version: 3.620.1 - resolution: "@aws-sdk/credential-provider-process@npm:3.620.1" +"@aws-sdk/credential-provider-process@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/shared-ini-file-loader": ^3.1.4 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/shared-ini-file-loader": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 1fd6d3abf35ccfaed0882ee74e4d876e8c6cfe8d8b23c574dfe98364b2a2f3226d97fe35e2d48b88dd514906595cbb5fa1e0066d989a9582f73f92117796168f + checksum: 1aff957324871821ec2fe9b4741229926ec4e6246f7af7b7b49ec7ac995b9be8bbcadae907208877542b32ee9051b65972406049e8adb2771287709de9c8ee52 languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.621.0" +"@aws-sdk/credential-provider-sso@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.654.0" dependencies: - "@aws-sdk/client-sso": 3.621.0 - "@aws-sdk/token-providers": 3.614.0 - "@aws-sdk/types": 3.609.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/shared-ini-file-loader": ^3.1.4 - "@smithy/types": ^3.3.0 + "@aws-sdk/client-sso": 3.654.0 + "@aws-sdk/token-providers": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/shared-ini-file-loader": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 4602308a0bf74bda9dd7b92caf02868b855935deb703a8894d052c81864ef9c9359408ec84569f71527c1b204577940ebe1a9f6cbba8de677471eb590d91c3ba + checksum: 3c434ce89101034afb58d757e45a48ab37184ad4823f8318baea26d60773308e3ec9627fbe3ec91d325f4a473133126f401e1311284cbd5315e7007d9d14a055 languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.621.0" +"@aws-sdk/credential-provider-web-identity@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-sts": ^3.621.0 - checksum: b1157e83b81c21f384ad95d29c646149124136c38e6395265ff8d9f5f6afec1e3e2b70c1f0be165c440058802c3871f3766c5e7ba44705ced447de221f4d04df - languageName: node - linkType: hard - -"@aws-sdk/credential-providers@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/credential-providers@npm:3.621.0" - dependencies: - "@aws-sdk/client-cognito-identity": 3.621.0 - "@aws-sdk/client-sso": 3.621.0 - "@aws-sdk/client-sts": 3.621.0 - "@aws-sdk/credential-provider-cognito-identity": 3.621.0 - "@aws-sdk/credential-provider-env": 3.620.1 - "@aws-sdk/credential-provider-http": 3.621.0 - "@aws-sdk/credential-provider-ini": 3.621.0 - "@aws-sdk/credential-provider-node": 3.621.0 - "@aws-sdk/credential-provider-process": 3.620.1 - "@aws-sdk/credential-provider-sso": 3.621.0 - "@aws-sdk/credential-provider-web-identity": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@smithy/credential-provider-imds": ^3.2.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/types": ^3.3.0 + "@aws-sdk/client-sts": ^3.654.0 + checksum: 4aaca40595163805a018e3573a7201dc1b662df8a011f807f20de154e58b7e6c24b4427863a05cce30bd909e99429c6428f9308bdface125b7b67acc88030a99 + languageName: node + linkType: hard + +"@aws-sdk/credential-providers@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/credential-providers@npm:3.654.0" + dependencies: + "@aws-sdk/client-cognito-identity": 3.654.0 + "@aws-sdk/client-sso": 3.654.0 + "@aws-sdk/client-sts": 3.654.0 + "@aws-sdk/credential-provider-cognito-identity": 3.654.0 + "@aws-sdk/credential-provider-env": 3.654.0 + "@aws-sdk/credential-provider-http": 3.654.0 + "@aws-sdk/credential-provider-ini": 3.654.0 + "@aws-sdk/credential-provider-node": 3.654.0 + "@aws-sdk/credential-provider-process": 3.654.0 + "@aws-sdk/credential-provider-sso": 3.654.0 + "@aws-sdk/credential-provider-web-identity": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@smithy/credential-provider-imds": ^3.2.3 + "@smithy/property-provider": ^3.1.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 152a8911ca52a0663e4552e720f73bef2d93088d7b9fc58da88cf2aac91ebc3b720ea8abc5d28228d1658888903bd737dbba88e7d5cc97e2a2c7fdc0e5243c6f + checksum: 0765f03917f6333f46b80624ece765b768cc0c1ec7969585dcc05cf65169245ddefdd03adcc38149887c447a8f194d658f789b35540531cdc6ff0379e4ac0c95 languageName: node linkType: hard @@ -1589,49 +1591,50 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/lib-dynamodb@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/lib-dynamodb@npm:3.621.0" +"@aws-sdk/lib-dynamodb@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/lib-dynamodb@npm:3.654.0" dependencies: - "@aws-sdk/util-dynamodb": 3.621.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 + "@aws-sdk/util-dynamodb": 3.654.0 + "@smithy/core": ^2.4.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-dynamodb": ^3.621.0 - checksum: c1651a91e17f2c28827569974dcac9a0982ebf6d41daa0455426e96a900654d5705758a194852b7994b41eb21e9e254d6cc17ae2ed0aeabc64a11f41307f32b1 + "@aws-sdk/client-dynamodb": ^3.654.0 + checksum: e32e94fb570bd99dbcf4a01044de6de7d46020b143290ba9ecdc07e3a4fda96dab5689d950c52043a6615da65a7aca64e3d9ce0d397e9e964e04a4b49ac66de0 languageName: node linkType: hard -"@aws-sdk/lib-storage@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/lib-storage@npm:3.621.0" +"@aws-sdk/lib-storage@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/lib-storage@npm:3.654.0" dependencies: - "@smithy/abort-controller": ^3.1.1 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/smithy-client": ^3.1.11 + "@smithy/abort-controller": ^3.1.4 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/smithy-client": ^3.3.2 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-s3": ^3.621.0 - checksum: 6c84a1601bcbea53aabad52c2453afd3c577e3935eda4bb13a7d9d4b1405289a3f2f4747327fc30c5eeaf482a9a78d56dfb4453c4c892f876f491e677d0f6c95 + "@aws-sdk/client-s3": ^3.654.0 + checksum: ba80c3c5bd70b422fd74cdb02308df388a29c56143e16814e005f179158d9fb74ffa496680a31779a6e7059ad370ad31614dddeda81f3c1de18eb0905089f8a8 languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.620.0" +"@aws-sdk/middleware-bucket-endpoint@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 + "@aws-sdk/types": 3.654.0 "@aws-sdk/util-arn-parser": 3.568.0 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 "@smithy/util-config-provider": ^3.0.0 tslib: ^2.6.2 - checksum: a5539ad611d3a2f7709952bf735a075a9d18ed5e9c0abe4e268bca32775a2100f3e956a264f6afb0ad0b48600f5b1d3389ec0566cd2795e6ae2dbe5af322b56b + checksum: e08e7cce23b6ed1226d5e4c99bccbb4115978e3a55dad7e7fc41f5d7bcf49fc756b95ad4894809418c279a2f89e8c4cec46b781c79695f1cb00407aedd7c2d0b languageName: node linkType: hard @@ -1646,45 +1649,47 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-endpoint-discovery@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-endpoint-discovery@npm:3.620.0" +"@aws-sdk/middleware-endpoint-discovery@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-endpoint-discovery@npm:3.654.0" dependencies: "@aws-sdk/endpoint-cache": 3.572.0 - "@aws-sdk/types": 3.609.0 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 03626108c22468c48e6d80dce818177ad3a726416acb7e2a7cd5be5b3282b0db3c023363e1a5624da1eaf00d292b823d6bdedeacb14e580576ab6321f1d2391d + checksum: dafccb21b420f81c0b4b67e8beb4efecfd805a8dcbe3de16f0d33b70ab257600772cbefc78c9aca03fa8e2e79819288c00873da3f7b3fb07ce85fb51c21b67b0 languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.620.0" +"@aws-sdk/middleware-expect-continue@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 739af454854562342984ac5e3c1c3dca39205344f52eda5961dae80020595062eaae6da2f76bf48ad486d92041c2a2bd30f1dd67b8b20ae043897d1d27b1a6bb + checksum: de613780c7d0e73aa7e99dedba967c05619e771899db0954efd2c58399dfe05154fa045df11281a764995664d5521e61fbc9a62afeb482f6272f695e311f45a9 languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.620.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.654.0" dependencies: "@aws-crypto/crc32": 5.2.0 "@aws-crypto/crc32c": 5.2.0 - "@aws-sdk/types": 3.609.0 + "@aws-sdk/types": 3.654.0 "@smithy/is-array-buffer": ^3.0.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 + "@smithy/util-middleware": ^3.0.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 2fc70ffa826d94924c0dae92ed5250c85aade93cbc15a488df81683d8f32eb7660f546569b1c2d2e878db5c8da02a83a3a98c331c5bb3adca46362e667abcbed + checksum: a79192c9400cc13b558a002948ccd782bca4a8ac1ff894081b5bfefbe8f9e49b7afe82c27f775bf2f3a218ed5c661f3a5e649ee4baa17d3dea56f495b0d7ec3e languageName: node linkType: hard @@ -1699,26 +1704,26 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.620.0" +"@aws-sdk/middleware-host-header@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 829c2d230e5051704f45c5283c42bec657607f4e4b2dd251fda8a716be90f8c9dfd6e7d45892a9a558c35cb64711628b3d0f88b90fe8cd061382c70b473991ef + checksum: 04900e4be56760653535775ad00907e1cb426aadb333e4d513abcf05ff9208de31a78f08d4ee50b3951efdc29a1c6bb7c894ccb0e4e7db1ebd680786d0f09cd8 languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.609.0": - version: 3.609.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.609.0" +"@aws-sdk/middleware-location-constraint@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: f7962cf0b382efdf56cd07f8c0279efead02365edd7a2c124be39551b51a8359ee0d6f0399fcbf679ead3d235e24d1765f79712cf88e06c0a5432bf2d0c317d8 + checksum: 056152d0b82f4783e651da9d8ac0c1f85e535892ad3072e8d2d0976ea37a5937b82fa1da07579775a68a1a8ac0f837fab34901d2c22cc80860f54678a46d3f78 languageName: node linkType: hard @@ -1732,26 +1737,26 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.609.0": - version: 3.609.0 - resolution: "@aws-sdk/middleware-logger@npm:3.609.0" +"@aws-sdk/middleware-logger@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-logger@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: b6f67a2e9ba082c8aec9d45905ae45ea5a95896f1beecb0c2d7fecfe17dd8fad99513f43b11ed7fd6ca9ff7764a0fc1ce63af91b1baed92b36f7b4b5390be5c6 + checksum: 550a0ea9f96d6843b70f470173d909869c6ffd314ad76ba612dd73d4342c48034731e0705e69d458749a54c8cf027a22b5f27585c8a4883fe411b6a6f1dac45c languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.620.0" +"@aws-sdk/middleware-recursion-detection@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 1dedbbab2f79d4c0b214ca183a45daf89e98f9a33b2a08dcd7d30e059c1bcc04a74499b49870300d5f0b827d660512582f96f857624d3feaaf194a7a92703cea + checksum: f535dd89c9926dd935e2bfb8e77c268440602f343227a17bfcc625bba88601abe87c9b06f6551f5ba66e0f916c19b7cfacefe256a9ed065d2a63cfa8dd418763 languageName: node linkType: hard @@ -1769,36 +1774,39 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.621.0" +"@aws-sdk/middleware-sdk-s3@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 + "@aws-sdk/core": 3.654.0 + "@aws-sdk/types": 3.654.0 "@aws-sdk/util-arn-parser": 3.568.0 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/signature-v4": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 + "@smithy/core": ^2.4.3 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/protocol-http": ^4.1.3 + "@smithy/signature-v4": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 "@smithy/util-config-provider": ^3.0.0 - "@smithy/util-stream": ^3.1.3 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-stream": ^3.1.6 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 2db38ad1575e9209f053474ff69731d0c0910304fc14dcdf22a09c06a849cbada0b18f36879d96e3020c0e050f80b02e47c45137da561a3f243cc080f6df4f3a + checksum: 5668d23a1dd23e58159f72518b665af1668385ddf46310f50bc00928b4c1f549ed1d8b057dd8d423581479a3cafa4dac61df438314d0074c31fc822fe551966b languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sqs@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.621.0" +"@aws-sdk/middleware-sdk-sqs@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 "@smithy/util-hex-encoding": ^3.0.0 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: d0cad52b1ffbfc6078fd62865bb88b4ac3b3a06d883ad5ef0a419dfaefe11d33314892f862150801d57e8c277e3a1465f8e798a1176c1ad6090765faf325c1d0 + checksum: 6642fa295e4a369ef556eecdba4bfefc0f2b06aa8073e435d7848a7a4ac742e9612f5ae3496a3e3ba1e6187cd6ebb40b51d261c09ba0534921affd21717813b0 languageName: node linkType: hard @@ -1824,29 +1832,14 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-signing@npm:3.620.0" +"@aws-sdk/middleware-ssec@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/protocol-http": ^4.1.0 - "@smithy/signature-v4": ^4.1.0 - "@smithy/types": ^3.3.0 - "@smithy/util-middleware": ^3.0.3 + "@aws-sdk/types": 3.654.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: ef2365b282ccfe8dc29983b620ca6cd5f2e41f5f8d12b84d2e3cb73866535ba3f7383579e561ec5a3c422d3fefdfbb2f3461ae7806a9f87e4fe92fd10a929386 - languageName: node - linkType: hard - -"@aws-sdk/middleware-ssec@npm:3.609.0": - version: 3.609.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.609.0" - dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/types": ^3.3.0 - tslib: ^2.6.2 - checksum: 4b40627ed103159ef0db4cc6bdc2148d1a65b786f3d1c643d34bccc79b9d265495613dc9bb34d18d5ab9b21b5d31110e495ec2b077e6e2f7603a0493254180a2 + checksum: b4f78ff906d3a7a47989678be6ecd063a9bdab820d357dcd49704061a07c14073be9b0a1c24c7410f59b5933231006599809aa6941b93685d9db86da18b60273 languageName: node linkType: hard @@ -1870,16 +1863,16 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.620.0": - version: 3.620.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.620.0" +"@aws-sdk/middleware-user-agent@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-endpoints": 3.614.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-endpoints": 3.654.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 137a969b5c17ebf172aa1f5a393812c443425dd9833fdf786a3039af5b3197e7d17139659891858b268ff723d9f2c5fbc840e8ac6a7af4b1cb566a3d54746a51 + checksum: ecaf8d8587ce0d36ef00686d8fc988c31a411100ae4c96153edf4d443112ed722c83abc08f64a83734c5b8a47dfe65807cd3e723c80d940606e4e4d4f9462122 languageName: node linkType: hard @@ -1949,50 +1942,50 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.614.0": - version: 3.614.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.614.0" +"@aws-sdk/region-config-resolver@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/types": ^3.4.2 "@smithy/util-config-provider": ^3.0.0 - "@smithy/util-middleware": ^3.0.3 + "@smithy/util-middleware": ^3.0.6 tslib: ^2.6.2 - checksum: dbaca50792c99685845b21dd4a53228613e0458ee517a21db941890ee521d91eff80704f08e9ee71b6f04e70fb86362c4823750bb0b3727240af68d78d8fa4be + checksum: 2752ac29cdc52a3ef5a4bd030b3373b7296fb84b293ef8928b5852347b4a74f3f5b859d9f3cf3476efc28f807175016c86d20f9a6cef6c8301710434747b5acd languageName: node linkType: hard -"@aws-sdk/s3-presigned-post@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/s3-presigned-post@npm:3.621.0" +"@aws-sdk/s3-presigned-post@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/s3-presigned-post@npm:3.654.0" dependencies: - "@aws-sdk/client-s3": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-format-url": 3.609.0 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/signature-v4": ^4.1.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/client-s3": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-format-url": 3.654.0 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/signature-v4": ^4.1.3 + "@smithy/types": ^3.4.2 "@smithy/util-hex-encoding": ^3.0.0 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 77ffb73b9a2523dbbb8f3a093dc206a78f6433d55c20c8a1a07b10eb209f631d43e2a80a6e75db4a08e4576c2be57570e1dafb45a0fdf29e7081cc2265755bb7 + checksum: b245a1b2c9040cb48b61f8d197c052e33726ac04fc29fad9e8bbee8dc6a6da4b35297951285d86462863151613bc4cb708c6e91d94a3e228ce99bed5e78d8ef9 languageName: node linkType: hard -"@aws-sdk/s3-request-presigner@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/s3-request-presigner@npm:3.621.0" +"@aws-sdk/s3-request-presigner@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/s3-request-presigner@npm:3.654.0" dependencies: - "@aws-sdk/signature-v4-multi-region": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@aws-sdk/util-format-url": 3.609.0 - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 + "@aws-sdk/signature-v4-multi-region": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@aws-sdk/util-format-url": 3.654.0 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 5bb67dbff7676e5d5626a8d3ceb0664e845a944e4aca93d33aa8334e595be19cb743b0885e21d548293aa385d42507dca66bbb5710b35dec8e8afbef4dca29b9 + checksum: d92a8166b825958a11102e11693d640b094758afbab3d868f5e201107cb7778fc9003b2e3c4a077452ec00fb839f2524cd9c651bb9233247df81d37edb9670c9 languageName: node linkType: hard @@ -2012,17 +2005,17 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.621.0" +"@aws-sdk/signature-v4-multi-region@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.654.0" dependencies: - "@aws-sdk/middleware-sdk-s3": 3.621.0 - "@aws-sdk/types": 3.609.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/signature-v4": ^4.1.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/middleware-sdk-s3": 3.654.0 + "@aws-sdk/types": 3.654.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/signature-v4": ^4.1.3 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 741c07e65341da4c807d55b0bc6f1ec0064190c6fceb7571b0796f6ec94cc156a6214c0f308a93ad364884adae48502d248d30bd0ef43e7a4542586f264982c4 + checksum: 4b670f9f8d9f86341caba62c5de29b321f6e7a99d20d5f284b41ce08b906713c601761767043349768d8b7bbf73658e56302c66d60a4ec856f986e8ee51421a4 languageName: node linkType: hard @@ -2050,18 +2043,18 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.614.0": - version: 3.614.0 - resolution: "@aws-sdk/token-providers@npm:3.614.0" +"@aws-sdk/token-providers@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/token-providers@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/property-provider": ^3.1.3 - "@smithy/shared-ini-file-loader": ^3.1.4 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/shared-ini-file-loader": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-sso-oidc": ^3.614.0 - checksum: 2901b8428afc3b76ff1df9ac29a2698db6bf65d1d2afcd8424b9bf187313d2a3ca747c3b205afeb5c132068b5a5a94d84ce82710f775fa0cbb79499d7fea2d64 + "@aws-sdk/client-sso-oidc": ^3.654.0 + checksum: 98fbea851eba9f1cd18447f7520e15127157a50c57ba0ee7a9e56f220bd42f066e4c8132bd0a2e71e8de1314c700751323e38c76e8f5c09bae7444da043a39d8 languageName: node linkType: hard @@ -2072,13 +2065,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.609.0": - version: 3.609.0 - resolution: "@aws-sdk/types@npm:3.609.0" +"@aws-sdk/types@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/types@npm:3.654.0" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 522768d08f104065b0ff6a37eddaa7803186014acee1c0011b3dbd3ef841e47ae694e58f608aeec8a39d22d644d759ade996fe51d18b880617778dc2dbbe1ede + checksum: 2b26f08a1b57437b051afa3820b4deaa52dcee1534972b4a61c66ae409b59dc81dbf7ca226c845564e1e021a51e1c9a667ce0c471fe9e6bda9d29ab95ec92c7b languageName: node linkType: hard @@ -2170,38 +2163,38 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-dynamodb@npm:3.621.0, @aws-sdk/util-dynamodb@npm:^3.621.0": - version: 3.621.0 - resolution: "@aws-sdk/util-dynamodb@npm:3.621.0" +"@aws-sdk/util-dynamodb@npm:3.654.0, @aws-sdk/util-dynamodb@npm:^3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/util-dynamodb@npm:3.654.0" dependencies: tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-dynamodb": ^3.621.0 - checksum: 15853b30b00b69ee1b83e0bc38c615edc00c7605b8f229784ad6191a0eff87a10d97b7b0cf92432f10156e76ca3d1eee66be5ed3206d3a686740220a13f34b9c + "@aws-sdk/client-dynamodb": ^3.654.0 + checksum: 271be6a9a4d509db773218e09992a8f13bd04a18c0b02a336a5b2cadd6bc94eb5858345d220911249ebaac02401c23742a15eeb71499c6721ee30e734a0b7bde languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.614.0": - version: 3.614.0 - resolution: "@aws-sdk/util-endpoints@npm:3.614.0" +"@aws-sdk/util-endpoints@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/util-endpoints@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/types": ^3.3.0 - "@smithy/util-endpoints": ^2.0.5 + "@aws-sdk/types": 3.654.0 + "@smithy/types": ^3.4.2 + "@smithy/util-endpoints": ^2.1.2 tslib: ^2.6.2 - checksum: 9d9973ceee59bf30af85c7f4328083daea033a987ec396dcb89eb7649f470ceb19c6b96635e121f3557e726f7ec7453236c956cf43f22128883c277f17d2a13f + checksum: 6c5f03ca1f8b0ff6323789c1a5993a95b6e139d7dcff5118cc1ec65043b6e1a05bd33b45f5b101dbbc45f01cba78fab695bba23817315e12b48805458f520819 languageName: node linkType: hard -"@aws-sdk/util-format-url@npm:3.609.0": - version: 3.609.0 - resolution: "@aws-sdk/util-format-url@npm:3.609.0" +"@aws-sdk/util-format-url@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/util-format-url@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/querystring-builder": ^3.0.3 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/querystring-builder": ^3.0.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 487a666cc4f6a3a8bcd27839fda7b79a3dccc81a7bd694a9903c6ea3695e1cc7cb2e538421335db5fb59460a148cc1a4a0a6e4796c5241a0ab406c4190f781d9 + checksum: 619e84fc68028f7465c9970dafdd2446545e361ef0f6f3a6a43b2c0246f2428a372b47bd9f2afd5f5ad9f7ca9096f127ba8a5c01dc8a749801a7f0987f6ad42e languageName: node linkType: hard @@ -2243,15 +2236,15 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.609.0": - version: 3.609.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.609.0" +"@aws-sdk/util-user-agent-browser@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/types": ^3.4.2 bowser: ^2.11.0 tslib: ^2.6.2 - checksum: 75ba1ae74dd1001f47870766d92b66ac02a0a488efcf42c1a368962a7978a778d99536e880f07f7db1c2ca66cc9b1863fd3342957a22dcf78bf2f4398265a7a5 + checksum: 07ffed5a12187a936fc70e8fa1b0acb66105383134df1c2053b825d10a6a069fd353414b8092ed92fd722b37853a84d85b5fbed753158c5c9a2efc43fcfa7fb4 languageName: node linkType: hard @@ -2266,20 +2259,20 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.614.0": - version: 3.614.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.614.0" +"@aws-sdk/util-user-agent-node@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.654.0" dependencies: - "@aws-sdk/types": 3.609.0 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/types": ^3.3.0 + "@aws-sdk/types": 3.654.0 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: 1f010080c2301fd836908963a235ef39e597d959e27461d15d4958fa582ab20795022f8cb7429c183c386f558a5c125cb254a0c4e844dbc6422169f4884be34a + checksum: 8799307e6e110b0ab1660dd3c146fa7785958f54558f00924a361b4fe6b7d541a4671062b64187171c8c51b7c405771ca79fe5f00749769aa2b19b5545468dc6 languageName: node linkType: hard @@ -2311,13 +2304,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:3.609.0": - version: 3.609.0 - resolution: "@aws-sdk/xml-builder@npm:3.609.0" +"@aws-sdk/xml-builder@npm:3.654.0": + version: 3.654.0 + resolution: "@aws-sdk/xml-builder@npm:3.654.0" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 0e9c8b7786737ff50a6cf39f7ca9a758897c2db364718364b5dad45f50a33e65bd7801348fd033af60768a5be64b454c3a7e65222e13c70d145e8df6211ca33c + checksum: e929b5637bb46762492f16db04d94f2a403847e84c4a8971167159030e2859bc0af0cba24e08100f3b5d700935520fe5926268e9222728ec9296d3b1d6f19240 languageName: node linkType: hard @@ -2332,33 +2325,6 @@ __metadata: languageName: node linkType: hard -"@babel/cli@npm:^7.22.6": - version: 7.24.7 - resolution: "@babel/cli@npm:7.24.7" - dependencies: - "@jridgewell/trace-mapping": ^0.3.25 - "@nicolo-ribaudo/chokidar-2": 2.1.8-no-fsevents.3 - chokidar: ^3.4.0 - commander: ^6.2.0 - convert-source-map: ^2.0.0 - fs-readdir-recursive: ^1.1.0 - glob: ^7.2.0 - make-dir: ^2.1.0 - slash: ^2.0.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - dependenciesMeta: - "@nicolo-ribaudo/chokidar-2": - optional: true - chokidar: - optional: true - bin: - babel: ./bin/babel.js - babel-external-helpers: ./bin/babel-external-helpers.js - checksum: 40dfde8062de913dc5bb1c65a4d4e88ec2c438f16387c5552b1f8b0524f8af454c3b7bf12364ca0da8509c5edafdabc1527a939587678dc7825659c38d357c1d - languageName: node - linkType: hard - "@babel/cli@npm:^7.23.9": version: 7.24.1 resolution: "@babel/cli@npm:7.24.1" @@ -2471,13 +2437,6 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/compat-data@npm:7.24.7" - checksum: 1fc276825dd434fe044877367dfac84171328e75a8483a6976aa28bf833b32367e90ee6df25bdd97c287d1aa8019757adcccac9153de70b1932c0d243a978ae9 - languageName: node - linkType: hard - "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3": version: 7.20.12 resolution: "@babel/core@npm:7.20.12" @@ -2524,29 +2483,6 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.22.8": - version: 7.24.7 - resolution: "@babel/core@npm:7.24.7" - dependencies: - "@ampproject/remapping": ^2.2.0 - "@babel/code-frame": ^7.24.7 - "@babel/generator": ^7.24.7 - "@babel/helper-compilation-targets": ^7.24.7 - "@babel/helper-module-transforms": ^7.24.7 - "@babel/helpers": ^7.24.7 - "@babel/parser": ^7.24.7 - "@babel/template": ^7.24.7 - "@babel/traverse": ^7.24.7 - "@babel/types": ^7.24.7 - convert-source-map: ^2.0.0 - debug: ^4.1.0 - gensync: ^1.0.0-beta.2 - json5: ^2.2.3 - semver: ^6.3.1 - checksum: 017497e2a1b4683a885219eef7d2aee83c1c0cf353506b2e180b73540ec28841d8ef1ea1837fa69f8c561574b24ddd72f04764b27b87afedfe0a07299ccef24d - languageName: node - linkType: hard - "@babel/core@npm:^7.23.9, @babel/core@npm:^7.24.0": version: 7.24.3 resolution: "@babel/core@npm:7.24.3" @@ -2619,18 +2555,6 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/generator@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - "@jridgewell/gen-mapping": ^0.3.5 - "@jridgewell/trace-mapping": ^0.3.25 - jsesc: ^2.5.1 - checksum: 0ff31a73b15429f1287e4d57b439bba4a266f8c673bb445fe313b82f6d110f586776997eb723a777cd7adad9d340edd162aea4973a90112c5d0cfcaf6686844b - languageName: node - linkType: hard - "@babel/helper-annotate-as-pure@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-annotate-as-pure@npm:7.18.6" @@ -2649,15 +2573,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: 6178566099a6a0657db7a7fa601a54fb4731ca0b8614fbdccfd8e523c210c13963649bc8fdfd53ce7dd14d05e3dda2fb22dea5b30113c488b9eb1a906d60212e - languageName: node - linkType: hard - "@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.18.6": version: 7.18.9 resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.18.9" @@ -2677,16 +2592,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.24.7" - dependencies: - "@babel/traverse": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 71a6158a9fdebffb82fdc400d5555ba8f2e370cea81a0d578155877bdc4db7d5252b75c43b2fdf3f72b3f68348891f99bd35ae315542daad1b7ace8322b1abcb - languageName: node - linkType: hard - "@babel/helper-compilation-targets@npm:^7.17.7, @babel/helper-compilation-targets@npm:^7.18.9, @babel/helper-compilation-targets@npm:^7.20.0, @babel/helper-compilation-targets@npm:^7.20.7": version: 7.20.7 resolution: "@babel/helper-compilation-targets@npm:7.20.7" @@ -2745,19 +2650,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-compilation-targets@npm:7.24.7" - dependencies: - "@babel/compat-data": ^7.24.7 - "@babel/helper-validator-option": ^7.24.7 - browserslist: ^4.22.2 - lru-cache: ^5.1.1 - semver: ^6.3.1 - checksum: dfc88bc35e223ade796c7267901728217c665adc5bc2e158f7b0ae850de14f1b7941bec4fe5950ae46236023cfbdeddd9c747c276acf9b39ca31f8dd97dc6cc6 - languageName: node - linkType: hard - "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.20.12, @babel/helper-create-class-features-plugin@npm:^7.20.5, @babel/helper-create-class-features-plugin@npm:^7.20.7": version: 7.20.12 resolution: "@babel/helper-create-class-features-plugin@npm:7.20.12" @@ -2795,25 +2687,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-create-class-features-plugin@npm:7.24.7" - dependencies: - "@babel/helper-annotate-as-pure": ^7.24.7 - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-function-name": ^7.24.7 - "@babel/helper-member-expression-to-functions": ^7.24.7 - "@babel/helper-optimise-call-expression": ^7.24.7 - "@babel/helper-replace-supers": ^7.24.7 - "@babel/helper-skip-transparent-expression-wrappers": ^7.24.7 - "@babel/helper-split-export-declaration": ^7.24.7 - semver: ^6.3.1 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 371a181a1717a9b0cebc97727c8ea9ca6afa34029476a684b6030f9d1ad94dcdafd7de175da10b63ae3ba79e4e82404db8ed968ebf264b768f097e5d64faab71 - languageName: node - linkType: hard - "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.20.5": version: 7.20.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.20.5" @@ -2852,19 +2725,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.24.7" - dependencies: - "@babel/helper-annotate-as-pure": ^7.24.7 - regexpu-core: ^5.3.1 - semver: ^6.3.1 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 17c59fa222af50f643946eca940ce1d474ff2da1f4afed2312687ab9d708ebbb8c9372754ddbdf44b6e21ead88b8fc144644f3a7b63ccb886de002458cef3974 - languageName: node - linkType: hard - "@babel/helper-define-polyfill-provider@npm:^0.3.3": version: 0.3.3 resolution: "@babel/helper-define-polyfill-provider@npm:0.3.3" @@ -2917,15 +2777,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-environment-visitor@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: 079d86e65701b29ebc10baf6ed548d17c19b808a07aa6885cc141b690a78581b180ee92b580d755361dc3b16adf975b2d2058b8ce6c86675fcaf43cf22f2f7c6 - languageName: node - linkType: hard - "@babel/helper-explode-assignable-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-explode-assignable-expression@npm:7.18.6" @@ -2965,16 +2816,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-function-name@npm:7.24.7" - dependencies: - "@babel/template": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 142ee08922074dfdc0ff358e09ef9f07adf3671ab6eef4fca74dcf7a551f1a43717e7efa358c9e28d7eea84c28d7f177b7a58c70452fc312ae3b1893c5dab2a4 - languageName: node - linkType: hard - "@babel/helper-hoist-variables@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-hoist-variables@npm:7.18.6" @@ -2993,15 +2834,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-hoist-variables@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: 6cfdcf2289cd12185dcdbdf2435fa8d3447b797ac75851166de9fc8503e2fd0021db6baf8dfbecad3753e582c08e6a3f805c8d00cbed756060a877d705bd8d8d - languageName: node - linkType: hard - "@babel/helper-member-expression-to-functions@npm:^7.20.7": version: 7.20.7 resolution: "@babel/helper-member-expression-to-functions@npm:7.20.7" @@ -3020,16 +2852,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-member-expression-to-functions@npm:7.24.7" - dependencies: - "@babel/traverse": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 9fecf412f85fa23b7cf55d19eb69de39f8240426a028b141c9df2aed8cfedf20b3ec3318d40312eb7a3dec9eea792828ce0d590e0ff62da3da532482f537192c - languageName: node - linkType: hard - "@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-module-imports@npm:7.18.6" @@ -3075,16 +2897,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-module-imports@npm:7.24.7" - dependencies: - "@babel/traverse": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 8ac15d96d262b8940bc469052a048e06430bba1296369be695fabdf6799f201dd0b00151762b56012a218464e706bc033f27c07f6cec20c6f8f5fd6543c67054 - languageName: node - linkType: hard - "@babel/helper-module-transforms@npm:^7.18.6, @babel/helper-module-transforms@npm:^7.20.11": version: 7.20.11 resolution: "@babel/helper-module-transforms@npm:7.20.11" @@ -3132,21 +2944,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-module-transforms@npm:7.24.7" - dependencies: - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-module-imports": ^7.24.7 - "@babel/helper-simple-access": ^7.24.7 - "@babel/helper-split-export-declaration": ^7.24.7 - "@babel/helper-validator-identifier": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: ddff3b41c2667876b4e4e73d961168f48a5ec9560c95c8c2d109e6221f9ca36c6f90c6317eb7a47f2a3c99419c356e529a86b79174cad0d4f7a61960866b88ca - languageName: node - linkType: hard - "@babel/helper-optimise-call-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-optimise-call-expression@npm:7.18.6" @@ -3165,15 +2962,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-optimise-call-expression@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-optimise-call-expression@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: 280654eaf90e92bf383d7eed49019573fb35a98c9e992668f701ad099957246721044be2068cf6840cb2299e0ad393705a1981c88c23a1048096a8d59e5f79a3 - languageName: node - linkType: hard - "@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.16.7, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.20.2 resolution: "@babel/helper-plugin-utils@npm:7.20.2" @@ -3195,13 +2983,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-plugin-utils@npm:7.24.7" - checksum: 81f2a15751d892e4a8fce25390f973363a5b27596167861d2d6eab0f61856eb2ba389b031a9f19f669c0bd4dd601185828d3cebafd25431be7a1696f2ce3ef68 - languageName: node - linkType: hard - "@babel/helper-remap-async-to-generator@npm:^7.18.9": version: 7.18.9 resolution: "@babel/helper-remap-async-to-generator@npm:7.18.9" @@ -3229,19 +3010,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-remap-async-to-generator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-remap-async-to-generator@npm:7.24.7" - dependencies: - "@babel/helper-annotate-as-pure": ^7.24.7 - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-wrap-function": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: bab7be178f875350f22a2cb9248f67fe3a8a8128db77a25607096ca7599fd972bc7049fb11ed9e95b45a3f1dd1fac3846a3279f9cbac16f337ecb0e6ca76e1fc - languageName: node - linkType: hard - "@babel/helper-replace-supers@npm:^7.18.6, @babel/helper-replace-supers@npm:^7.20.7": version: 7.20.7 resolution: "@babel/helper-replace-supers@npm:7.20.7" @@ -3269,19 +3037,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-replace-supers@npm:7.24.7" - dependencies: - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-member-expression-to-functions": ^7.24.7 - "@babel/helper-optimise-call-expression": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 2bf0d113355c60d86a04e930812d36f5691f26c82d4ec1739e5ec0a4c982c9113dad3167f7c74f888a96328bd5e696372232406d8200e5979e6e0dc2af5e7c76 - languageName: node - linkType: hard - "@babel/helper-simple-access@npm:^7.20.2": version: 7.20.2 resolution: "@babel/helper-simple-access@npm:7.20.2" @@ -3309,16 +3064,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-simple-access@npm:7.24.7" - dependencies: - "@babel/traverse": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: ddbf55f9dea1900213f2a1a8500fabfd21c5a20f44dcfa957e4b0d8638c730f88751c77f678644f754f1a1dc73f4eb8b766c300deb45a9daad000e4247957819 - languageName: node - linkType: hard - "@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0": version: 7.20.0 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.20.0" @@ -3337,16 +3082,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.24.7" - dependencies: - "@babel/traverse": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 11b28fe534ce2b1a67c4d8e51a7b5711a2a0a0cae802f74614eee54cca58c744d9a62f6f60103c41759e81c537d270bfd665bf368a6bea214c6052f2094f8407 - languageName: node - linkType: hard - "@babel/helper-split-export-declaration@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-split-export-declaration@npm:7.18.6" @@ -3365,15 +3100,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-split-export-declaration@npm:7.24.7" - dependencies: - "@babel/types": ^7.24.7 - checksum: e3ddc91273e5da67c6953f4aa34154d005a00791dc7afa6f41894e768748540f6ebcac5d16e72541aea0c89bee4b89b4da6a3d65972a0ea8bfd2352eda5b7e22 - languageName: node - linkType: hard - "@babel/helper-string-parser@npm:^7.19.4": version: 7.19.4 resolution: "@babel/helper-string-parser@npm:7.19.4" @@ -3402,13 +3128,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-string-parser@npm:7.24.7" - checksum: 09568193044a578743dd44bf7397940c27ea693f9812d24acb700890636b376847a611cdd0393a928544e79d7ad5b8b916bd8e6e772bc8a10c48a647a96e7b1a - languageName: node - linkType: hard - "@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-validator-identifier@npm:7.19.1" @@ -3465,13 +3184,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-validator-option@npm:7.24.7" - checksum: 9689166bf3f777dd424c026841c8cd651e41b21242dbfd4569a53086179a3e744c8eddd56e9d10b54142270141c91581b53af0d7c00c82d552d2540e2a919f7e - languageName: node - linkType: hard - "@babel/helper-wrap-function@npm:^7.18.9": version: 7.20.5 resolution: "@babel/helper-wrap-function@npm:7.20.5" @@ -3495,18 +3207,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-wrap-function@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-wrap-function@npm:7.24.7" - dependencies: - "@babel/helper-function-name": ^7.24.7 - "@babel/template": ^7.24.7 - "@babel/traverse": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 085bf130ed08670336e3976f5841ae44e3e10001131632e22ef234659341978d2fd37e65785f59b6cb1745481347fc3bce84b33a685cacb0a297afbe1d2b03af - languageName: node - linkType: hard - "@babel/helpers@npm:^7.20.7": version: 7.20.13 resolution: "@babel/helpers@npm:7.20.13" @@ -3540,16 +3240,6 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helpers@npm:7.24.7" - dependencies: - "@babel/template": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: 934da58098a3670ca7f9f42425b9c44d0ca4f8fad815c0f51d89fc7b64c5e0b4c7d5fec038599de691229ada737edeaf72fad3eba8e16dd5842e8ea447f76b66 - languageName: node - linkType: hard - "@babel/highlight@npm:^7.18.6": version: 7.18.6 resolution: "@babel/highlight@npm:7.18.6" @@ -3652,27 +3342,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/parser@npm:7.24.7" - bin: - parser: ./bin/babel-parser.js - checksum: fc9d2c4c8712f89672edc55c0dc5cf640dcec715b56480f111f85c2bc1d507e251596e4110d65796690a96ac37a4b60432af90b3e97bb47e69d4ef83872dbbd6 - languageName: node - linkType: hard - -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.24.7" - dependencies: - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 68d315642b53af143aa17a71eb976cf431b51339aee584e29514a462b81c998636dd54219c2713b5f13e1df89eaf130dfab59683f9116825608708c81696b96c - languageName: node - linkType: hard - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.18.6" @@ -3695,17 +3364,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 7eb4e7ce5e3d6db4b0fdbdfaaa301c2e58f38a7ee39d5a4259a1fda61a612e83d3e4bc90fc36fb0345baf57e1e1a071e0caffeb80218623ad163f2fdc2e53a54 - languageName: node - linkType: hard - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.18.9": version: 7.20.7 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.20.7" @@ -3732,19 +3390,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-skip-transparent-expression-wrappers": ^7.24.7 - "@babel/plugin-transform-optional-chaining": ^7.24.7 - peerDependencies: - "@babel/core": ^7.13.0 - checksum: 07b92878ac58a98ea1fdf6a8b4ec3413ba4fa66924e28b694d63ec5b84463123fbf4d7153b56cf3cedfef4a3482c082fe3243c04f8fb2c041b32b0e29b4a9e21 - languageName: node - linkType: hard - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.24.1" @@ -3757,18 +3402,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.24.7" - dependencies: - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 8324d458db57060590942c7c2e9603880d07718ccb6450ec935105b8bd3c4393c4b8ada88e178c232258d91f33ffdcf2b1043d54e07a86989e50667ee100a32e - languageName: node - linkType: hard - "@babel/plugin-proposal-async-generator-functions@npm:^7.20.1": version: 7.20.7 resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.20.7" @@ -4090,17 +3723,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c4d67be4eb1d4637e361477dbe01f5b392b037d17c1f861cfa0faa120030e137aab90a9237931b8040fd31d1e5d159e11866fa1165f78beef7a3be876a391a17 - languageName: node - linkType: hard - "@babel/plugin-syntax-import-attributes@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.1" @@ -4112,17 +3734,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 590dbb5d1a15264f74670b427b8d18527672c3d6c91d7bae7e65f80fd810edbc83d90e68065088644cbad3f2457ed265a54a9956fb789fcb9a5b521822b3a275 - languageName: node - linkType: hard - "@babel/plugin-syntax-import-meta@npm:^7.10.4, @babel/plugin-syntax-import-meta@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" @@ -4344,17 +3955,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 707c209b5331c7dc79bd326128c6a6640dbd62a78da1653c844db20c4f36bf7b68454f1bc4d2d051b3fde9136fa291f276ec03a071bb00ee653069ff82f91010 - languageName: node - linkType: hard - "@babel/plugin-transform-async-generator-functions@npm:^7.24.3": version: 7.24.3 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.3" @@ -4369,20 +3969,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.24.7" - dependencies: - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-remap-async-to-generator": ^7.24.7 - "@babel/plugin-syntax-async-generators": ^7.8.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 112e3b18f9c496ebc01209fc27f0b41a3669c479c7bc44f7249383172b432ebaae1e523caa7c6ecbd2d0d7adcb7e5769fe2798f8cb01c08cd57232d1bb6d8ad4 - languageName: node - linkType: hard - "@babel/plugin-transform-async-to-generator@npm:^7.18.6": version: 7.20.7 resolution: "@babel/plugin-transform-async-to-generator@npm:7.20.7" @@ -4409,19 +3995,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.24.7" - dependencies: - "@babel/helper-module-imports": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-remap-async-to-generator": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 13704fb3b83effc868db2b71bfb2c77b895c56cb891954fc362e95e200afd523313b0e7cf04ce02f45b05e76017c5b5fa8070c92613727a35131bb542c253a36 - languageName: node - linkType: hard - "@babel/plugin-transform-block-scoped-functions@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.18.6" @@ -4444,17 +4017,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoped-functions@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 249cdcbff4e778b177245f9652b014ea4f3cd245d83297f10a7bf6d97790074089aa62bcde8c08eb299c5e68f2faed346b587d3ebac44d625ba9a83a4ee27028 - languageName: node - linkType: hard - "@babel/plugin-transform-block-scoping@npm:^7.20.2": version: 7.20.15 resolution: "@babel/plugin-transform-block-scoping@npm:7.20.15" @@ -4477,17 +4039,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-block-scoping@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 039206155533600f079f3a455f85888dd7d4970ff7ffa85ef44760f4f5acb9f19c9d848cc1fec1b9bdbc0dfec9e8a080b90d0ab66ad2bdc7138b5ca4ba96e61c - languageName: node - linkType: hard - "@babel/plugin-transform-class-properties@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-class-properties@npm:7.24.1" @@ -4500,18 +4051,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-class-properties@npm:7.24.7" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 1348d7ce74da38ba52ea85b3b4289a6a86913748569ef92ef0cff30702a9eb849e5eaf59f1c6f3517059aa68115fb3067e389735dccacca39add4e2b0c67e291 - languageName: node - linkType: hard - "@babel/plugin-transform-class-static-block@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-class-static-block@npm:7.24.1" @@ -4525,19 +4064,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-class-static-block@npm:7.24.7" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - peerDependencies: - "@babel/core": ^7.12.0 - checksum: 324049263504f18416f1c3e24033baebfafd05480fdd885c8ebe6f2b415b0fc8e0b98d719360f9e30743cc78ac387fabc0b3c6606d2b54135756ffb92963b382 - languageName: node - linkType: hard - "@babel/plugin-transform-classes@npm:^7.20.2": version: 7.20.7 resolution: "@babel/plugin-transform-classes@npm:7.20.7" @@ -4575,24 +4101,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-classes@npm:7.24.7" - dependencies: - "@babel/helper-annotate-as-pure": ^7.24.7 - "@babel/helper-compilation-targets": ^7.24.7 - "@babel/helper-environment-visitor": ^7.24.7 - "@babel/helper-function-name": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-replace-supers": ^7.24.7 - "@babel/helper-split-export-declaration": ^7.24.7 - globals: ^11.1.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f01cb31143730d425681e9816020cbb519c7ddb3b6ca308dfaf2821eda5699a746637fc6bf19811e2fb42cfdf8b00a21b31c754da83771a5c280077925677354 - languageName: node - linkType: hard - "@babel/plugin-transform-computed-properties@npm:^7.18.9": version: 7.20.7 resolution: "@babel/plugin-transform-computed-properties@npm:7.20.7" @@ -4617,18 +4125,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-computed-properties@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/template": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 0cf8c1b1e4ea57dec8d4612460d84fd4cdbf71a7499bb61ee34632cf89018a59eee818ffca88a8d99ee7057c20a4257044d7d463fda6daef9bf1db9fa81563cb - languageName: node - linkType: hard - "@babel/plugin-transform-destructuring@npm:^7.20.2": version: 7.20.7 resolution: "@babel/plugin-transform-destructuring@npm:7.20.7" @@ -4651,17 +4147,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-destructuring@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: b9637b27faf9d24a8119bc5a1f98a2f47c69e6441bd8fc71163500be316253a72173308a93122bcf27d8d314ace43344c976f7291cf6376767f408350c8149d4 - languageName: node - linkType: hard - "@babel/plugin-transform-dotall-regex@npm:^7.18.6, @babel/plugin-transform-dotall-regex@npm:^7.4.4": version: 7.18.6 resolution: "@babel/plugin-transform-dotall-regex@npm:7.18.6" @@ -4686,18 +4171,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-dotall-regex@npm:7.24.7" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 67b10fc6abb1f61f0e765288eb4c6d63d1d0f9fc0660e69f6f2170c56fa16bc74e49857afc644beda112b41771cd90cf52df0940d11e97e52617c77c7dcff171 - languageName: node - linkType: hard - "@babel/plugin-transform-duplicate-keys@npm:^7.18.9": version: 7.18.9 resolution: "@babel/plugin-transform-duplicate-keys@npm:7.18.9" @@ -4720,17 +4193,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-duplicate-keys@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-duplicate-keys@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: d1da2ff85ecb56a63f4ccfd9dc9ae69400d85f0dadf44ecddd9e71c6e5c7a9178e74e3a9637555f415a2bb14551e563f09f98534ab54f53d25e8439fdde6ba2d - languageName: node - linkType: hard - "@babel/plugin-transform-dynamic-import@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.1" @@ -4743,18 +4205,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 776509ff62ab40c12be814a342fc56a5cc09b91fb63032b2633414b635875fd7da03734657be0f6db2891fe6e3033b75d5ddb6f2baabd1a02e4443754a785002 - languageName: node - linkType: hard - "@babel/plugin-transform-exponentiation-operator@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.18.6" @@ -4779,18 +4229,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.24.7" - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 23c84a23eb56589fdd35a3540f9a1190615be069110a2270865223c03aee3ba4e0fc68fe14850800cf36f0712b26e4964d3026235261f58f0405a29fe8dac9b1 - languageName: node - linkType: hard - "@babel/plugin-transform-export-namespace-from@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" @@ -4803,18 +4241,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3bd3a10038f10ae0dea1ee42137f3edcf7036b5e9e570a0d1cbd0865f03658990c6c2d84fa2475f87a754e7dc5b46766c16f7ce5c9b32c3040150b6a21233a80 - languageName: node - linkType: hard - "@babel/plugin-transform-for-of@npm:^7.18.8": version: 7.18.8 resolution: "@babel/plugin-transform-for-of@npm:7.18.8" @@ -4838,18 +4264,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-for-of@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-skip-transparent-expression-wrappers": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a53b42dc93ab4b7d1ebd3c695b52be22b3d592f6a3dbdb3dc2fea2c8e0a7e1508fe919864c455cde552aec44ce7518625fccbb70c7063373ca228d884f4f49ea - languageName: node - linkType: hard - "@babel/plugin-transform-function-name@npm:^7.18.9": version: 7.18.9 resolution: "@babel/plugin-transform-function-name@npm:7.18.9" @@ -4876,19 +4290,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-function-name@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-function-name@npm:7.24.7" - dependencies: - "@babel/helper-compilation-targets": ^7.24.7 - "@babel/helper-function-name": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 8eb1a67894a124910b5a67630bed4307757504381f39f0fb5cf82afc7ae8647dbc03b256d13865b73a749b9071b68e9fb8a28cef2369917b4299ebb93fd66146 - languageName: node - linkType: hard - "@babel/plugin-transform-json-strings@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-json-strings@npm:7.24.1" @@ -4901,18 +4302,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-json-strings@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-json-strings": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 88874d0b7a1ddea66c097fc0abb68801ffae194468aa44b828dde9a0e20ac5d8647943793de86092eabaa2911c96f67a6b373793d4bb9c932ef81b2711c06c2e - languageName: node - linkType: hard - "@babel/plugin-transform-literals@npm:^7.18.9": version: 7.18.9 resolution: "@babel/plugin-transform-literals@npm:7.18.9" @@ -4935,17 +4324,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-literals@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-literals@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3c075cc093a3dd9e294b8b7d6656e65f889e7ca2179ca27978dcd65b4dc4885ebbfb327408d7d8f483c55547deed00ba840956196f3ac8a3c3d2308a330a8c23 - languageName: node - linkType: hard - "@babel/plugin-transform-logical-assignment-operators@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.24.1" @@ -4958,18 +4336,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3367ce0be243704dc6fce23e86a592c4380f01998ee5dd9f94c54b1ef7b971ac6f8a002901eb51599ac6cbdc0d067af8d1a720224fca1c40fde8bb8aab804aac - languageName: node - linkType: hard - "@babel/plugin-transform-member-expression-literals@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.18.6" @@ -4992,17 +4358,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-member-expression-literals@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 2720c57aa3bf70576146ba7d6ea03227f4611852122d76d237924f7b008dafc952e6ae61a19e5024f26c665f44384bbd378466f01b6bd1305b3564a3b7fb1a5d - languageName: node - linkType: hard - "@babel/plugin-transform-modules-amd@npm:^7.19.6": version: 7.20.11 resolution: "@babel/plugin-transform-modules-amd@npm:7.20.11" @@ -5027,18 +4382,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-amd@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-modules-amd@npm:7.24.7" - dependencies: - "@babel/helper-module-transforms": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f1dd0fb2f46c0f8f21076b8c7ccd5b33a85ce6dcb31518ea4c648d9a5bb2474cd4bd87c9b1b752e68591e24b022e334ba0d07631fef2b6b4d8a4b85cf3d581f5 - languageName: node - linkType: hard - "@babel/plugin-transform-modules-commonjs@npm:^7.19.6": version: 7.20.11 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.20.11" @@ -5065,19 +4408,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.7" - dependencies: - "@babel/helper-module-transforms": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-simple-access": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: bfda2a0297197ed342e2a02e5f9847a489a3ae40a4a7d7f00f4aeb8544a85e9006e0c5271c8f61f39bc97975ef2717b5594cf9486694377a53433162909d64c1 - languageName: node - linkType: hard - "@babel/plugin-transform-modules-systemjs@npm:^7.19.6": version: 7.20.11 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.20.11" @@ -5106,20 +4436,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.24.7" - dependencies: - "@babel/helper-hoist-variables": ^7.24.7 - "@babel/helper-module-transforms": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-validator-identifier": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 8af7a9db2929991d82cfdf41fb175dee344274d39b39122f8c35f24b5d682f98368e3d8f5130401298bd21412df21d416a7d8b33b59c334fae3d3c762118b1d8 - languageName: node - linkType: hard - "@babel/plugin-transform-modules-umd@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-modules-umd@npm:7.18.6" @@ -5144,18 +4460,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-umd@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-modules-umd@npm:7.24.7" - dependencies: - "@babel/helper-module-transforms": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 9ff1c464892efe042952ba778468bda6131b196a2729615bdcc3f24cdc94014f016a4616ee5643c5845bade6ba698f386833e61056d7201314b13a7fd69fac88 - languageName: node - linkType: hard - "@babel/plugin-transform-named-capturing-groups-regex@npm:^7.19.1": version: 7.20.5 resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.20.5" @@ -5180,18 +4484,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.24.7" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: f1c6c7b5d60a86b6d7e4dd098798e1d393d55e993a0b57a73b53640c7a94985b601a96bdacee063f809a9a700bcea3a2ff18e98fa561554484ac56b761d774bd - languageName: node - linkType: hard - "@babel/plugin-transform-new-target@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-new-target@npm:7.18.6" @@ -5214,17 +4506,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-new-target@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-new-target@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3cb94cd1076b270f768f91fdcf9dd2f6d487f8dbfff3df7ca8d07b915900b86d02769a35ba1407d16fe49499012c8f055e1741299e2c880798b953d942a8fa1b - languageName: node - linkType: hard - "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.1" @@ -5237,18 +4518,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 4a9221356401d87762afbc37a9e8e764afc2daf09c421117537820f8cfbed6876888372ad3a7bcfae2d45c95f026651f050ab4020b777be31d3ffb00908dbdd3 - languageName: node - linkType: hard - "@babel/plugin-transform-numeric-separator@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-numeric-separator@npm:7.24.1" @@ -5261,18 +4530,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 561b5f1d08b2c3f92ce849f092751558b5e6cfeb7eb55c79e7375c34dd9c3066dce5e630bb439affef6adcf202b6cbcaaa23870070276fa5bb429c8f5b8c7514 - languageName: node - linkType: hard - "@babel/plugin-transform-object-rest-spread@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.1" @@ -5287,20 +4544,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.7" - dependencies: - "@babel/helper-compilation-targets": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-transform-parameters": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 169d257b9800c13e1feb4c37fb05dae84f702e58b342bb76e19e82e6692b7b5337c9923ee89e3916a97c0dd04a3375bdeca14f5e126f110bbacbeb46d1886ca2 - languageName: node - linkType: hard - "@babel/plugin-transform-object-super@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-object-super@npm:7.18.6" @@ -5325,18 +4568,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-object-super@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-object-super@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-replace-supers": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f71e607a830ee50a22fa1a2686524d3339440cf9dea63032f6efbd865cfe4e35000e1e3f3492459e5c986f7c0c07dc36938bf3ce61fc9ba5f8ab732d0b64ab37 - languageName: node - linkType: hard - "@babel/plugin-transform-optional-catch-binding@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.1" @@ -5349,18 +4580,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7229f3a5a4facaab40f4fdfc7faabc157dc38a67d66bed7936599f4bc509e0bff636f847ac2aa45294881fce9cf8a0a460b85d2a465b7b977de9739fce9b18f6 - languageName: node - linkType: hard - "@babel/plugin-transform-optional-chaining@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.1" @@ -5374,19 +4593,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-skip-transparent-expression-wrappers": ^7.24.7 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 877e7ce9097d475132c7f4d1244de50bb2fd37993dc4580c735f18f8cbc49282f6e77752821bcad5ca9d3528412d2c8a7ee0aa7ca71bb680ff82648e7a5fed25 - languageName: node - linkType: hard - "@babel/plugin-transform-parameters@npm:^7.20.1, @babel/plugin-transform-parameters@npm:^7.20.7": version: 7.20.7 resolution: "@babel/plugin-transform-parameters@npm:7.20.7" @@ -5409,17 +4615,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-parameters@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ab534b03ac2eff94bc79342b8f39a4584666f5305a6c63c1964afda0b1b004e6b861e49d1683548030defe248e3590d3ff6338ee0552cb90c064f7e1479968c3 - languageName: node - linkType: hard - "@babel/plugin-transform-private-methods@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-methods@npm:7.24.1" @@ -5432,18 +4627,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-private-methods@npm:7.24.7" - dependencies: - "@babel/helper-create-class-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: c151548e34909be2adcceb224d8fdd70bafa393bc1559a600906f3f647317575bf40db670470934a360e90ee8084ef36dffa34ec25d387d414afd841e74cf3fe - languageName: node - linkType: hard - "@babel/plugin-transform-private-property-in-object@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.24.1" @@ -5458,20 +4641,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.24.7" - dependencies: - "@babel/helper-annotate-as-pure": ^7.24.7 - "@babel/helper-create-class-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 8cee9473095305cc787bb653fd681719b49363281feabf677db8a552e8e41c94441408055d7e5fd5c7d41b315e634fa70b145ad0c7c54456216049df4ed57350 - languageName: node - linkType: hard - "@babel/plugin-transform-property-literals@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-property-literals@npm:7.18.6" @@ -5494,17 +4663,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-property-literals@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-property-literals@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 9aeefc3aab6c6bf9d1fae1cf3a2d38c7d886fd3c6c81b7c608c477f5758aee2e7abf52f32724310fe861da61af934ee2508b78a5b5f234b9740c9134e1c14437 - languageName: node - linkType: hard - "@babel/plugin-transform-react-constant-elements@npm:^7.18.12": version: 7.21.3 resolution: "@babel/plugin-transform-react-constant-elements@npm:7.21.3" @@ -5653,18 +4811,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-regenerator@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - regenerator-transform: ^0.15.2 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 20c6c3fb6fc9f407829087316653388d311e8c1816b007609bb09aeef254092a7157adace8b3aaa8f34be752503717cb85c88a5fe482180a9b11bcbd676063be - languageName: node - linkType: hard - "@babel/plugin-transform-reserved-words@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-reserved-words@npm:7.18.6" @@ -5687,17 +4833,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-reserved-words@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-reserved-words@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3d5876954d5914d7270819479504f30c4bf5452a65c677f44e2dab2db50b3c9d4b47793c45dfad7abf4f377035dd79e4b3f554ae350df9f422201d370ce9f8dd - languageName: node - linkType: hard - "@babel/plugin-transform-runtime@npm:^7.24.0": version: 7.24.3 resolution: "@babel/plugin-transform-runtime@npm:7.24.3" @@ -5736,17 +4871,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 7b524245814607188212b8eb86d8c850e5974203328455a30881b4a92c364b93353fae14bc2af5b614ef16300b75b8c1d3b8f3a08355985b4794a7feb240adc3 - languageName: node - linkType: hard - "@babel/plugin-transform-spread@npm:^7.19.0": version: 7.20.7 resolution: "@babel/plugin-transform-spread@npm:7.20.7" @@ -5771,18 +4895,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-spread@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-skip-transparent-expression-wrappers": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 4c4254c8b9cceb1a8f975fa9b92257ddb08380a35c0a3721b8f4b9e13a3d82e403af2e0fba577b9f2452dd8f06bc3dea71cc53b1e2c6af595af5db52a13429d6 - languageName: node - linkType: hard - "@babel/plugin-transform-sticky-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-sticky-regex@npm:7.18.6" @@ -5805,17 +4917,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-sticky-regex@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-sticky-regex@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 118fc7a7ebf7c20411b670c8a030535fdfe4a88bc5643bb625a584dbc4c8a468da46430a20e6bf78914246962b0f18f1b9d6a62561a7762c4f34a038a5a77179 - languageName: node - linkType: hard - "@babel/plugin-transform-template-literals@npm:^7.18.9": version: 7.18.9 resolution: "@babel/plugin-transform-template-literals@npm:7.18.9" @@ -5838,17 +4939,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-template-literals@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: ad44e5826f5a98c1575832dbdbd033adfe683cdff195e178528ead62507564bf02f479b282976cfd3caebad8b06d5fd7349c1cdb880dec3c56daea4f1f179619 - languageName: node - linkType: hard - "@babel/plugin-transform-typeof-symbol@npm:^7.18.9": version: 7.18.9 resolution: "@babel/plugin-transform-typeof-symbol@npm:7.18.9" @@ -5871,17 +4961,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typeof-symbol@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-typeof-symbol@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 6bd16b9347614d44187d8f8ee23ebd7be30dabf3632eed5ff0415f35a482e827de220527089eae9cdfb75e85aa72db0e141ebc2247c4b1187c1abcdacdc34895 - languageName: node - linkType: hard - "@babel/plugin-transform-typescript@npm:^7.18.6": version: 7.20.13 resolution: "@babel/plugin-transform-typescript@npm:7.20.13" @@ -5931,17 +5010,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-escapes@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-unicode-escapes@npm:7.24.7" - dependencies: - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 4af0a193e1ddea6ff82b2b15cc2501b872728050bd625740b813c8062fec917d32d530ff6b41de56c15e7296becdf3336a58db81f5ca8e7c445c1306c52f3e01 - languageName: node - linkType: hard - "@babel/plugin-transform-unicode-property-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.24.1" @@ -5954,18 +5022,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.24.7" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: aae13350c50973f5802ca7906d022a6a0cc0e3aebac9122d0450bbd51e78252d4c2032ad69385e2759fcbdd3aac5d571bd7e26258907f51f8e1a51b53be626c2 - languageName: node - linkType: hard - "@babel/plugin-transform-unicode-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-transform-unicode-regex@npm:7.18.6" @@ -5990,18 +5046,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-unicode-regex@npm:7.24.7" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 1cb4e70678906e431da0a05ac3f8350025fee290304ad7482d9cfaa1ca67b2e898654de537c9268efbdad5b80d3ebadf42b4a88ea84609bd8a4cce7b11b48afd - languageName: node - linkType: hard - "@babel/plugin-transform-unicode-sets-regex@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.24.1" @@ -6014,18 +5058,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-sets-regex@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.24.7" - dependencies: - "@babel/helper-create-regexp-features-plugin": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 08a2844914f33dacd2ce1ab021ce8c1cc35dc6568521a746d8bf29c21571ee5be78787b454231c4bb3526cbbe280f1893223c82726cec5df2be5dae0a3b51837 - languageName: node - linkType: hard - "@babel/polyfill@npm:^7.10.1, @babel/polyfill@npm:^7.4.4": version: 7.12.1 resolution: "@babel/polyfill@npm:7.12.1" @@ -6121,97 +5153,6 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.22.7": - version: 7.24.7 - resolution: "@babel/preset-env@npm:7.24.7" - dependencies: - "@babel/compat-data": ^7.24.7 - "@babel/helper-compilation-targets": ^7.24.7 - "@babel/helper-plugin-utils": ^7.24.7 - "@babel/helper-validator-option": ^7.24.7 - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ^7.24.7 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.24.7 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.24.7 - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ^7.24.7 - "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2 - "@babel/plugin-syntax-async-generators": ^7.8.4 - "@babel/plugin-syntax-class-properties": ^7.12.13 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - "@babel/plugin-syntax-import-assertions": ^7.24.7 - "@babel/plugin-syntax-import-attributes": ^7.24.7 - "@babel/plugin-syntax-import-meta": ^7.10.4 - "@babel/plugin-syntax-json-strings": ^7.8.3 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - "@babel/plugin-syntax-top-level-await": ^7.14.5 - "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6 - "@babel/plugin-transform-arrow-functions": ^7.24.7 - "@babel/plugin-transform-async-generator-functions": ^7.24.7 - "@babel/plugin-transform-async-to-generator": ^7.24.7 - "@babel/plugin-transform-block-scoped-functions": ^7.24.7 - "@babel/plugin-transform-block-scoping": ^7.24.7 - "@babel/plugin-transform-class-properties": ^7.24.7 - "@babel/plugin-transform-class-static-block": ^7.24.7 - "@babel/plugin-transform-classes": ^7.24.7 - "@babel/plugin-transform-computed-properties": ^7.24.7 - "@babel/plugin-transform-destructuring": ^7.24.7 - "@babel/plugin-transform-dotall-regex": ^7.24.7 - "@babel/plugin-transform-duplicate-keys": ^7.24.7 - "@babel/plugin-transform-dynamic-import": ^7.24.7 - "@babel/plugin-transform-exponentiation-operator": ^7.24.7 - "@babel/plugin-transform-export-namespace-from": ^7.24.7 - "@babel/plugin-transform-for-of": ^7.24.7 - "@babel/plugin-transform-function-name": ^7.24.7 - "@babel/plugin-transform-json-strings": ^7.24.7 - "@babel/plugin-transform-literals": ^7.24.7 - "@babel/plugin-transform-logical-assignment-operators": ^7.24.7 - "@babel/plugin-transform-member-expression-literals": ^7.24.7 - "@babel/plugin-transform-modules-amd": ^7.24.7 - "@babel/plugin-transform-modules-commonjs": ^7.24.7 - "@babel/plugin-transform-modules-systemjs": ^7.24.7 - "@babel/plugin-transform-modules-umd": ^7.24.7 - "@babel/plugin-transform-named-capturing-groups-regex": ^7.24.7 - "@babel/plugin-transform-new-target": ^7.24.7 - "@babel/plugin-transform-nullish-coalescing-operator": ^7.24.7 - "@babel/plugin-transform-numeric-separator": ^7.24.7 - "@babel/plugin-transform-object-rest-spread": ^7.24.7 - "@babel/plugin-transform-object-super": ^7.24.7 - "@babel/plugin-transform-optional-catch-binding": ^7.24.7 - "@babel/plugin-transform-optional-chaining": ^7.24.7 - "@babel/plugin-transform-parameters": ^7.24.7 - "@babel/plugin-transform-private-methods": ^7.24.7 - "@babel/plugin-transform-private-property-in-object": ^7.24.7 - "@babel/plugin-transform-property-literals": ^7.24.7 - "@babel/plugin-transform-regenerator": ^7.24.7 - "@babel/plugin-transform-reserved-words": ^7.24.7 - "@babel/plugin-transform-shorthand-properties": ^7.24.7 - "@babel/plugin-transform-spread": ^7.24.7 - "@babel/plugin-transform-sticky-regex": ^7.24.7 - "@babel/plugin-transform-template-literals": ^7.24.7 - "@babel/plugin-transform-typeof-symbol": ^7.24.7 - "@babel/plugin-transform-unicode-escapes": ^7.24.7 - "@babel/plugin-transform-unicode-property-regex": ^7.24.7 - "@babel/plugin-transform-unicode-regex": ^7.24.7 - "@babel/plugin-transform-unicode-sets-regex": ^7.24.7 - "@babel/preset-modules": 0.1.6-no-external-plugins - babel-plugin-polyfill-corejs2: ^0.4.10 - babel-plugin-polyfill-corejs3: ^0.10.4 - babel-plugin-polyfill-regenerator: ^0.6.1 - core-js-compat: ^3.31.0 - semver: ^6.3.1 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 1a82c883c7404359b19b7436d0aab05f8dd4e89e8b1f7de127cc65d0ff6a9b1c345211d9c038f5b6e8f93d26f091fa9c73812d82851026ab4ec93f5ed0f0d675 - languageName: node - linkType: hard - "@babel/preset-env@npm:^7.24.0": version: 7.24.3 resolution: "@babel/preset-env@npm:7.24.3" @@ -6520,17 +5461,6 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/template@npm:7.24.7" - dependencies: - "@babel/code-frame": ^7.24.7 - "@babel/parser": ^7.24.7 - "@babel/types": ^7.24.7 - checksum: ea90792fae708ddf1632e54c25fe1a86643d8c0132311f81265d2bdbdd42f9f4fac65457056c1b6ca87f7aa0d6a795b549566774bba064bdcea2034ab3960ee9 - languageName: node - linkType: hard - "@babel/traverse@npm:^7.24.0": version: 7.24.1 resolution: "@babel/traverse@npm:7.24.1" @@ -6604,17 +5534,6 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/types@npm:7.24.7" - dependencies: - "@babel/helper-string-parser": ^7.24.7 - "@babel/helper-validator-identifier": ^7.24.7 - to-fast-properties: ^2.0.0 - checksum: 3e4437fced97e02982972ce5bebd318c47d42c9be2152c0fd28c6f786cc74086cc0a8fb83b602b846e41df37f22c36254338eada1a47ef9d8a1ec92332ca3ea8 - languageName: node - linkType: hard - "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -11857,6 +10776,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/commons@npm:^2.0.0": + version: 2.0.0 + resolution: "@sinonjs/commons@npm:2.0.0" + dependencies: + type-detect: 4.0.8 + checksum: 5023ba17edf2b85ed58262313b8e9b59e23c6860681a9af0200f239fe939e2b79736d04a260e8270ddd57196851dde3ba754d7230be5c5234e777ae2ca8af137 + languageName: node + linkType: hard + "@sinonjs/commons@npm:^3.0.0": version: 3.0.0 resolution: "@sinonjs/commons@npm:3.0.0" @@ -11866,6 +10794,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/commons@npm:^3.0.1": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: 4.0.8 + checksum: a7c3e7cc612352f4004873747d9d8b2d4d90b13a6d483f685598c945a70e734e255f1ca5dc49702515533c403b32725defff148177453b3f3915bcb60e9d4601 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^10.0.2": version: 10.2.0 resolution: "@sinonjs/fake-timers@npm:10.2.0" @@ -11875,6 +10812,24 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^10.3.0": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": ^3.0.0 + checksum: 614d30cb4d5201550c940945d44c9e0b6d64a888ff2cd5b357f95ad6721070d6b8839cd10e15b76bf5e14af0bcc1d8f9ec00d49a46318f1f669a4bec1d7f3148 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^11.2.2": + version: 11.3.1 + resolution: "@sinonjs/fake-timers@npm:11.3.1" + dependencies: + "@sinonjs/commons": ^3.0.1 + checksum: 173376bb02e870467705829b003c996bcac958f34238875458961ac6483c6029cd9623950d20c68b648499635a0e6d04c26aac822e4f5c120cc7c217aeba6553 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^6.0.0, @sinonjs/fake-timers@npm:^6.0.1": version: 6.0.1 resolution: "@sinonjs/fake-timers@npm:6.0.1" @@ -11895,6 +10850,17 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^8.0.0": + version: 8.0.0 + resolution: "@sinonjs/samsam@npm:8.0.0" + dependencies: + "@sinonjs/commons": ^2.0.0 + lodash.get: ^4.4.2 + type-detect: ^4.0.8 + checksum: 95e40d0bb9f7288e27c379bee1b03c3dc51e7e78b9d5ea6aef66a690da7e81efc4715145b561b449cefc5361a171791e3ce30fb1a46ab247d4c0766024c60a60 + languageName: node + linkType: hard + "@sinonjs/text-encoding@npm:^0.7.1": version: 0.7.2 resolution: "@sinonjs/text-encoding@npm:0.7.2" @@ -11902,6 +10868,13 @@ __metadata: languageName: node linkType: hard +"@sinonjs/text-encoding@npm:^0.7.2": + version: 0.7.3 + resolution: "@sinonjs/text-encoding@npm:0.7.3" + checksum: d53f3a3fc94d872b171f7f0725662f4d863e32bca8b44631be4fe67708f13058925ad7297524f882ea232144d7ab978c7fe62c5f79218fca7544cf91be3d233d + languageName: node + linkType: hard + "@smithy/abort-controller@npm:^2.0.10": version: 2.0.10 resolution: "@smithy/abort-controller@npm:2.0.10" @@ -11912,13 +10885,13 @@ __metadata: languageName: node linkType: hard -"@smithy/abort-controller@npm:^3.1.1": - version: 3.1.1 - resolution: "@smithy/abort-controller@npm:3.1.1" +"@smithy/abort-controller@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/abort-controller@npm:3.1.4" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 7b7497f49d58787cad858f8c5ea9931ccd44d39536db4abdd531a5abf37784469522e41d9ad1d541892caa0ed3bea750447809a0a18f4689a9543d672aa61d48 + checksum: 7fbf773a29ec160b6d230d95454f904a84c263e33421a7fb094abd2e04ef6d7286a1d938388eac01de0ba6085ef0770191b2ab776e024073e5eddf963c7ec65a languageName: node linkType: hard @@ -11941,158 +10914,160 @@ __metadata: languageName: node linkType: hard -"@smithy/config-resolver@npm:^3.0.5": - version: 3.0.5 - resolution: "@smithy/config-resolver@npm:3.0.5" +"@smithy/config-resolver@npm:^3.0.8": + version: 3.0.8 + resolution: "@smithy/config-resolver@npm:3.0.8" dependencies: - "@smithy/node-config-provider": ^3.1.4 - "@smithy/types": ^3.3.0 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/types": ^3.4.2 "@smithy/util-config-provider": ^3.0.0 - "@smithy/util-middleware": ^3.0.3 + "@smithy/util-middleware": ^3.0.6 tslib: ^2.6.2 - checksum: 96895ae0622a229655fa08f009d29a20157043020125014e84cb5ca33a10171c9724c309491214c2422d9c4c6681e7f5ec5f7faa8f45e11250449cf07f3552ec + checksum: 23571e36a04ac1369f96401f8f88e0bf0867bd31899370168502c084342da3aa4604c6edc09e252599cb7b4cbefc2b731ee40025cf3ba7c4583a3d5fefd71b40 languageName: node linkType: hard -"@smithy/core@npm:^2.3.1": - version: 2.3.1 - resolution: "@smithy/core@npm:2.3.1" - dependencies: - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-retry": ^3.0.13 - "@smithy/middleware-serde": ^3.0.3 - "@smithy/protocol-http": ^4.1.0 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/util-middleware": ^3.0.3 +"@smithy/core@npm:^2.4.3": + version: 2.4.3 + resolution: "@smithy/core@npm:2.4.3" + dependencies: + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-retry": ^3.0.18 + "@smithy/middleware-serde": ^3.0.6 + "@smithy/protocol-http": ^4.1.3 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: f191a36742a08c6898f4aaa43335bfb96cdca9a9099c0608fb0ef151917816b03ee1b27ef3720d89fba90d2d24fedfb47478f837d47c1c2e5ff8ceeb844ec7d6 + checksum: 7f7c8857632b44b8e8d6b84a944d976c678b69e1b01de9e0b6da6a468da502ad73cb01a75f6164c481a94439314217ba818ec981cc15912b3390e2ba5f9fb6d5 languageName: node linkType: hard -"@smithy/credential-provider-imds@npm:^3.2.0": - version: 3.2.0 - resolution: "@smithy/credential-provider-imds@npm:3.2.0" +"@smithy/credential-provider-imds@npm:^3.2.3": + version: 3.2.3 + resolution: "@smithy/credential-provider-imds@npm:3.2.3" dependencies: - "@smithy/node-config-provider": ^3.1.4 - "@smithy/property-provider": ^3.1.3 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/property-provider": ^3.1.6 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 tslib: ^2.6.2 - checksum: fc79919133008db91a83f2caf6eba11d704f34af5fa3dd1f7b8cc048214e805d9f20a3f302f5ed4862ee6b6c3bb28ff3a5d8c77d2f497d10f3be14915e59debe + checksum: 23aff4f9f671fe5a25c911a98d66489f6ea27cb4a39c3cab3d1d20bb85b50200a18e5a1923983f03b5c9ac551ed402002a1348016ed28def2185a1e4ac6c311e languageName: node linkType: hard -"@smithy/eventstream-codec@npm:^3.1.2": - version: 3.1.2 - resolution: "@smithy/eventstream-codec@npm:3.1.2" +"@smithy/eventstream-codec@npm:^3.1.5": + version: 3.1.5 + resolution: "@smithy/eventstream-codec@npm:3.1.5" dependencies: "@aws-crypto/crc32": 5.2.0 - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 "@smithy/util-hex-encoding": ^3.0.0 tslib: ^2.6.2 - checksum: b0c836acbf59b57a7e2ef948a54bd441d11b75d70f1c334723c27fce1ab0ff93ea9f936976b754272b5e90413b5a169c60b1df7ecfd7d061ebaae8d5cc067d94 + checksum: da5dbda693a53f4003c0e8b33abd1b1b59b6fdd2e8e0dec8e9ce48dcba764cfdd0715a4a42f094179d3850d733d0bb74c503601dfa400d767bddd227978bd8ed languageName: node linkType: hard -"@smithy/eventstream-serde-browser@npm:^3.0.5": - version: 3.0.5 - resolution: "@smithy/eventstream-serde-browser@npm:3.0.5" +"@smithy/eventstream-serde-browser@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/eventstream-serde-browser@npm:3.0.9" dependencies: - "@smithy/eventstream-serde-universal": ^3.0.4 - "@smithy/types": ^3.3.0 + "@smithy/eventstream-serde-universal": ^3.0.8 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 14e8a2027745e7a1ad261068e792e4a660043ce53fefc5f564b38b841ba02d40992b38fbd2357e762f0a1ecb658df3bbf23cf5ef33c3ec2488d316be95b61b9e + checksum: 50a68586fc00232da9f0c4028b7a8a504d7db87597e3c59d3a484b4e8b8416b8d6fc92dcf881d0ff0107f63c9e46d4cad877c97c5cac4195bedb58c60dce4f0d languageName: node linkType: hard -"@smithy/eventstream-serde-config-resolver@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/eventstream-serde-config-resolver@npm:3.0.3" +"@smithy/eventstream-serde-config-resolver@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/eventstream-serde-config-resolver@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: c61780aa0ad8c479618d0b3fcb2b42f1f9a74dcf814dba08305107ed1f088f56aa1c346db9c72439ff18617f31b9c59c6895060e4c9765c81d759150a22674af + checksum: b6c26fa6afc0679a6b7c64f22ccfcf4af2fd1dd17f18cf1e76878675438cedeca451532af53ff9585140727b514633c3852e0e72e4467657f6cdb7f3939c3844 languageName: node linkType: hard -"@smithy/eventstream-serde-node@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/eventstream-serde-node@npm:3.0.4" +"@smithy/eventstream-serde-node@npm:^3.0.8": + version: 3.0.8 + resolution: "@smithy/eventstream-serde-node@npm:3.0.8" dependencies: - "@smithy/eventstream-serde-universal": ^3.0.4 - "@smithy/types": ^3.3.0 + "@smithy/eventstream-serde-universal": ^3.0.8 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 0a75b184d95ab8c08efd93bf32c5fd9d735b5879df556599bd2ab78f23e3f77452e597bbdd42586c9bbedcc2b0b7683de4c816db739c19a2ebd62a34096ca86d + checksum: fdc7a2ebb59c913b3ae01a71f4d0ace882b660f4b27c3870c341535aba5f4e0d96f2f1e651947a1f955c8152fbf0e84e0baadd642e9313370fc29cd13c1bf670 languageName: node linkType: hard -"@smithy/eventstream-serde-universal@npm:^3.0.4": - version: 3.0.4 - resolution: "@smithy/eventstream-serde-universal@npm:3.0.4" +"@smithy/eventstream-serde-universal@npm:^3.0.8": + version: 3.0.8 + resolution: "@smithy/eventstream-serde-universal@npm:3.0.8" dependencies: - "@smithy/eventstream-codec": ^3.1.2 - "@smithy/types": ^3.3.0 + "@smithy/eventstream-codec": ^3.1.5 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 8463403ca4caf4ad48dba89b126f394439a289c9095ce6361c1f186c6021c1cd8ea402d1ce06b7284069c3415091ae4d802f66ded1b89e9da9d4c255b8402668 + checksum: 17d804e0ff80a15995bbf29b1e9ac8d8861991de55638ac7f0c4e47ad3a799b8be26ba314b401629e777d88418d396ae89dc8cca591ff511a233a47932a4e12a languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^3.2.4": - version: 3.2.4 - resolution: "@smithy/fetch-http-handler@npm:3.2.4" +"@smithy/fetch-http-handler@npm:^3.2.7": + version: 3.2.7 + resolution: "@smithy/fetch-http-handler@npm:3.2.7" dependencies: - "@smithy/protocol-http": ^4.1.0 - "@smithy/querystring-builder": ^3.0.3 - "@smithy/types": ^3.3.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/querystring-builder": ^3.0.6 + "@smithy/types": ^3.4.2 "@smithy/util-base64": ^3.0.0 tslib: ^2.6.2 - checksum: 73df885c637c14353f449678a4e109aeb19945c5370a615793ca2a54a29746a78e725e324b01cfd86fc71f4afd6386da2758fccc49d247a623ecbe70f607cb74 + checksum: b54e9e60a4b73d768f8afae8bd327e4b2d05f9042b1dc2b52acaf7e3202a77127737693ff961795c106bc8c707ecf466a85d7fb99c8cb9ce3e0315651dd59cc8 languageName: node linkType: hard -"@smithy/hash-blob-browser@npm:^3.1.2": - version: 3.1.2 - resolution: "@smithy/hash-blob-browser@npm:3.1.2" +"@smithy/hash-blob-browser@npm:^3.1.5": + version: 3.1.5 + resolution: "@smithy/hash-blob-browser@npm:3.1.5" dependencies: "@smithy/chunked-blob-reader": ^3.0.0 "@smithy/chunked-blob-reader-native": ^3.0.0 - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 959ec975cd4b3d86e3d0288e24b460343795bc305ef38fc43f8485cd1440da4068d375c5d1dab73ae875f02e861f194512a7adf5afcd7395bbeb97897d8a809b + checksum: ddeeff9afd84a1cd61af220465d2e1ac895bee4468912288ebb3ab7bf0cdc578f6d05e032e39dbdc5d721427c2a18f205fe57d5f00d4ddb1843cd8b0ca017a2a languageName: node linkType: hard -"@smithy/hash-node@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/hash-node@npm:3.0.3" +"@smithy/hash-node@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/hash-node@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 "@smithy/util-buffer-from": ^3.0.0 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 203a3581bec5373e63d42e03f62129022f03d17390e9358a4e25fc1d44c43962ea80ab5bcbb91605e3025e22136bed059665a3b16835f66316f43ed391df9548 + checksum: afd8335df075237f2e92c1b1da05eaa85cac6f08d0b6532aeba6c00e629d0ac089b10ca26ad89993310379f2602068bf147ae8708f4bab9d02aebaa9d3b612bd languageName: node linkType: hard -"@smithy/hash-stream-node@npm:^3.1.2": - version: 3.1.2 - resolution: "@smithy/hash-stream-node@npm:3.1.2" +"@smithy/hash-stream-node@npm:^3.1.5": + version: 3.1.5 + resolution: "@smithy/hash-stream-node@npm:3.1.5" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: e5284ef06548e301aa50bd06fe06bf3e2ed11ecd57f73d2d85c98cf26119c2cc0084b5b8be49d4127cb798c6011651d5361958eb6546c19b45fd6c94ea11ef47 + checksum: ea7bc7d43a626110e00b15ff323697becaa6c1d59f4b5c01a709e6fb84cfb5a9a9fef9cb93603128ad3249a26fbd15f5eeae8a9edc16612793a62d49ec1bd464 languageName: node linkType: hard -"@smithy/invalid-dependency@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/invalid-dependency@npm:3.0.3" +"@smithy/invalid-dependency@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/invalid-dependency@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 459b4ae4e47595e8a675ff2e8bfea7f58a41f77138416ea310c89e29312e08963a701cdc354324da9dd578a7995158b4421695365070d74b0276ddff7f701bba + checksum: 2581cf77bc5e26e66617c26fd4edc298722791c3a1b65a79c6547834e695e533a57c3fe6a306c3ee054c02ef3482014dfb3e715ba87714b107e9c0c3b9a6ef48 languageName: node linkType: hard @@ -12114,89 +11089,89 @@ __metadata: languageName: node linkType: hard -"@smithy/md5-js@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/md5-js@npm:3.0.3" +"@smithy/md5-js@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/md5-js@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 52ef56439be4187cc65391f4252173ffad0ce5a2ce5f636d78e9cdfb517844889340156ddbdbbe86f63e7f7e0fc924fe6905749a1c833910784015133a467406 + checksum: e95b8a0cc0ce38c7110a60ad684770a5d5525a2024649b01295ae68cab622ce14fe73fcc884154394e6e3f6b06a94f7185ed70a1265ab5fe79fe34bb74fc5884 languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^3.0.5": - version: 3.0.5 - resolution: "@smithy/middleware-content-length@npm:3.0.5" +"@smithy/middleware-content-length@npm:^3.0.8": + version: 3.0.8 + resolution: "@smithy/middleware-content-length@npm:3.0.8" dependencies: - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 21c667530f5e64db300827dfe8aa5b18396d151ac489a3e634f882179fae5c0f84940e1f831f0bf7b4b6b9623283f4a516da92d89c13ba395aede8788b523cd3 + checksum: 292310d3d6ed5639e24705283d94e6d5b800214b310b3d34617d2b94394846fde9e8312f661b38e0d7769314071f124826ceaf3202f345d32fc017c1d2b31665 languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^3.1.0": - version: 3.1.0 - resolution: "@smithy/middleware-endpoint@npm:3.1.0" - dependencies: - "@smithy/middleware-serde": ^3.0.3 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/shared-ini-file-loader": ^3.1.4 - "@smithy/types": ^3.3.0 - "@smithy/url-parser": ^3.0.3 - "@smithy/util-middleware": ^3.0.3 +"@smithy/middleware-endpoint@npm:^3.1.3": + version: 3.1.3 + resolution: "@smithy/middleware-endpoint@npm:3.1.3" + dependencies: + "@smithy/middleware-serde": ^3.0.6 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/shared-ini-file-loader": ^3.1.7 + "@smithy/types": ^3.4.2 + "@smithy/url-parser": ^3.0.6 + "@smithy/util-middleware": ^3.0.6 tslib: ^2.6.2 - checksum: 3271b7c1ec5d01db63439757d198e921618a2f1d4827c64fc425163e7d1bc969049d8c8ce74f3a8c0b0dac5ea3466c154f38805c8d62fe19fb9bd6af72ca2d3c + checksum: c3f4fcffa0ee0da9def2270d4dd5d84342edfebaec2ed1ffbbc9dec9615a01180d269ff9bb9cec72269707e2c44a53e7180e42802e1ea31e4b85ef763a577c66 languageName: node linkType: hard -"@smithy/middleware-retry@npm:^3.0.13": - version: 3.0.13 - resolution: "@smithy/middleware-retry@npm:3.0.13" +"@smithy/middleware-retry@npm:^3.0.18": + version: 3.0.18 + resolution: "@smithy/middleware-retry@npm:3.0.18" dependencies: - "@smithy/node-config-provider": ^3.1.4 - "@smithy/protocol-http": ^4.1.0 - "@smithy/service-error-classification": ^3.0.3 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 - "@smithy/util-middleware": ^3.0.3 - "@smithy/util-retry": ^3.0.3 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/protocol-http": ^4.1.3 + "@smithy/service-error-classification": ^3.0.6 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 + "@smithy/util-middleware": ^3.0.6 + "@smithy/util-retry": ^3.0.6 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: 3aa98ae08633022dedc19a60b173289d4c8e04c94fe45a626499269f596de517a94b587272a578d554d1e91dc8cb19297e3d3c45da01829d520a6c730e77df36 + checksum: 8561ed11d073579603fa3f8d6fbb63048831b948f804edcae48c3382df61f975580d13ef8ba4777bed05b28031319fdafc4d5c7676dc5a98955bedf073743489 languageName: node linkType: hard -"@smithy/middleware-serde@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/middleware-serde@npm:3.0.3" +"@smithy/middleware-serde@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/middleware-serde@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 6c633bb8957e078d480888bd33d5a8c269a483a1358c2b28c62daecfd442c711c509d9e69302e6b19fc298139ee67cdda63a604e7da0e4ef9005117d8e0897cc + checksum: a16b4ebec9262ca82b89467d8400d3b0940bc1f7504f60f7e6cad9baa3e41b48327b8d628286af59314c7622760aee9099878c142f9f456c585da8d59da6bd32 languageName: node linkType: hard -"@smithy/middleware-stack@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/middleware-stack@npm:3.0.3" +"@smithy/middleware-stack@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/middleware-stack@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: f4a450e2ebca0a8a3b4e1bbfad7d7e9c45edccbe1c984a22f2228092a526120748365e8964b478357249675d8bbc28fdaa8a4a19643a3c1d86bd74e1499327c5 + checksum: 5851dcf20eebe391e61f741570e78ccd2fe281acd6b34e54fd3f4bc4d6714cc80d0bd6ec6cea092f674e5e1eecb66b0e88ecec1aa3b19dfa5dba177944aa3f2a languageName: node linkType: hard -"@smithy/node-config-provider@npm:^3.1.4": - version: 3.1.4 - resolution: "@smithy/node-config-provider@npm:3.1.4" +"@smithy/node-config-provider@npm:^3.1.7": + version: 3.1.7 + resolution: "@smithy/node-config-provider@npm:3.1.7" dependencies: - "@smithy/property-provider": ^3.1.3 - "@smithy/shared-ini-file-loader": ^3.1.4 - "@smithy/types": ^3.3.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/shared-ini-file-loader": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 7ea4e7cea93ab154ab89a9d6b2453c8f96b96db18883070d287bc5fa9cfd10091bb00006a15bb7e6ed25810fd1a133d458e45310a8eaa1727a55d4ce2be3ba09 + checksum: 4bf1e1322c6a68e26fc426016ba2308e0416d8efeebd8473a694f263a91fdc57954b45406a022d6653f6016ce0d5533bdfa44184f759efb8ea4f3bc1e707f186 languageName: node linkType: hard @@ -12213,26 +11188,26 @@ __metadata: languageName: node linkType: hard -"@smithy/node-http-handler@npm:^3.1.4": - version: 3.1.4 - resolution: "@smithy/node-http-handler@npm:3.1.4" +"@smithy/node-http-handler@npm:^3.2.2": + version: 3.2.2 + resolution: "@smithy/node-http-handler@npm:3.2.2" dependencies: - "@smithy/abort-controller": ^3.1.1 - "@smithy/protocol-http": ^4.1.0 - "@smithy/querystring-builder": ^3.0.3 - "@smithy/types": ^3.3.0 + "@smithy/abort-controller": ^3.1.4 + "@smithy/protocol-http": ^4.1.3 + "@smithy/querystring-builder": ^3.0.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 8f2f611bef99800b122b852103c3d1ff391b91077df191ca06676bd8c4e50d7e335bf2a311f90ff6a9fa0639a812216492e5e0ecee703e89eb05a4b253c8273c + checksum: 2ccae7bba714e7831e5f1436106e5b1a1ea41f6b7d52868246d566b5f88a63efde6be52c424ff29993acd2f1f9ea3881b97450b0b05042251ec3ec53483f68fc languageName: node linkType: hard -"@smithy/property-provider@npm:^3.1.3": - version: 3.1.3 - resolution: "@smithy/property-provider@npm:3.1.3" +"@smithy/property-provider@npm:^3.1.6": + version: 3.1.6 + resolution: "@smithy/property-provider@npm:3.1.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 37a3d92267a2a32c2cc17fd1f0ab2b336f75fb7807db88f6194efede9d6a66068658a7effb7773451404fca990924393dbbf3d57e2aca67ef2e489a85666e225 + checksum: b8e3f06a01a5833ab7204fde1701fc2fa92737c2205daa7defab43505cc50928dd71eadca359f04f85be49f913efc8c57222899f7861ae95f9e778db84ff3d90 languageName: node linkType: hard @@ -12246,13 +11221,13 @@ __metadata: languageName: node linkType: hard -"@smithy/protocol-http@npm:^4.1.0": - version: 4.1.0 - resolution: "@smithy/protocol-http@npm:4.1.0" +"@smithy/protocol-http@npm:^4.1.3": + version: 4.1.3 + resolution: "@smithy/protocol-http@npm:4.1.3" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: fe4f97bc35075c6e6669c12ff90a4ab3dbe620b375298795d0c1104b30d04536cf002ea81f29983895c6042c7a30eecd1d2306d3ac90bf7910ec02929233f5ad + checksum: 885b077e3ac70d323b139c86938d145d9c38e67336d0ca0e7f2ed650de7ed6224d900a69d38eab8675161eae5773a8e09df799dedc856a2636bf71cfb1b42a33 languageName: node linkType: hard @@ -12267,73 +11242,73 @@ __metadata: languageName: node linkType: hard -"@smithy/querystring-builder@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/querystring-builder@npm:3.0.3" +"@smithy/querystring-builder@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/querystring-builder@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 "@smithy/util-uri-escape": ^3.0.0 tslib: ^2.6.2 - checksum: 5c46c620d87f9b4e67b8eb543667b0160fb05bbec01d62d45adb94305369dca9e82daba47d81e840fdc399fa47f9b5930ce668d65fe83ee278a1b27d59d0b5d3 + checksum: a6a3fc016606e4eb491c37fdf97b4c2f7bf090cc994535bc3cc94d50ab4931771f11078aa70f1b83bf4151cd9e6de7f1f76ec19315af56a664c8b8197f727b43 languageName: node linkType: hard -"@smithy/querystring-parser@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/querystring-parser@npm:3.0.3" +"@smithy/querystring-parser@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/querystring-parser@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 1de11cbc4325578b243a0e3e89b46371f4705d3df41ea51b37e8efa655d3b75253180b0fca9ceed8b3955a2d458689f551cd24fd904d0f65647c62c6b08795bf + checksum: afa89d43e01a21375a8958a66e68857fc878264a847da486660875aecb804f642c0b74aa6641d404d0c5361ed58cf98de5b5acd20df425dfd17475ec6f061722 languageName: node linkType: hard -"@smithy/service-error-classification@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/service-error-classification@npm:3.0.3" +"@smithy/service-error-classification@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/service-error-classification@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 - checksum: 5bef710f5698c929c97865cba41f36b0c59100b9a1c4478a2d47caeb5e3a1a18077b870b365efaa45c94666f2075bc8978f7a6e8b964afbba3a4e490eb6c13eb + "@smithy/types": ^3.4.2 + checksum: 16b9a181c250064c1ca795575cd8a0a476cbca83594b4939890092cb74f768180d4b54d4293071c942d251f2f88990ee4e380c522b72358f211467845087daf9 languageName: node linkType: hard -"@smithy/shared-ini-file-loader@npm:^3.1.4": - version: 3.1.4 - resolution: "@smithy/shared-ini-file-loader@npm:3.1.4" +"@smithy/shared-ini-file-loader@npm:^3.1.7": + version: 3.1.7 + resolution: "@smithy/shared-ini-file-loader@npm:3.1.7" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: c5321635f3be34e424009fc9045454a9ceec543ec20b3b9719bf3a48bbfc03b794f4545546e9c2dcb0a987de2ca5ff8999df9bf7c166c6fc7685c1fa1f068bc1 + checksum: 2e222de3bb4693db441dd84b5a3fadfbe4f08eb978df1131e5701657214b3104c811f69d0a7157b39c77d8d80c8a368b97343c68cb81adea8877bc452de5c4a6 languageName: node linkType: hard -"@smithy/signature-v4@npm:^4.1.0": - version: 4.1.0 - resolution: "@smithy/signature-v4@npm:4.1.0" +"@smithy/signature-v4@npm:^4.1.3": + version: 4.1.3 + resolution: "@smithy/signature-v4@npm:4.1.3" dependencies: "@smithy/is-array-buffer": ^3.0.0 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 "@smithy/util-hex-encoding": ^3.0.0 - "@smithy/util-middleware": ^3.0.3 + "@smithy/util-middleware": ^3.0.6 "@smithy/util-uri-escape": ^3.0.0 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 8c58bbc5b3f9eed092351f36dc0193ed2e43f916856dc95eadc65b42460a0c5f662016156034ef8a25419f95be8f6a15a7e85469358b77db236445919f66c77e + checksum: a542dbdea4d04be53b424658f7372fedf1810520be6f8595e0692f5164f8e35554e3a065befd211de4ac99b56bcc6b03460c4bd2dd1fef31468b0971539d4c0c languageName: node linkType: hard -"@smithy/smithy-client@npm:^3.1.11": - version: 3.1.11 - resolution: "@smithy/smithy-client@npm:3.1.11" +"@smithy/smithy-client@npm:^3.3.2": + version: 3.3.2 + resolution: "@smithy/smithy-client@npm:3.3.2" dependencies: - "@smithy/middleware-endpoint": ^3.1.0 - "@smithy/middleware-stack": ^3.0.3 - "@smithy/protocol-http": ^4.1.0 - "@smithy/types": ^3.3.0 - "@smithy/util-stream": ^3.1.3 + "@smithy/middleware-endpoint": ^3.1.3 + "@smithy/middleware-stack": ^3.0.6 + "@smithy/protocol-http": ^4.1.3 + "@smithy/types": ^3.4.2 + "@smithy/util-stream": ^3.1.6 tslib: ^2.6.2 - checksum: acbbd29f45d342845eebf9086fd37cbf5abc563c60f37f787be92e4922bf09594cd882a6f9dcec85073120580c030ad14a035fd10c120733ca402938d87c143b + checksum: 58725fc69f9efb6dbe0c5f8449ac0b6fcd26a6bbecb4bdaca4446f6cebb1ca30e651d9bcab9f1cfa4eb3c3b618eb115955aacef4f2d0e6c32e7c1d5d63dba06a languageName: node linkType: hard @@ -12346,23 +11321,23 @@ __metadata: languageName: node linkType: hard -"@smithy/types@npm:^3.3.0": - version: 3.3.0 - resolution: "@smithy/types@npm:3.3.0" +"@smithy/types@npm:^3.4.2": + version: 3.4.2 + resolution: "@smithy/types@npm:3.4.2" dependencies: tslib: ^2.6.2 - checksum: 29bb5f83c41e32f8d4094a2aba2d3dfbd763ab5943784a700f3fa22df0dcf0ccac1b1907f7a87fbb9f6f2269fcd4750524bcb48f892249e200ffe397c0981309 + checksum: 84daaa72d890a977185fa34566879ba3ee6cab6d32986dfa773c540b6dee81701128067ed0fe876d9f2dd197e4079d66ec32bdd0b52c18e9a9b0c493bc1a7478 languageName: node linkType: hard -"@smithy/url-parser@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/url-parser@npm:3.0.3" +"@smithy/url-parser@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/url-parser@npm:3.0.6" dependencies: - "@smithy/querystring-parser": ^3.0.3 - "@smithy/types": ^3.3.0 + "@smithy/querystring-parser": ^3.0.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 86b4bc8e6c176b56076c30233ca4cfeb98d162fe27a348ddfda5f163ce7d173b8e684aa26202bbf4e0b5695b0ad43c0cb40170ca6793652d0ea6edb00443c036 + checksum: 861000a437bc81cc9d09ca272458fdd2934d6d9fbdff238e672783435ce9b1c46cc9cd4f9f037e2f9950f4e8123dc6b23f6d73a62d3789bee163db5ee176b484 languageName: node linkType: hard @@ -12424,42 +11399,42 @@ __metadata: languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^3.0.13": - version: 3.0.13 - resolution: "@smithy/util-defaults-mode-browser@npm:3.0.13" +"@smithy/util-defaults-mode-browser@npm:^3.0.18": + version: 3.0.18 + resolution: "@smithy/util-defaults-mode-browser@npm:3.0.18" dependencies: - "@smithy/property-provider": ^3.1.3 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 + "@smithy/property-provider": ^3.1.6 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 bowser: ^2.11.0 tslib: ^2.6.2 - checksum: 087c583d0b276df7369053b068217821c4474bf3d9de3111110ef81e8d895539f80b53a11ecaaae105ce2d24a78a52d958840b8a6c6d3e18696c45f27015a3f3 + checksum: f5bd83ad958b3cbd288f59963cda095a014e6d7ea717222548147c3bfcf27679dcff81203472d61b757c75e0dfdf98d67e8d088901a702bbdd13c5563ccc9abf languageName: node linkType: hard -"@smithy/util-defaults-mode-node@npm:^3.0.13": - version: 3.0.13 - resolution: "@smithy/util-defaults-mode-node@npm:3.0.13" +"@smithy/util-defaults-mode-node@npm:^3.0.18": + version: 3.0.18 + resolution: "@smithy/util-defaults-mode-node@npm:3.0.18" dependencies: - "@smithy/config-resolver": ^3.0.5 - "@smithy/credential-provider-imds": ^3.2.0 - "@smithy/node-config-provider": ^3.1.4 - "@smithy/property-provider": ^3.1.3 - "@smithy/smithy-client": ^3.1.11 - "@smithy/types": ^3.3.0 + "@smithy/config-resolver": ^3.0.8 + "@smithy/credential-provider-imds": ^3.2.3 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/property-provider": ^3.1.6 + "@smithy/smithy-client": ^3.3.2 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: a56c9f06472bb42e4180fd424ec237a6ca170eef5d487d1a3806a8845cd9c0bef37b431b22a9340afee5ae26b6bca6c11647112ef73433ba949f8c671e2b2067 + checksum: 86917bf7d46826ff859ee656e88c00c7ab15ef977a7aacaa7906075d17039914b1219c6cd3428c2b3273b9378191d414f64137be3606357871294519f0c6fa5f languageName: node linkType: hard -"@smithy/util-endpoints@npm:^2.0.5": - version: 2.0.5 - resolution: "@smithy/util-endpoints@npm:2.0.5" +"@smithy/util-endpoints@npm:^2.1.2": + version: 2.1.2 + resolution: "@smithy/util-endpoints@npm:2.1.2" dependencies: - "@smithy/node-config-provider": ^3.1.4 - "@smithy/types": ^3.3.0 + "@smithy/node-config-provider": ^3.1.7 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: bb2a96323f52beaf2820f4e5764c865cff3ac5bca0c0df6923bb4582b0f87faf1606110cd4e36005ac43f41e9673ebdca4bbb8b913880fc2a4e0ff3301250da8 + checksum: b769e64828b9aa3f9e327514cfd35a62584fcc363092173f7f4c55a602c5e5aba342616d6816a2045d334797ffe26086534b627d6e007d4bd4a54358c7ed4a8d languageName: node linkType: hard @@ -12472,40 +11447,40 @@ __metadata: languageName: node linkType: hard -"@smithy/util-middleware@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/util-middleware@npm:3.0.3" +"@smithy/util-middleware@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/util-middleware@npm:3.0.6" dependencies: - "@smithy/types": ^3.3.0 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: f37f25d65595af5ff4c3f69fa7e66545ac1651f77979e15ffbc9047e18fc668dae90458ee76add85a49ea3729c49d317e40542d5430e81e2eafe8dcae2ddb3bc + checksum: d51a473bd376aef6e26b1e26ced37350464058661fb685addf84babbe14f5225734470cdf47a80e478c679d6e984fbdaf9af70c9ff66578e180af9f7f81e5c35 languageName: node linkType: hard -"@smithy/util-retry@npm:^3.0.3": - version: 3.0.3 - resolution: "@smithy/util-retry@npm:3.0.3" +"@smithy/util-retry@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/util-retry@npm:3.0.6" dependencies: - "@smithy/service-error-classification": ^3.0.3 - "@smithy/types": ^3.3.0 + "@smithy/service-error-classification": ^3.0.6 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: c760595376154be67414083aa6f76094022df72987521469b124ef3ef5848c0536757dcd2006520580380db6a4d7b597a05569470c3151f71d5e678df63f4c13 + checksum: 3bd5ddabf8f856343a5da2375425ff71ae8739b43f21e22ca9d910506fb4e35fa4c43d3a0fa6afbe47b5619624d4c4736806df246168a6fae1bf748862483f2f languageName: node linkType: hard -"@smithy/util-stream@npm:^3.1.3": - version: 3.1.3 - resolution: "@smithy/util-stream@npm:3.1.3" +"@smithy/util-stream@npm:^3.1.6": + version: 3.1.6 + resolution: "@smithy/util-stream@npm:3.1.6" dependencies: - "@smithy/fetch-http-handler": ^3.2.4 - "@smithy/node-http-handler": ^3.1.4 - "@smithy/types": ^3.3.0 + "@smithy/fetch-http-handler": ^3.2.7 + "@smithy/node-http-handler": ^3.2.2 + "@smithy/types": ^3.4.2 "@smithy/util-base64": ^3.0.0 "@smithy/util-buffer-from": ^3.0.0 "@smithy/util-hex-encoding": ^3.0.0 "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: b663124c3b857b7744a0f2db47d091c570ff4fe4e8a8d254a7e1b8fed1d57fa4dc8fb12e496f7f607666ac5bc36d6f44a57ef3132bc882ff1d869b9bfdb5fc6e + checksum: 84cbcbd1febedb5e04267288128eb45a4f68e33397b3a1d8cb65fb03d9104b150e6ac7bf5ca4c9b6a9376e9376f7247b3c0446803386e8b31b8311cee5db0b70 languageName: node linkType: hard @@ -12547,14 +11522,14 @@ __metadata: languageName: node linkType: hard -"@smithy/util-waiter@npm:^3.1.2": - version: 3.1.2 - resolution: "@smithy/util-waiter@npm:3.1.2" +"@smithy/util-waiter@npm:^3.1.5": + version: 3.1.5 + resolution: "@smithy/util-waiter@npm:3.1.5" dependencies: - "@smithy/abort-controller": ^3.1.1 - "@smithy/types": ^3.3.0 + "@smithy/abort-controller": ^3.1.4 + "@smithy/types": ^3.4.2 tslib: ^2.6.2 - checksum: 35773b1bbbb215102555a55ce4de57cbd3e38f37546ca3e6748ce3856119019a613946b399c6d97981a0bad447ce9c41f87c276325ff4c0e5a2276ee4e9e384e + checksum: aa2dedcd9be3c6c2a56cba24c3586af63ff2b9f0aab0ba8054fa7cc3bbf1553a7346c2e464743580e93f740d7ccbc9f254728f7d67ea3b110fce78a74fe7b85a languageName: node linkType: hard @@ -12932,6 +11907,15 @@ __metadata: languageName: node linkType: hard +"@types/adm-zip@npm:^0.5.5": + version: 0.5.5 + resolution: "@types/adm-zip@npm:0.5.5" + dependencies: + "@types/node": "*" + checksum: 808c25b8a1c2e1c594cf9b1514e7953105cf96e19e38aa7dc109ff2537bda7345b950ef1f4e54a6e824e5503e29d24b0ff6d0aa1ff9bd4afb79ef0ef2df9ebab + languageName: node + linkType: hard + "@types/aos@npm:^3.0.4": version: 3.0.4 resolution: "@types/aos@npm:3.0.4" @@ -13926,6 +12910,22 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^10.0.10": + version: 10.0.20 + resolution: "@types/sinon@npm:10.0.20" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: 7322771345c202b90057f8112e0d34b7339e5ae1827fb1bfe385fc9e38ed6a2f18b4c66e88d27d98c775f7f74fb1167c0c14f61ca64155786534541e6c6eb05f + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.5 + resolution: "@types/sinonjs__fake-timers@npm:8.1.5" + checksum: 7e3c08f6c13df44f3ea7d9a5155ddf77e3f7314c156fa1c5a829a4f3763bafe2f75b1283b887f06e6b4296996a2f299b70f64ff82625f9af5885436e2524d10c + languageName: node + linkType: hard + "@types/sinonjs__fake-timers@npm:8.1.1": version: 8.1.1 resolution: "@types/sinonjs__fake-timers@npm:8.1.1" @@ -13993,6 +12993,15 @@ __metadata: languageName: node linkType: hard +"@types/unzipper@npm:^0.10.10": + version: 0.10.10 + resolution: "@types/unzipper@npm:0.10.10" + dependencies: + "@types/node": "*" + checksum: 4ba5f6c4c5a892f5f5ce7724a4c3f2ea772a29043e296a28b725162ffff8fb25d2d0995c5536705e13bfbde7765d085f0da5408b25946457d050ac4b75aaefee + languageName: node + linkType: hard + "@types/validate-npm-package-name@npm:^3.0.3": version: 3.0.3 resolution: "@types/validate-npm-package-name@npm:3.0.3" @@ -15355,9 +14364,9 @@ __metadata: version: 0.0.0-use.local resolution: "@webiny/api-headless-cms-es-tasks@workspace:packages/api-headless-cms-es-tasks" dependencies: - "@babel/cli": ^7.22.6 - "@babel/core": ^7.22.8 - "@babel/preset-env": ^7.22.7 + "@babel/cli": ^7.23.9 + "@babel/core": ^7.24.0 + "@babel/preset-env": ^7.24.0 "@faker-js/faker": ^8.4.1 "@webiny/api": 0.0.0 "@webiny/api-elasticsearch": 0.0.0 @@ -15380,6 +14389,50 @@ __metadata: languageName: unknown linkType: soft +"@webiny/api-headless-cms-import-export@0.0.0, @webiny/api-headless-cms-import-export@workspace:packages/api-headless-cms-import-export": + version: 0.0.0-use.local + resolution: "@webiny/api-headless-cms-import-export@workspace:packages/api-headless-cms-import-export" + 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 + "@smithy/node-http-handler": ^2.1.6 + "@types/adm-zip": ^0.5.5 + "@types/unzipper": ^0.10.10 + "@webiny/api": 0.0.0 + "@webiny/api-admin-users": 0.0.0 + "@webiny/api-file-manager": 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/aws-sdk": 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/tasks": 0.0.0 + "@webiny/utils": 0.0.0 + "@webiny/wcp": 0.0.0 + adm-zip: ^0.5.14 + archiver: ^7.0.1 + aws-sdk-client-mock: ^4.0.1 + bytes: ^3.1.2 + graphql: ^15.8.0 + ttypescript: ^1.5.13 + typescript: ^4.7.4 + uniqid: ^5.4.0 + unzipper: ^0.12.3 + zod: ^3.23.8 + languageName: unknown + linkType: soft + "@webiny/api-headless-cms-tasks@0.0.0, @webiny/api-headless-cms-tasks@workspace:packages/api-headless-cms-tasks": version: 0.0.0-use.local resolution: "@webiny/api-headless-cms-tasks@workspace:packages/api-headless-cms-tasks" @@ -15387,6 +14440,7 @@ __metadata: "@babel/cli": ^7.23.9 "@babel/core": ^7.24.0 "@webiny/api-headless-cms-bulk-actions": 0.0.0 + "@webiny/api-headless-cms-import-export": 0.0.0 "@webiny/cli": 0.0.0 "@webiny/project-utils": 0.0.0 ttypescript: ^1.5.12 @@ -15442,7 +14496,7 @@ __metadata: ttypescript: ^1.5.12 typescript: 4.9.5 write-json-file: ^4.3.0 - zod: ^3.22.4 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -15562,7 +14616,7 @@ __metadata: rimraf: ^5.0.5 ttypescript: ^1.5.12 typescript: 4.9.5 - zod: ^3.22.4 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -15675,7 +14729,7 @@ __metadata: stream: ^0.0.2 ttypescript: ^1.5.12 typescript: 4.9.5 - uniqid: ^5.2.0 + uniqid: ^5.4.0 yauzl: ^2.10.0 languageName: unknown linkType: soft @@ -15809,8 +14863,8 @@ __metadata: stream: ^0.0.2 ttypescript: ^1.5.12 typescript: 4.9.5 - uniqid: ^5.2.0 - zod: ^3.22.4 + uniqid: ^5.4.0 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -16255,7 +15309,7 @@ __metadata: ttypescript: ^1.5.13 type-fest: ^2.19.0 typescript: 4.9.5 - zod: ^3.22.4 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -16326,7 +15380,7 @@ __metadata: store: ^2.0.12 ttypescript: ^1.5.12 typescript: 4.9.5 - zod: ^3.22.4 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -16793,7 +15847,7 @@ __metadata: rimraf: ^5.0.5 ttypescript: ^1.5.13 typescript: 4.9.5 - zod: ^3.22.4 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -17311,7 +16365,7 @@ __metadata: swiper: ^9.3.2 ttypescript: ^1.5.12 typescript: 4.9.5 - uniqid: ^5.0.3 + uniqid: ^5.4.0 languageName: unknown linkType: soft @@ -17841,27 +16895,27 @@ __metadata: version: 0.0.0-use.local resolution: "@webiny/aws-sdk@workspace:packages/aws-sdk" dependencies: - "@aws-sdk/client-apigatewaymanagementapi": ^3.621.0 - "@aws-sdk/client-cloudfront": ^3.621.0 - "@aws-sdk/client-cloudwatch-events": ^3.621.0 - "@aws-sdk/client-cloudwatch-logs": ^3.621.0 - "@aws-sdk/client-cognito-identity-provider": ^3.621.0 - "@aws-sdk/client-dynamodb": ^3.621.0 - "@aws-sdk/client-dynamodb-streams": ^3.621.0 - "@aws-sdk/client-eventbridge": ^3.621.0 - "@aws-sdk/client-iam": ^3.621.0 - "@aws-sdk/client-iot": ^3.621.0 - "@aws-sdk/client-lambda": ^3.621.0 - "@aws-sdk/client-s3": ^3.621.0 - "@aws-sdk/client-sfn": ^3.621.0 - "@aws-sdk/client-sqs": ^3.621.0 - "@aws-sdk/client-sts": ^3.621.0 - "@aws-sdk/credential-providers": ^3.621.0 - "@aws-sdk/lib-dynamodb": ^3.621.0 - "@aws-sdk/lib-storage": ^3.621.0 - "@aws-sdk/s3-presigned-post": ^3.621.0 - "@aws-sdk/s3-request-presigner": ^3.621.0 - "@aws-sdk/util-dynamodb": ^3.621.0 + "@aws-sdk/client-apigatewaymanagementapi": ^3.654.0 + "@aws-sdk/client-cloudfront": ^3.654.0 + "@aws-sdk/client-cloudwatch-events": ^3.654.0 + "@aws-sdk/client-cloudwatch-logs": ^3.654.0 + "@aws-sdk/client-cognito-identity-provider": ^3.654.0 + "@aws-sdk/client-dynamodb": ^3.654.0 + "@aws-sdk/client-dynamodb-streams": ^3.654.0 + "@aws-sdk/client-eventbridge": ^3.654.0 + "@aws-sdk/client-iam": ^3.654.0 + "@aws-sdk/client-iot": ^3.654.0 + "@aws-sdk/client-lambda": ^3.654.0 + "@aws-sdk/client-s3": ^3.654.0 + "@aws-sdk/client-sfn": ^3.654.0 + "@aws-sdk/client-sqs": ^3.654.0 + "@aws-sdk/client-sts": ^3.654.0 + "@aws-sdk/credential-providers": ^3.654.0 + "@aws-sdk/lib-dynamodb": ^3.654.0 + "@aws-sdk/lib-storage": ^3.654.0 + "@aws-sdk/s3-presigned-post": ^3.654.0 + "@aws-sdk/s3-request-presigner": ^3.654.0 + "@aws-sdk/util-dynamodb": ^3.654.0 "@babel/cli": ^7.23.9 "@babel/core": ^7.24.0 "@webiny/cli": 0.0.0 @@ -18226,7 +17280,7 @@ __metadata: semver: ^7.3.5 ts-morph: ^11.0.0 typescript: 4.9.5 - uniqid: 5.4.0 + uniqid: ^5.4.0 yargs: ^17.4.0 bin: webiny: ./bin.js @@ -18593,7 +17647,7 @@ __metadata: reflect-metadata: ^0.1.13 ttypescript: ^1.5.13 typescript: 4.9.5 - zod: ^3.22.4 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -18784,7 +17838,7 @@ __metadata: rimraf: ^5.0.5 ttypescript: ^1.5.13 typescript: 4.9.5 - uniqid: ^5.2.0 + uniqid: ^5.4.0 languageName: unknown linkType: soft @@ -19161,11 +18215,12 @@ __metadata: deep-equal: ^2.2.3 lodash: ^4.17.21 object-merge-advanced: ^12.1.0 + object-sizeof: ^2.6.4 rimraf: ^5.0.5 ttypescript: ^1.5.13 type-fest: ^2.19.0 typescript: 4.9.5 - zod: ^3.22.4 + zod: ^3.23.8 languageName: unknown linkType: soft @@ -19652,6 +18707,13 @@ __metadata: languageName: node linkType: hard +"adm-zip@npm:^0.5.14": + version: 0.5.16 + resolution: "adm-zip@npm:0.5.16" + checksum: 1f4104f3462b99e1b34d78ccfbdcf47e533a9cc7f894cedec6cd67b06cc6ad0b3a45241d66df5471050c7abbdd67e5707e3959fc76d75176ed6101a5b2a580d5 + languageName: node + linkType: hard + "admin@workspace:apps/admin": version: 0.0.0-use.local resolution: "admin@workspace:apps/admin" @@ -20833,6 +19895,17 @@ __metadata: languageName: node linkType: hard +"aws-sdk-client-mock@npm:^4.0.1": + version: 4.0.1 + resolution: "aws-sdk-client-mock@npm:4.0.1" + dependencies: + "@types/sinon": ^10.0.10 + sinon: ^16.1.3 + tslib: ^2.1.0 + checksum: ba2e38c05d3dd89b71a305880b9994b60ae8744caf8ad111cb63f6cc65bc7ec15772b6c967e029933eba15b65304a80b1cf6ef5e7267ae49c334a8559125f138 + languageName: node + linkType: hard + "aws-sdk@npm:^2.814.0": version: 2.1310.0 resolution: "aws-sdk@npm:2.1310.0" @@ -21307,7 +20380,7 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:^3.5.1, bluebird@npm:^3.7.2": +"bluebird@npm:^3.5.1, bluebird@npm:^3.7.2, bluebird@npm:~3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" checksum: 869417503c722e7dc54ca46715f70e15f4d9c602a423a02c825570862d12935be59ed9c7ba34a9b31f186c017c23cac6b54e35446f8353059c101da73eac22ef @@ -21740,7 +20813,7 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2, bytes@npm:^3.0.0, bytes@npm:^3.1.0": +"bytes@npm:3.1.2, bytes@npm:^3.0.0, bytes@npm:^3.1.0, bytes@npm:^3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e @@ -22666,7 +21739,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.0, commander@npm:^6.2.1": +"commander@npm:^6.2.1": version: 6.2.1 resolution: "commander@npm:6.2.1" checksum: d7090410c0de6bc5c67d3ca41c41760d6d268f3c799e530aafb73b7437d1826bbf0d2a3edac33f8b57cc9887b4a986dce307fa5557e109be40eadb7c43b21742 @@ -23807,7 +22880,7 @@ __metadata: nanoid: ^3.3.7 node-fetch: ^2.6.1 typescript: 4.9.5 - uniqid: ^5.2.0 + uniqid: ^5.4.0 languageName: unknown linkType: soft @@ -24532,6 +23605,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.1.0": + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 12b63ca9c36c72bafa3effa77121f0581b4015df18bc16bac1f8e263597735649f1a173c26f7eba17fb4162b073fee61788abe49610e6c70a2641fe1895443fd + languageName: node + linkType: hard + "diffie-hellman@npm:^5.0.0": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -24879,6 +23959,15 @@ __metadata: languageName: node linkType: hard +"duplexer2@npm:~0.1.4": + version: 0.1.4 + resolution: "duplexer2@npm:0.1.4" + dependencies: + readable-stream: ^2.0.2 + checksum: 744961f03c7f54313f90555ac20284a3fb7bf22fdff6538f041a86c22499560eb6eac9d30ab5768054137cb40e6b18b40f621094e0261d7d8c35a37b7a5ad241 + languageName: node + linkType: hard + "duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -27685,7 +26774,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.2.11": +"graceful-fs@npm:4.2.11, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.2": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 @@ -30653,6 +29742,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^6.2.0": + version: 6.2.0 + resolution: "just-extend@npm:6.2.0" + checksum: 022024d6f687c807963b97a24728a378799f7e4af7357d1c1f90dedb402943d5c12be99a5136654bed8362c37a358b1793feaad3366896f239a44e17c5032d86 + languageName: node + linkType: hard + "jwa@npm:^1.4.1": version: 1.4.1 resolution: "jwa@npm:1.4.1" @@ -32782,6 +31878,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^5.1.4": + version: 5.1.9 + resolution: "nise@npm:5.1.9" + dependencies: + "@sinonjs/commons": ^3.0.0 + "@sinonjs/fake-timers": ^11.2.2 + "@sinonjs/text-encoding": ^0.7.2 + just-extend: ^6.2.0 + path-to-regexp: ^6.2.1 + checksum: ab9fd6eabc98170f18aef6c9567983145c1dc62c7aef46eda0fea754083316c1f0f9b2c32e9b4bfdd25122276d670293596ed672b54dd1ffa8eb58b56a30ea95 + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -33498,6 +32607,15 @@ __metadata: languageName: node linkType: hard +"object-sizeof@npm:^2.6.4": + version: 2.6.4 + resolution: "object-sizeof@npm:2.6.4" + dependencies: + buffer: ^6.0.3 + checksum: 0d8767669b6bdfdfd4fe48381078567af53e5c1e35522eadc9641fed60ed45b661263b104384df9b3ea838b0c5ac8beff4ed740d54913b8d6b61387fe088cf02 + languageName: node + linkType: hard + "object.assign@npm:^4.1.0, object.assign@npm:^4.1.3, object.assign@npm:^4.1.4": version: 4.1.4 resolution: "object.assign@npm:4.1.4" @@ -34348,6 +33466,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^6.2.1": + version: 6.2.2 + resolution: "path-to-regexp@npm:6.2.2" + checksum: b7b0005c36f5099f9ed1fb20a820d2e4ed1297ffe683ea1d678f5e976eb9544f01debb281369dabdc26da82e6453901bf71acf2c7ed14b9243536c2a45286c33 + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -37119,7 +36244,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.3.8": +"readable-stream@npm:^2.0.2, readable-stream@npm:^2.3.8": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -38621,6 +37746,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^16.1.3": + version: 16.1.3 + resolution: "sinon@npm:16.1.3" + dependencies: + "@sinonjs/commons": ^3.0.0 + "@sinonjs/fake-timers": ^10.3.0 + "@sinonjs/samsam": ^8.0.0 + diff: ^5.1.0 + nise: ^5.1.4 + supports-color: ^7.2.0 + checksum: 83e5ccd724efdb5d1471e41b2cfaf4a46d9fe2cc76c15a23dedfbd741f7d448e6aec3d34a8253a79bdf8ddcc5bd889c5277ae71879e4709c621cb41094fbcdab + languageName: node + linkType: hard + "sinon@npm:^9.0.2": version: 9.2.4 resolution: "sinon@npm:9.2.4" @@ -39601,7 +38740,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -40646,7 +39785,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:4.9.5": +"typescript@npm:4.9.5, typescript@npm:^4.7.4": version: 4.9.5 resolution: "typescript@npm:4.9.5" bin: @@ -40676,7 +39815,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@4.9.5#~builtin": +"typescript@patch:typescript@4.9.5#~builtin, typescript@patch:typescript@^4.7.4#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" bin: @@ -40803,7 +39942,7 @@ __metadata: languageName: node linkType: hard -"uniqid@npm:5.4.0, uniqid@npm:^5.0.3, uniqid@npm:^5.2.0": +"uniqid@npm:^5.4.0": version: 5.4.0 resolution: "uniqid@npm:5.4.0" checksum: 69fc28e7b2b5b24227b4295e51aa7c1d4085a60655ad071db8fc350876cc9044d68ee0781c0d27c2c98691380aa418970e0d1b02a1e4564480f9d19b0dc1707b @@ -40915,6 +40054,19 @@ __metadata: languageName: node linkType: hard +"unzipper@npm:^0.12.3": + version: 0.12.3 + resolution: "unzipper@npm:0.12.3" + dependencies: + bluebird: ~3.7.2 + duplexer2: ~0.1.4 + fs-extra: ^11.2.0 + graceful-fs: ^4.2.2 + node-int64: ^0.4.0 + checksum: 2e3296d1fad307b02b3d0f3e9c4ac1bdd56047e66fe5108a9e580b417f4ac9b07c31e9ded3e006e01edaaba3e20b13c638bd3c893600f75c589b6e0f778d9ffd + languageName: node + linkType: hard + "upath@npm:2.0.1": version: 2.0.1 resolution: "upath@npm:2.0.1" @@ -42325,9 +41477,16 @@ __metadata: languageName: node linkType: hard -"zod@npm:3.22.4, zod@npm:^3.22.4": +"zod@npm:3.22.4": version: 3.22.4 resolution: "zod@npm:3.22.4" checksum: 80bfd7f8039b24fddeb0718a2ec7c02aa9856e4838d6aa4864335a047b6b37a3273b191ef335bf0b2002e5c514ef261ffcda5a589fb084a48c336ffc4cdbab7f languageName: node linkType: hard + +"zod@npm:^3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 15949ff82118f59c893dacd9d3c766d02b6fa2e71cf474d5aa888570c469dbf5446ac5ad562bb035bf7ac9650da94f290655c194f4a6de3e766f43febd432c5c + languageName: node + linkType: hard