diff --git a/apps/products-feed/package.json b/apps/products-feed/package.json index 2f274ff..c7807c2 100644 --- a/apps/products-feed/package.json +++ b/apps/products-feed/package.json @@ -16,6 +16,7 @@ "schemaVersion": "3.10" }, "dependencies": { + "@aws-sdk/client-s3": "^3.332.0", "@hookform/resolvers": "^2.9.10", "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.11.3", diff --git a/apps/products-feed/src/lib/google-feed/fetch-product-data.ts b/apps/products-feed/src/lib/google-feed/fetch-product-data.ts index b8eebc7..99b6b75 100644 --- a/apps/products-feed/src/lib/google-feed/fetch-product-data.ts +++ b/apps/products-feed/src/lib/google-feed/fetch-product-data.ts @@ -7,13 +7,16 @@ import { GoogleFeedProductVariantFragment, } from "../../../generated/graphql"; -const getCursors = async ({ client, channel }: { client: Client; channel: string }) => { +export const getCursors = async ({ client, channel }: { client: Client; channel: string }) => { + const logger = createLogger({ saleorApiUrl: url, channel, fn: "getCursors" }); + + logger.debug(`Fetching cursors for channel ${channel}`); + let result = await client .query(FetchProductCursorsDocument, { channel: channel as string, first: 100 }) .toPromise(); - // First page is queried without the `after` param, so we start an array with `undefined` - const cursors: Array = [undefined]; + const cursors: Array = []; while (result.data?.productVariants?.pageInfo.hasNextPage) { result = await client @@ -44,6 +47,8 @@ const fetchVariants = async ({ }): Promise => { const logger = createLogger({ saleorApiUrl: url, channel, fn: "fetchVariants" }); + logger.debug(`Fetching variants for channel ${channel} with cursor ${after}`); + const result = await client .query(FetchProductDataForFeedDocument, { channel: channel as string, @@ -63,16 +68,19 @@ const fetchVariants = async ({ interface FetchProductDataArgs { client: Client; channel: string; + cursors?: Array; } -export const fetchProductData = async ({ client, channel }: FetchProductDataArgs) => { +export const fetchProductData = async ({ client, channel, cursors }: FetchProductDataArgs) => { const logger = createLogger({ saleorApiUrl: url, channel, route: "Google Product Feed" }); - const cursors = await getCursors({ client, channel }); + const cachedCursors = cursors || (await getCursors({ client, channel })); - logger.debug(`Query generated ${cursors.length} cursors`); + const pageCursors = [undefined, ...cachedCursors]; - const promises = cursors.map((cursor) => fetchVariants({ client, after: cursor, channel })); + logger.debug(`Query generated ${pageCursors.length} cursors`); + + const promises = pageCursors.map((cursor) => fetchVariants({ client, after: cursor, channel })); const results = await Promise.all(promises); diff --git a/apps/products-feed/src/lib/google-feed/get-google-feed-settings.ts b/apps/products-feed/src/lib/google-feed/get-google-feed-settings.ts index c0b22d4..a3a1cbf 100644 --- a/apps/products-feed/src/lib/google-feed/get-google-feed-settings.ts +++ b/apps/products-feed/src/lib/google-feed/get-google-feed-settings.ts @@ -29,5 +29,6 @@ export const getGoogleFeedSettings = async ({ authData, channel }: GetGoogleFeed return { storefrontUrl, productStorefrontUrl, + s3BucketConfiguration: configuration.s3BucketConfiguration, }; }; diff --git a/apps/products-feed/src/modules/app-configuration/metadata-manager.ts b/apps/products-feed/src/lib/metadata-manager.ts similarity index 98% rename from apps/products-feed/src/modules/app-configuration/metadata-manager.ts rename to apps/products-feed/src/lib/metadata-manager.ts index f63ba63..a852374 100644 --- a/apps/products-feed/src/modules/app-configuration/metadata-manager.ts +++ b/apps/products-feed/src/lib/metadata-manager.ts @@ -4,7 +4,7 @@ import { FetchAppDetailsDocument, FetchAppDetailsQuery, UpdateAppMetadataDocument, -} from "../../../generated/graphql"; +} from "../../generated/graphql"; gql` mutation UpdateAppMetadata($id: ID!, $input: [MetadataInput!]!) { diff --git a/apps/products-feed/src/modules/app-configuration/app-config-container.ts b/apps/products-feed/src/modules/app-configuration/app-config-container.ts index 6ac58c0..a474fdc 100644 --- a/apps/products-feed/src/modules/app-configuration/app-config-container.ts +++ b/apps/products-feed/src/modules/app-configuration/app-config-container.ts @@ -5,6 +5,11 @@ const getDefaultEmptyUrlConfiguration = (): SellerShopConfig["urlConfiguration"] productStorefrontUrl: "", }); +const getDefaultEmptyPerChannelConfiguration = (): SellerShopConfig => ({ + urlConfiguration: getDefaultEmptyUrlConfiguration(), + s3BucketConfiguration: undefined, //getDefaultEmptyS3BucketConfiguration(), +}); + const getChannelUrlConfiguration = (appConfig: AppConfig | null | undefined) => (channelSlug: string) => { try { @@ -20,15 +25,38 @@ const setChannelUrlConfiguration = (urlConfiguration: SellerShopConfig["urlConfiguration"]) => { const appConfigNormalized = structuredClone(appConfig) ?? { shopConfigPerChannel: {} }; - appConfigNormalized.shopConfigPerChannel[channelSlug] ??= { - urlConfiguration: getDefaultEmptyUrlConfiguration(), - }; + appConfigNormalized.shopConfigPerChannel[channelSlug] ??= + getDefaultEmptyPerChannelConfiguration(); + appConfigNormalized.shopConfigPerChannel[channelSlug].urlConfiguration = urlConfiguration; return appConfigNormalized; }; +const getChannelS3BucketConfiguration = + (appConfig: AppConfig | null | undefined) => (channelSlug: string) => { + try { + return appConfig?.shopConfigPerChannel[channelSlug].s3BucketConfiguration ?? null; + } catch (e) { + return null; + } + }; + +const setChannelS3BucketConfiguration = + (appConfig: AppConfig | null | undefined) => + (channelSlug: string) => + (s3BucketConfiguration: SellerShopConfig["s3BucketConfiguration"]) => { + const appConfigNormalized = structuredClone(appConfig) ?? { shopConfigPerChannel: {} }; + + appConfigNormalized.shopConfigPerChannel[channelSlug].s3BucketConfiguration = + s3BucketConfiguration; + + return appConfigNormalized; + }; + export const AppConfigContainer = { - getChannelUrlConfiguration: getChannelUrlConfiguration, - setChannelUrlConfiguration: setChannelUrlConfiguration, + getChannelUrlConfiguration, + setChannelUrlConfiguration, + getChannelS3BucketConfiguration, + setChannelS3BucketConfiguration, }; diff --git a/apps/products-feed/src/modules/app-configuration/app-config-input-schema.ts b/apps/products-feed/src/modules/app-configuration/app-config-input-schema.ts index 851d8ab..a2c3eeb 100644 --- a/apps/products-feed/src/modules/app-configuration/app-config-input-schema.ts +++ b/apps/products-feed/src/modules/app-configuration/app-config-input-schema.ts @@ -1,15 +1,6 @@ import { z } from "zod"; +import { sellerShopConfigSchema } from "./app-config"; export const appConfigInputSchema = z.object({ - shopConfigPerChannel: z.record( - z.object({ - urlConfiguration: z.object({ - /** - * min() to allow empty strings - */ - storefrontUrl: z.string().min(0), - productStorefrontUrl: z.string().min(0), - }), - }) - ), + shopConfigPerChannel: z.record(sellerShopConfigSchema), }); diff --git a/apps/products-feed/src/modules/app-configuration/app-config.ts b/apps/products-feed/src/modules/app-configuration/app-config.ts index 7923989..0585402 100644 --- a/apps/products-feed/src/modules/app-configuration/app-config.ts +++ b/apps/products-feed/src/modules/app-configuration/app-config.ts @@ -1,9 +1,31 @@ -export interface SellerShopConfig { - urlConfiguration: { - storefrontUrl: string; - productStorefrontUrl: string; - }; -} +import { z } from "zod"; +import { UrlConfiguration } from "./url-configuration"; + +export const s3BucketConfigurationSchema = z.object({ + bucketName: z.string(), + secretAccessKey: z.string(), + accessKeyId: z.string(), + region: z.string(), +}); + +export type S3BucketConfiguration = z.infer; + +export const urlConfigurationSchema = z.object({ + /** + * min() to allow empty strings + */ + storefrontUrl: z.string().min(0), + productStorefrontUrl: z.string().min(0), +}); + +export type UrlConfiguration = z.infer; + +export const sellerShopConfigSchema = z.object({ + urlConfiguration: urlConfigurationSchema, + s3BucketConfiguration: s3BucketConfigurationSchema.optional(), +}); + +export type SellerShopConfig = z.infer; export type ShopConfigPerChannelSlug = Record; diff --git a/apps/products-feed/src/modules/app-configuration/app-configuration.router.ts b/apps/products-feed/src/modules/app-configuration/app-configuration.router.ts index bd6330e..4ff7cfb 100644 --- a/apps/products-feed/src/modules/app-configuration/app-configuration.router.ts +++ b/apps/products-feed/src/modules/app-configuration/app-configuration.router.ts @@ -1,10 +1,11 @@ import { router } from "../trpc/trpc-server"; import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { PrivateMetadataAppConfigurator } from "./app-configurator"; -import { createSettingsManager } from "./metadata-manager"; +import { createSettingsManager } from "../../lib/metadata-manager"; import { createLogger } from "@saleor/apps-shared"; import { appConfigInputSchema } from "./app-config-input-schema"; import { GetAppConfigurationService } from "./get-app-configuration.service"; +import { updateCacheForConfigurations } from "../metadata-cache/update-cache-for-configurations"; export const appConfigurationRouter = router({ fetch: protectedClientProcedure.query(async ({ ctx, input }) => { @@ -30,6 +31,12 @@ export const appConfigurationRouter = router({ ctx.saleorApiUrl ); + await updateCacheForConfigurations({ + client: ctx.apiClient, + configurations: input, + saleorApiUrl: ctx.saleorApiUrl, + }); + await appConfigurator.setConfig(input); return null; diff --git a/apps/products-feed/src/modules/app-configuration/get-app-configuration.service.ts b/apps/products-feed/src/modules/app-configuration/get-app-configuration.service.ts index 5cbbd2c..3e5e0f2 100644 --- a/apps/products-feed/src/modules/app-configuration/get-app-configuration.service.ts +++ b/apps/products-feed/src/modules/app-configuration/get-app-configuration.service.ts @@ -1,5 +1,5 @@ import { PrivateMetadataAppConfigurator } from "./app-configurator"; -import { createSettingsManager } from "./metadata-manager"; +import { createSettingsManager } from "../../lib/metadata-manager"; import { ChannelsFetcher } from "../channels/channels-fetcher"; import { ShopInfoFetcher } from "../shop-info/shop-info-fetcher"; import { FallbackAppConfig } from "./fallback-app-config"; diff --git a/apps/products-feed/src/modules/app-configuration/ui/channels-configuration.tsx b/apps/products-feed/src/modules/app-configuration/ui/channels-configuration.tsx index 849be16..858c3d5 100644 --- a/apps/products-feed/src/modules/app-configuration/ui/channels-configuration.tsx +++ b/apps/products-feed/src/modules/app-configuration/ui/channels-configuration.tsx @@ -10,6 +10,7 @@ import { FeedPreviewCard } from "./feed-preview-card"; import { Instructions } from "./instructions"; import { SideMenu } from "./side-menu"; import { useDashboardNotification } from "@saleor/apps-shared"; +import { S3ConfigurationForm } from "./s3-configuration-form"; const useStyles = makeStyles((theme) => { return { @@ -100,7 +101,7 @@ export const ChannelsConfiguration = () => { { const newConfig = AppConfigContainer.setChannelUrlConfiguration(configurationData)( @@ -114,6 +115,22 @@ export const ChannelsConfiguration = () => { )} channelName={activeChannel?.name ?? activeChannelSlug} /> + { + const newConfig = AppConfigContainer.setChannelS3BucketConfiguration( + configurationData + )(activeChannel.slug)(data); + + mutate(newConfig); + }} + initialData={AppConfigContainer.getChannelS3BucketConfiguration(configurationData)( + activeChannel.slug + )} + channelName={activeChannel?.name ?? activeChannelSlug} + /> {saveError && {saveError.message}} diff --git a/apps/products-feed/src/modules/app-configuration/ui/s3-configuration-form.tsx b/apps/products-feed/src/modules/app-configuration/ui/s3-configuration-form.tsx new file mode 100644 index 0000000..d0a130b --- /dev/null +++ b/apps/products-feed/src/modules/app-configuration/ui/s3-configuration-form.tsx @@ -0,0 +1,68 @@ +import { S3BucketConfiguration, SellerShopConfig } from "../app-config"; +import { useForm } from "react-hook-form"; +import { TextField, TextFieldProps, Typography } from "@material-ui/core"; +import { Button, makeStyles } from "@saleor/macaw-ui"; +import React from "react"; +import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge"; + +const useStyles = makeStyles({ + field: { + marginBottom: 20, + }, + form: { + padding: 20, + }, + channelName: { + fontFamily: "monospace", + cursor: "pointer", + }, +}); + +type Props = { + channelSlug: string; + channelName: string; + channelID: string; + onSubmit(data: S3BucketConfiguration): Promise; + initialData?: S3BucketConfiguration | null; +}; + +export const S3ConfigurationForm = (props: Props) => { + const { register, handleSubmit } = useForm({ + defaultValues: props.initialData ?? undefined, + }); + const styles = useStyles(); + const { appBridge } = useAppBridge(); + + const CommonFieldProps: TextFieldProps = { + className: styles.field, + fullWidth: true, + }; + + return ( +
{ + props.onSubmit(data); + })} + className={styles.form} + > + + S3 storage + + + + + + + + + + + + ); +}; diff --git a/apps/products-feed/src/modules/file-storage/s3/const.ts b/apps/products-feed/src/modules/file-storage/s3/const.ts new file mode 100644 index 0000000..e079436 --- /dev/null +++ b/apps/products-feed/src/modules/file-storage/s3/const.ts @@ -0,0 +1,6 @@ +/* + * AWS multipart uploads require a minimum file size of 5 MB. + * https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html + */ + +export const MULTI_PART_SIZE_THRESHOLD = 5 * 1024 * 1024; diff --git a/apps/products-feed/src/modules/file-storage/s3/create-s3-client-from-configuration.ts b/apps/products-feed/src/modules/file-storage/s3/create-s3-client-from-configuration.ts new file mode 100644 index 0000000..0be4e3e --- /dev/null +++ b/apps/products-feed/src/modules/file-storage/s3/create-s3-client-from-configuration.ts @@ -0,0 +1,16 @@ +import { S3Client } from "@aws-sdk/client-s3"; +import { S3BucketConfiguration } from "../../app-configuration/app-config"; + +export const createS3ClientFromConfiguration = ({ + accessKeyId, + secretAccessKey, + region, +}: S3BucketConfiguration) => { + return new S3Client({ + credentials: { + accessKeyId: accessKeyId, + secretAccessKey: secretAccessKey, + }, + region: region, + }); +}; diff --git a/apps/products-feed/src/modules/file-storage/s3/get-file-details.ts b/apps/products-feed/src/modules/file-storage/s3/get-file-details.ts new file mode 100644 index 0000000..db34371 --- /dev/null +++ b/apps/products-feed/src/modules/file-storage/s3/get-file-details.ts @@ -0,0 +1,17 @@ +import { GetObjectAttributesCommand, S3Client } from "@aws-sdk/client-s3"; + +export interface GetFileDetailsArgs { + s3Client: S3Client; + fileName: string; + bucketName: string; +} + +export const getFileDetails = async ({ s3Client, bucketName, fileName }: GetFileDetailsArgs) => { + return await s3Client.send( + new GetObjectAttributesCommand({ + Bucket: bucketName, + Key: fileName, + ObjectAttributes: ["ObjectParts"], + }) + ); +}; diff --git a/apps/products-feed/src/modules/file-storage/s3/upload-file.ts b/apps/products-feed/src/modules/file-storage/s3/upload-file.ts new file mode 100644 index 0000000..effb981 --- /dev/null +++ b/apps/products-feed/src/modules/file-storage/s3/upload-file.ts @@ -0,0 +1,18 @@ +import { S3Client } from "@aws-sdk/client-s3"; +import { UploadMultiPart } from "./upload-multi-part"; +import { UploadSinglePart } from "./upload-single-part"; +import { MULTI_PART_SIZE_THRESHOLD } from "./const"; + +export interface UploadFileArgs { + s3Client: S3Client; + fileName: string; + buffer: Buffer; + bucketName: string; +} + +export const uploadFile = async (args: UploadFileArgs) => { + if (args.buffer.length > MULTI_PART_SIZE_THRESHOLD) { + return await UploadMultiPart(args); + } + return await UploadSinglePart(args); +}; diff --git a/apps/products-feed/src/modules/file-storage/s3/upload-multi-part.ts b/apps/products-feed/src/modules/file-storage/s3/upload-multi-part.ts new file mode 100644 index 0000000..283946b --- /dev/null +++ b/apps/products-feed/src/modules/file-storage/s3/upload-multi-part.ts @@ -0,0 +1,94 @@ +import { + AbortMultipartUploadCommand, + CompleteMultipartUploadCommand, + CreateMultipartUploadCommand, + UploadPartCommand, +} from "@aws-sdk/client-s3"; +import { UploadFileArgs } from "./upload-file"; +import { createLogger } from "@saleor/apps-shared"; +import { MULTI_PART_SIZE_THRESHOLD } from "./const"; + +/* + * Code based on S3 docs: + * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/javascript_s3_code_examples.html + */ + +const logger = createLogger({ + fn: "UploadMultiPart", +}); + +export const UploadMultiPart = async ({ + s3Client, + fileName, + buffer, + bucketName, +}: UploadFileArgs) => { + let uploadId; + + try { + const multipartUpload = await s3Client.send( + new CreateMultipartUploadCommand({ + Bucket: bucketName, + Key: fileName, + }) + ); + + uploadId = multipartUpload.UploadId; + + const uploadPromises = []; + // Multipart uploads require a minimum size of 5 MB per part. + const partSize = MULTI_PART_SIZE_THRESHOLD; + const numberOfParts = Math.ceil(buffer.length / partSize); + + // Upload each part. + for (let i = 0; i < numberOfParts; i++) { + const start = i * partSize; + const end = start + partSize; + + uploadPromises.push( + s3Client + .send( + new UploadPartCommand({ + Bucket: bucketName, + Key: fileName, + UploadId: uploadId, + Body: buffer.subarray(start, end), + PartNumber: i + 1, + }) + ) + .then((d) => { + logger.debug(`Part ${i + 1}/${numberOfParts} uploaded`); + return d; + }) + ); + } + + const uploadResults = await Promise.all(uploadPromises); + + return await s3Client.send( + new CompleteMultipartUploadCommand({ + Bucket: bucketName, + Key: fileName, + UploadId: uploadId, + MultipartUpload: { + Parts: uploadResults.map(({ ETag }, i) => ({ + ETag, + PartNumber: i + 1, + })), + }, + }) + ); + } catch (err) { + logger.error(err); + + if (uploadId) { + const abortCommand = new AbortMultipartUploadCommand({ + Bucket: bucketName, + Key: fileName, + UploadId: uploadId, + }); + + await s3Client.send(abortCommand); + } + } +}; diff --git a/apps/products-feed/src/modules/file-storage/s3/upload-single-part.ts b/apps/products-feed/src/modules/file-storage/s3/upload-single-part.ts new file mode 100644 index 0000000..a0b29d6 --- /dev/null +++ b/apps/products-feed/src/modules/file-storage/s3/upload-single-part.ts @@ -0,0 +1,17 @@ +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import { UploadFileArgs } from "./upload-file"; + +export const UploadSinglePart = async ({ + s3Client, + fileName, + buffer, + bucketName, +}: UploadFileArgs) => { + return await s3Client.send( + new PutObjectCommand({ + Bucket: bucketName, + Body: buffer, + Key: fileName, + }) + ); +}; diff --git a/apps/products-feed/src/modules/file-storage/s3/urls-and-names.ts b/apps/products-feed/src/modules/file-storage/s3/urls-and-names.ts new file mode 100644 index 0000000..ce80a0c --- /dev/null +++ b/apps/products-feed/src/modules/file-storage/s3/urls-and-names.ts @@ -0,0 +1,28 @@ +import { S3BucketConfiguration } from "../../app-configuration/app-config"; + +interface GetDownloadUrlArgs { + s3BucketConfiguration: S3BucketConfiguration; + saleorApiUrl: string; + channel: string; +} + +export const getDownloadUrl = ({ + s3BucketConfiguration, + saleorApiUrl, + channel, +}: GetDownloadUrlArgs) => { + return `https://${s3BucketConfiguration.bucketName}.s3.${ + s3BucketConfiguration.region + }.amazonaws.com/${getFileName({ saleorApiUrl, channel })}`; +}; + +interface GetFileNameArgs { + saleorApiUrl: string; + channel: string; +} + +export const getFileName = ({ saleorApiUrl, channel }: GetFileNameArgs) => { + const apiUrl = new URL(saleorApiUrl); + + return `${apiUrl.hostname}/${channel}/google.xml`; +}; diff --git a/apps/products-feed/src/modules/metadata-cache/cache-configurator.ts b/apps/products-feed/src/modules/metadata-cache/cache-configurator.ts new file mode 100644 index 0000000..4ebce03 --- /dev/null +++ b/apps/products-feed/src/modules/metadata-cache/cache-configurator.ts @@ -0,0 +1,31 @@ +import { SettingsManager } from "@saleor/app-sdk/settings-manager"; + +export class CacheConfigurator { + private metadataKeyPrefix = "cursor-cache-"; + + constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {} + + get({ channel }: { channel: string }): Promise { + return this.metadataManager + .get(this.metadataKeyPrefix + channel, this.saleorApiUrl) + .then((data) => { + if (!data) { + return undefined; + } + + try { + return JSON.parse(data); + } catch (e) { + throw new Error("Invalid metadata value, can't be parsed"); + } + }); + } + + set({ channel, value }: { channel: string; value: string[] }): Promise { + return this.metadataManager.set({ + key: this.metadataKeyPrefix + channel, + value: JSON.stringify(value), + domain: this.saleorApiUrl, + }); + } +} diff --git a/apps/products-feed/src/modules/metadata-cache/update-cache-for-configurations.ts b/apps/products-feed/src/modules/metadata-cache/update-cache-for-configurations.ts new file mode 100644 index 0000000..36910e5 --- /dev/null +++ b/apps/products-feed/src/modules/metadata-cache/update-cache-for-configurations.ts @@ -0,0 +1,35 @@ +import { createLogger } from "@saleor/apps-shared"; +import { CacheConfigurator } from "./cache-configurator"; +import { createSettingsManager } from "../../lib/metadata-manager"; +import { getCursors } from "../../lib/google-feed/fetch-product-data"; +import { Client } from "urql"; +import { z } from "zod"; +import { appConfigInputSchema } from "../app-configuration/app-config-input-schema"; + +interface UpdateCacheForConfigurationsArgs { + client: Client; + saleorApiUrl: string; + configurations: z.infer; +} + +export const updateCacheForConfigurations = async ({ + client, + configurations, + saleorApiUrl, +}: UpdateCacheForConfigurationsArgs) => { + const logger = createLogger({ saleorApiUrl: saleorApiUrl }); + + logger.debug("Updating the cursor cache"); + const cache = new CacheConfigurator(createSettingsManager(client), saleorApiUrl); + + const channelsToUpdate = Object.keys(configurations.shopConfigPerChannel); + + const cacheUpdatePromises = channelsToUpdate.map(async (channel) => { + const cursors = await getCursors({ client, channel }); + + await cache.set({ channel, value: cursors }); + }); + + await Promise.all(cacheUpdatePromises); + logger.debug("Cursor cache updated"); +}; diff --git a/apps/products-feed/src/pages/api/feed/[url]/[channel]/google.xml.ts b/apps/products-feed/src/pages/api/feed/[url]/[channel]/google.xml.ts index 353ddc2..f6e982d 100644 --- a/apps/products-feed/src/pages/api/feed/[url]/[channel]/google.xml.ts +++ b/apps/products-feed/src/pages/api/feed/[url]/[channel]/google.xml.ts @@ -7,6 +7,14 @@ import { fetchProductData } from "../../../../../lib/google-feed/fetch-product-d import { getGoogleFeedSettings } from "../../../../../lib/google-feed/get-google-feed-settings"; import { generateGoogleXmlFeed } from "../../../../../lib/google-feed/generate-google-xml-feed"; import { fetchShopData } from "../../../../../lib/google-feed/fetch-shop-data"; +import { CacheConfigurator } from "../../../../../modules/metadata-cache/cache-configurator"; +import { createSettingsManager } from "../../../../../lib/metadata-manager"; +import { createClient } from "../../../../../lib/create-graphq-client"; +import { uploadFile } from "../../../../../modules/file-storage/s3/upload-file"; +import { createS3ClientFromConfiguration } from "../../../../../modules/file-storage/s3/create-s3-client-from-configuration"; +import { S3BucketConfiguration } from "../../../../../modules/app-configuration/app-config"; +import { getFileDetails } from "../../../../../modules/file-storage/s3/get-file-details"; +import { getDownloadUrl, getFileName } from "../../../../../modules/file-storage/s3/urls-and-names"; // By default we cache the feed for 5 minutes. This can be changed by setting the FEED_CACHE_MAX_AGE const FEED_CACHE_MAX_AGE = process.env.FEED_CACHE_MAX_AGE @@ -60,12 +68,14 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => { let storefrontUrl: string; let productStorefrontUrl: string; + let bucketConfiguration: S3BucketConfiguration | undefined; try { const settings = await getGoogleFeedSettings({ authData, channel }); storefrontUrl = settings.storefrontUrl; productStorefrontUrl = settings.productStorefrontUrl; + bucketConfiguration = settings.s3BucketConfiguration; } catch (error) { logger.warn("The application has not been configured"); return res @@ -86,11 +96,65 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(500).json({ error: "Could not fetch the shop details" }); } + if (bucketConfiguration) { + logger.debug("Bucket configuration found, checking if the feed has been generated recently"); + const s3Client = createS3ClientFromConfiguration(bucketConfiguration); + const fileName = getFileName({ + saleorApiUrl: authData.saleorApiUrl, + channel, + }); + + const feedLastModificationDate = await getFileDetails({ + s3Client, + bucketName: bucketConfiguration.bucketName, + fileName, + }) + .then((data) => data.LastModified) + // If the file does not exist, error is thrown and we can ignore it + .catch(() => undefined); + + if (feedLastModificationDate) { + logger.debug("Feed has been generated previously, checking the last modification date"); + + const secondsSinceLastModification = (Date.now() - feedLastModificationDate.getTime()) / 1000; + + if (secondsSinceLastModification < FEED_CACHE_MAX_AGE) { + logger.debug("Feed has been generated recently, returning the last version"); + + const downloadUrl = getDownloadUrl({ + s3BucketConfiguration: bucketConfiguration, + saleorApiUrl: authData.saleorApiUrl, + channel, + }); + + return res.redirect(downloadUrl); + } + + logger.debug("Feed is outdated, generating a new one"); + } + } + + logger.debug("Generating a new feed"); + + const cacheClient = createClient(authData.saleorApiUrl, async () => + Promise.resolve({ token: authData.token }) + ); + + if (!cacheClient) { + logger.error("Can't create the gql client"); + return res.status(500).end(); + } + + // get cached cursors + const cache = new CacheConfigurator(createSettingsManager(cacheClient), authData.saleorApiUrl); + + const cursors = await cache.get({ channel }); + // TODO: instead of separate variants, use group id https://support.google.com/merchants/answer/6324507?hl=en let productVariants: GoogleFeedProductVariantFragment[] = []; try { - productVariants = await fetchProductData({ client, channel }); + productVariants = await fetchProductData({ client, channel, cursors }); } catch (error) { logger.error(error); return res.status(400).end(); @@ -108,10 +172,43 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => { logger.debug("Feed generated. Returning formatted XML"); - res.setHeader("Content-Type", "text/xml"); - res.setHeader("Cache-Control", `s-maxage=${FEED_CACHE_MAX_AGE}`); - res.write(xmlContent); - res.end(); + if (!bucketConfiguration) { + logger.debug("Bucket configuration not found, returning feed directly"); + + res.setHeader("Content-Type", "text/xml"); + res.setHeader("Cache-Control", `s-maxage=${FEED_CACHE_MAX_AGE}`); + res.write(xmlContent); + res.end(); + return; + } + + logger.debug("Bucket configuration found, uploading the feed to S3"); + const s3Client = createS3ClientFromConfiguration(bucketConfiguration); + const fileName = getFileName({ + saleorApiUrl: authData.saleorApiUrl, + channel, + }); + + try { + await uploadFile({ + s3Client, + bucketName: bucketConfiguration.bucketName, + buffer: Buffer.from(xmlContent), + fileName, + }); + + logger.debug("Feed uploaded to S3, redirecting the download URL"); + const downloadUrl = getDownloadUrl({ + s3BucketConfiguration: bucketConfiguration, + saleorApiUrl: authData.saleorApiUrl, + channel, + }); + + return res.redirect(downloadUrl); + } catch (error) { + logger.error("Could not upload the feed to S3"); + return res.status(500).json({ error: "Could not upload the feed to S3" }); + } }; export default handler; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40587fc..e73474c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1070,6 +1070,9 @@ importers: apps/products-feed: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.332.0 + version: 3.332.0 '@hookform/resolvers': specifier: ^2.9.10 version: 2.9.11(react-hook-form@7.43.1) @@ -1925,6 +1928,955 @@ packages: - encoding dev: true + /@aws-crypto/crc32@3.0.0: + resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.329.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/crc32c@3.0.0: + resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.329.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/ie11-detection@3.0.0: + resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha1-browser@3.0.0: + resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-browser@3.0.0: + resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-js@3.0.0: + resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.329.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/supports-web-crypto@3.0.0: + resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/util@3.0.0: + resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} + dependencies: + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-sdk/abort-controller@3.329.0: + resolution: {integrity: sha512-hzrjPNQcJoSPe0oS20V5i98oiEZSM3mKNiR6P3xHTHTPI/F23lyjGZ+/CSkCmJbSWfGZ5sHZZcU6AWuS7xBdTw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/chunked-blob-reader@3.310.0: + resolution: {integrity: sha512-CrJS3exo4mWaLnWxfCH+w88Ou0IcAZSIkk4QbmxiHl/5Dq705OLoxf4385MVyExpqpeVJYOYQ2WaD8i/pQZ2fg==} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/client-s3@3.332.0: + resolution: {integrity: sha512-4AkbBPGjFkIvN15l9uDHcry3kwMknpl0b7mqFaNQqQJR2OyFJnr7US/KyeTjwijJAuU+f7lKz8QMTtBcghJm3w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 3.0.0 + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.332.0 + '@aws-sdk/config-resolver': 3.329.0 + '@aws-sdk/credential-provider-node': 3.332.0 + '@aws-sdk/eventstream-serde-browser': 3.329.0 + '@aws-sdk/eventstream-serde-config-resolver': 3.329.0 + '@aws-sdk/eventstream-serde-node': 3.329.0 + '@aws-sdk/fetch-http-handler': 3.329.0 + '@aws-sdk/hash-blob-browser': 3.329.0 + '@aws-sdk/hash-node': 3.329.0 + '@aws-sdk/hash-stream-node': 3.329.0 + '@aws-sdk/invalid-dependency': 3.329.0 + '@aws-sdk/md5-js': 3.329.0 + '@aws-sdk/middleware-bucket-endpoint': 3.329.0 + '@aws-sdk/middleware-content-length': 3.329.0 + '@aws-sdk/middleware-endpoint': 3.329.0 + '@aws-sdk/middleware-expect-continue': 3.329.0 + '@aws-sdk/middleware-flexible-checksums': 3.331.0 + '@aws-sdk/middleware-host-header': 3.329.0 + '@aws-sdk/middleware-location-constraint': 3.329.0 + '@aws-sdk/middleware-logger': 3.329.0 + '@aws-sdk/middleware-recursion-detection': 3.329.0 + '@aws-sdk/middleware-retry': 3.329.0 + '@aws-sdk/middleware-sdk-s3': 3.329.0 + '@aws-sdk/middleware-serde': 3.329.0 + '@aws-sdk/middleware-signing': 3.329.0 + '@aws-sdk/middleware-ssec': 3.329.0 + '@aws-sdk/middleware-stack': 3.329.0 + '@aws-sdk/middleware-user-agent': 3.332.0 + '@aws-sdk/node-config-provider': 3.329.0 + '@aws-sdk/node-http-handler': 3.329.0 + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/signature-v4-multi-region': 3.329.0 + '@aws-sdk/smithy-client': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/url-parser': 3.329.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.329.0 + '@aws-sdk/util-defaults-mode-node': 3.329.0 + '@aws-sdk/util-endpoints': 3.332.0 + '@aws-sdk/util-retry': 3.329.0 + '@aws-sdk/util-stream-browser': 3.329.0 + '@aws-sdk/util-stream-node': 3.331.0 + '@aws-sdk/util-user-agent-browser': 3.329.0 + '@aws-sdk/util-user-agent-node': 3.329.0 + '@aws-sdk/util-utf8': 3.310.0 + '@aws-sdk/util-waiter': 3.329.0 + '@aws-sdk/xml-builder': 3.310.0 + fast-xml-parser: 4.1.2 + tslib: 2.5.0 + transitivePeerDependencies: + - '@aws-sdk/signature-v4-crt' + - aws-crt + dev: false + + /@aws-sdk/client-sso-oidc@3.332.0: + resolution: {integrity: sha512-tz8k8Yqm4TScIfit0Tum2zWAq1md+gZKr747CSixd4Zwcp7Vwh75cRoL7Rz1ZHSEn1Yo983MWREevVez3SubLw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.329.0 + '@aws-sdk/fetch-http-handler': 3.329.0 + '@aws-sdk/hash-node': 3.329.0 + '@aws-sdk/invalid-dependency': 3.329.0 + '@aws-sdk/middleware-content-length': 3.329.0 + '@aws-sdk/middleware-endpoint': 3.329.0 + '@aws-sdk/middleware-host-header': 3.329.0 + '@aws-sdk/middleware-logger': 3.329.0 + '@aws-sdk/middleware-recursion-detection': 3.329.0 + '@aws-sdk/middleware-retry': 3.329.0 + '@aws-sdk/middleware-serde': 3.329.0 + '@aws-sdk/middleware-stack': 3.329.0 + '@aws-sdk/middleware-user-agent': 3.332.0 + '@aws-sdk/node-config-provider': 3.329.0 + '@aws-sdk/node-http-handler': 3.329.0 + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/smithy-client': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/url-parser': 3.329.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.329.0 + '@aws-sdk/util-defaults-mode-node': 3.329.0 + '@aws-sdk/util-endpoints': 3.332.0 + '@aws-sdk/util-retry': 3.329.0 + '@aws-sdk/util-user-agent-browser': 3.329.0 + '@aws-sdk/util-user-agent-node': 3.329.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.332.0: + resolution: {integrity: sha512-4q1Nko8M6YVANdEiLYvdv1qb00j4xN4ppE/6d4xpGp7DxHYlm0GA762h0/TR2dun+2I+SMnwj4Fv6BxOmzBaEw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.329.0 + '@aws-sdk/fetch-http-handler': 3.329.0 + '@aws-sdk/hash-node': 3.329.0 + '@aws-sdk/invalid-dependency': 3.329.0 + '@aws-sdk/middleware-content-length': 3.329.0 + '@aws-sdk/middleware-endpoint': 3.329.0 + '@aws-sdk/middleware-host-header': 3.329.0 + '@aws-sdk/middleware-logger': 3.329.0 + '@aws-sdk/middleware-recursion-detection': 3.329.0 + '@aws-sdk/middleware-retry': 3.329.0 + '@aws-sdk/middleware-serde': 3.329.0 + '@aws-sdk/middleware-stack': 3.329.0 + '@aws-sdk/middleware-user-agent': 3.332.0 + '@aws-sdk/node-config-provider': 3.329.0 + '@aws-sdk/node-http-handler': 3.329.0 + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/smithy-client': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/url-parser': 3.329.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.329.0 + '@aws-sdk/util-defaults-mode-node': 3.329.0 + '@aws-sdk/util-endpoints': 3.332.0 + '@aws-sdk/util-retry': 3.329.0 + '@aws-sdk/util-user-agent-browser': 3.329.0 + '@aws-sdk/util-user-agent-node': 3.329.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.332.0: + resolution: {integrity: sha512-uVobnXIzMcEhwBDyk6iOt36N/TRNI8hwq7MQugjYGj7Inma9g4vnR09hXJ24HxyKCoVUoIgMbEguQ43+/+uvDQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.329.0 + '@aws-sdk/credential-provider-node': 3.332.0 + '@aws-sdk/fetch-http-handler': 3.329.0 + '@aws-sdk/hash-node': 3.329.0 + '@aws-sdk/invalid-dependency': 3.329.0 + '@aws-sdk/middleware-content-length': 3.329.0 + '@aws-sdk/middleware-endpoint': 3.329.0 + '@aws-sdk/middleware-host-header': 3.329.0 + '@aws-sdk/middleware-logger': 3.329.0 + '@aws-sdk/middleware-recursion-detection': 3.329.0 + '@aws-sdk/middleware-retry': 3.329.0 + '@aws-sdk/middleware-sdk-sts': 3.329.0 + '@aws-sdk/middleware-serde': 3.329.0 + '@aws-sdk/middleware-signing': 3.329.0 + '@aws-sdk/middleware-stack': 3.329.0 + '@aws-sdk/middleware-user-agent': 3.332.0 + '@aws-sdk/node-config-provider': 3.329.0 + '@aws-sdk/node-http-handler': 3.329.0 + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/smithy-client': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/url-parser': 3.329.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-body-length-browser': 3.310.0 + '@aws-sdk/util-body-length-node': 3.310.0 + '@aws-sdk/util-defaults-mode-browser': 3.329.0 + '@aws-sdk/util-defaults-mode-node': 3.329.0 + '@aws-sdk/util-endpoints': 3.332.0 + '@aws-sdk/util-retry': 3.329.0 + '@aws-sdk/util-user-agent-browser': 3.329.0 + '@aws-sdk/util-user-agent-node': 3.329.0 + '@aws-sdk/util-utf8': 3.310.0 + fast-xml-parser: 4.1.2 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/config-resolver@3.329.0: + resolution: {integrity: sha512-Oj6eiT3q+Jn685yvUrfRi8PhB3fb81hasJqdrsEivA8IP8qAgnVUTJzXsh8O2UX8UM2MF6A1gTgToSgneJuw2Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-config-provider': 3.310.0 + '@aws-sdk/util-middleware': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-env@3.329.0: + resolution: {integrity: sha512-B4orC9hMt9hG82vAR0TAnQqjk6cFDbO2S14RdzUj2n2NPlGWW4Blkv3NTo86K0lq011VRhtqaLcuTwn5EJD5Sg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-imds@3.329.0: + resolution: {integrity: sha512-ggPlnd7QROPTid0CwT01TYYGvstRRTpzTGsQ/B31wkh30IrRXE81W3S4xrOYuqQD3u0RnflSxnvhs+EayJEYjg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/node-config-provider': 3.329.0 + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/url-parser': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-ini@3.332.0: + resolution: {integrity: sha512-DTW6d6rcqizPVyvcIrwvxecQ7e5GONtVc5Wyf0RTfqf41sDOVZYmn6G+zEFSpBLW0975uZbJS0lyLWtJe2VujQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.329.0 + '@aws-sdk/credential-provider-imds': 3.329.0 + '@aws-sdk/credential-provider-process': 3.329.0 + '@aws-sdk/credential-provider-sso': 3.332.0 + '@aws-sdk/credential-provider-web-identity': 3.329.0 + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/shared-ini-file-loader': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.332.0: + resolution: {integrity: sha512-KkBayS9k4WyJTvC86ngeRM+RmWxNCS1BHvudkR6PLXfnsNPDzxySDVY0UgxVhbNYDYsO561fXZt9ccpKyVWjgg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.329.0 + '@aws-sdk/credential-provider-imds': 3.329.0 + '@aws-sdk/credential-provider-ini': 3.332.0 + '@aws-sdk/credential-provider-process': 3.329.0 + '@aws-sdk/credential-provider-sso': 3.332.0 + '@aws-sdk/credential-provider-web-identity': 3.329.0 + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/shared-ini-file-loader': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-process@3.329.0: + resolution: {integrity: sha512-5oO220qoFc2pMdZDQa6XN/mVhp669I3+LqMbbscGtX/UgLJPSOb7YzPld9Wjv12L5rf+sD3G1PF3LZXO0vKLFA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/shared-ini-file-loader': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/credential-provider-sso@3.332.0: + resolution: {integrity: sha512-SaKXl48af3n6LRitcaEqbeg1YDXwQ0A5QziC1xQyYPraEIj3IZ/GyTjx04Lo2jxNYHuEOE8u4aTw1+IK1GDKbg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.332.0 + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/shared-ini-file-loader': 3.329.0 + '@aws-sdk/token-providers': 3.332.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-web-identity@3.329.0: + resolution: {integrity: sha512-lcEibZD7AlutCacpQ6DyNUqElZJDq+ylaIo5a8MH9jGh7Pg2WpDg0Sy+B6FbGCkVn4eIjdHxeX54JM245nhESg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-codec@3.329.0: + resolution: {integrity: sha512-1r+6MNfye0za35FNLxMR5V9zpKY1lyzwySyu7o7aj8lnStBaCcjOEe7iHboP/z3DH73KJbxR++O2N+UC/XHFrg==} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-hex-encoding': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-browser@3.329.0: + resolution: {integrity: sha512-oWFSn4o6sxlbFF0AIuDJYf7N0fkiOyWvYgRW3VTX9FSbd66f/KnDspdxIasaDPDUzJl5YRMwUvQbPWw8y9ZQfQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-serde-universal': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-config-resolver@3.329.0: + resolution: {integrity: sha512-iQguqvTtxWXAIniaWmmAO0Qy8080fqnS309p9jbYzz7KaT90sNSCX+CxGFHPy5F0QY36uklDdHn1d1fwWTZciA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-node@3.329.0: + resolution: {integrity: sha512-+DFia0wdZiHpdOKjBcl1baZjtzPKf4U4MvOpsUpC6CeW1kSy0hoikKzJstNvRb1qxrTSamElT4gKkMHxxVhPBQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-serde-universal': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/eventstream-serde-universal@3.329.0: + resolution: {integrity: sha512-n9UzW6HKAhVD5wuz3FMC1ew3VI/vUvRSPXGUpKReMiR2z+YyjmuW8UM4nn7q6i7A/I4QHBt1TC/ax/J2yupgPg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-codec': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/fetch-http-handler@3.329.0: + resolution: {integrity: sha512-9jfIeJhYCcTX4ScXOueRTB3S/tVce0bRsKxKDP0PnTxnGYOwKXoM9lAPmiYItzYmQ/+QzjTI8xfkA9Usz2SK/Q==} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/querystring-builder': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-base64': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/hash-blob-browser@3.329.0: + resolution: {integrity: sha512-F5HwXYYSpJtUJqmCRKbz/xwDdOyxKpu69TlfsliECLvAQiQGMh2GO1wGm7grolgTROVVqLYRKk2TSJl/WBg1pw==} + dependencies: + '@aws-sdk/chunked-blob-reader': 3.310.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/hash-node@3.329.0: + resolution: {integrity: sha512-6RmnWXNWpi7yAs0oRDQlkMn2wfXOStr/8kTCgiAiqrk1KopGSBkC2veKiKRSfv02FTd1yV/ISqYNIRqW1VLyxg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-buffer-from': 3.310.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/hash-stream-node@3.329.0: + resolution: {integrity: sha512-blSZcb/hJyw3c1bH2Hc1aRoRgruNhRK/qc2svq5kXQFW+qBI5O4fwJayKSdo62/Wh2ejR/N06teYQ9haQLVJEA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/invalid-dependency@3.329.0: + resolution: {integrity: sha512-UXynGusDxN/HxLma5ByJ7u+XnuMd47NbHOjJgYsaAjb1CVZT7hEPXOB+mcZ+Ku7To5SCOKu2QbRn7m4bGespBg==} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/is-array-buffer@3.310.0: + resolution: {integrity: sha512-urnbcCR+h9NWUnmOtet/s4ghvzsidFmspfhYaHAmSRdy9yDjdjBJMFjjsn85A1ODUktztm+cVncXjQ38WCMjMQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/md5-js@3.329.0: + resolution: {integrity: sha512-newSeHd+CO2hNmXhQOrUk5Y1hH7BsJ5J4IldcqHKY93UwWqvQNiepRowSa2bV5EuS1qx3kfXhD66PFNRprrIlQ==} + dependencies: + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-bucket-endpoint@3.329.0: + resolution: {integrity: sha512-h3/JdK+FmJ/nxLcd8QciJYLy0B4QRsYqqxSffXJ7DYlDjEhUgvVpfGdVgAYHrTtOP8rHSG/K7l7iY7QqTaZpuw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-arn-parser': 3.310.0 + '@aws-sdk/util-config-provider': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-content-length@3.329.0: + resolution: {integrity: sha512-7kCd+CvY/4KbyXB0uyL7jCwPjMi2yERMALFdEH9dsUciwmxIQT6eSc4aF6wImC4UrbafaqmXvvHErABKMVBTKA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-endpoint@3.329.0: + resolution: {integrity: sha512-hdJRoNdCM0BT4W+rrtee+kfFRgGPGXQDgtbIQlf/FuuuYz2sdef7/SYWr0mxuncnVBW5WkYSPP8h6q07whSKbg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-serde': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/url-parser': 3.329.0 + '@aws-sdk/util-middleware': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-expect-continue@3.329.0: + resolution: {integrity: sha512-E/Jp2KijdR/BwF4s899xcSN4/bbHqYznwmBRL5PiHI+HImA6aZ11qTP8kPt5U5p0l2j5iTmW3FpMnByQKJP5Dw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-flexible-checksums@3.331.0: + resolution: {integrity: sha512-rdRa4yvyqSQ/HDCh4p1Glv8Y/uRNuIwmOG4nDuL6/GYK1BQdpUpbgrhsszPormku10SnbAdsaWGmVhy3qlUSCQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@aws-crypto/crc32c': 3.0.0 + '@aws-sdk/is-array-buffer': 3.310.0 + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-host-header@3.329.0: + resolution: {integrity: sha512-JrHeUdTIpTCfXDo9JpbAbZTS1x4mt63CCytJRq0mpWp+FlP9hjckBcNxWdR/wSKEzP9pDRnTri638BOwWH7O8w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-location-constraint@3.329.0: + resolution: {integrity: sha512-iUTkyXyhchqoEPkdMZSkHhRQmXe0El1+r9oOw8y9JN6IY0T1bnaqUlerGXzb/tQUeENk9OXYuvDHExegHjEWug==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-logger@3.329.0: + resolution: {integrity: sha512-lKeeTXsYC1NiwmxrXsZepcwNXPoQxTNNbeD1qaCELPGK2cJlrGoeAP2YRWzpwO2kNZWrDLaGAPT/EUEhqw+d1w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.329.0: + resolution: {integrity: sha512-0/TYOJwrj1Z8s+Y7thibD23hggBq/K/01NwPk32CwWG/G+1vWozs5DefknEl++w0vuV+39pkY4KHI8m/+wOCpg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-retry@3.329.0: + resolution: {integrity: sha512-cB3D7GlhHUcHGOlygOYxD9cPhwsTYEAMcohK38An8+RHNp6VQEWezzLFCmHVKUSeCQ+wkjZfPA40jOG0rbjSgQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/service-error-classification': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-middleware': 3.329.0 + '@aws-sdk/util-retry': 3.329.0 + tslib: 2.5.0 + uuid: 8.3.2 + dev: false + + /@aws-sdk/middleware-sdk-s3@3.329.0: + resolution: {integrity: sha512-Uo8dLXLDpOb3BnLVl0mkTPiVXlNzNGOXOVtpihvYhF2Z+hGFJW1Ro3aUDbVEsFHu753r2Lss4dLiq1fzREeBKA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-arn-parser': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.329.0: + resolution: {integrity: sha512-bqtZuhkH8pANb2Gb4FEM1p27o+BoDBmVhEWm8sWH+APsyOor3jc6eUG2GxkfoO6D5tGNIuyCC/GuvW9XDIe4Kg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-serde@3.329.0: + resolution: {integrity: sha512-tvM9NdPuRPCozPjTGNOeYZeLlyx3BcEyajrkRorCRf1YzG/mXdB6I1stote7i4q1doFtYTz0sYL8bqW3LUPn9A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-signing@3.329.0: + resolution: {integrity: sha512-bL1nI+EUcF5B1ipwDXxiKL+Uw02Mbt/TNX54PbzunBGZIyO6DZG/H+M3U296bYbvPlwlZhp26O830g6K7VEWsA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/signature-v4': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-middleware': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-ssec@3.329.0: + resolution: {integrity: sha512-XtDA/P2Sf79scu4a7tG77QC3VLtAGq/pit73x+qwctnI4gBgZlQ+FpE15d89ulntd7rIaD4v6tVU0bAg/L3PIQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-stack@3.329.0: + resolution: {integrity: sha512-2huFLhJ45td2nuiIOjpc9JKJbFNn5CYmw9U8YDITTcydpteRN62CzCpeqroDvF89VOLWxh0ZFtuLCGUr7liSWQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/middleware-user-agent@3.332.0: + resolution: {integrity: sha512-rSL1xP4QmcMOsunN1p5ZDR9GT3vvoSCnYa4iPvMSjP8Jx7l4ff/aVctwfZkMs/up12+68Jqwj4TvtaCvCFXdUA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-endpoints': 3.332.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/node-config-provider@3.329.0: + resolution: {integrity: sha512-hg9rGNlkzh8aeR/sQbijrkFx2BIO53j4Z6qDxPNWwSGpl05jri1VHxHx2HZMwgbY6Zy/DSguETN/BL8vdFqyLg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/shared-ini-file-loader': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/node-http-handler@3.329.0: + resolution: {integrity: sha512-OrjaHjU2ZTPfoHa5DruRvTIbeHH/cc0wvh4ml+FwDpWaPaBpOhLiluhZ3anqX1l5QjrXNiQnL8FxSM5OV/zVCA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.329.0 + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/querystring-builder': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/property-provider@3.329.0: + resolution: {integrity: sha512-1cHLTV6yyMGaMSWWDW/p4vTkJ1cc5BOEO+A0eHuAcoSOk+LDe9IKhUG3/ZOvvYKQYcqIj5jjGSni/noXNCl/qw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/protocol-http@3.329.0: + resolution: {integrity: sha512-0rLEHY6QTHTUUcVxzGbPUSmCKlXWplxT/fcYRh0bcc5MBK4naKfcQft1O6Ajp8uqs/9YPZ7XCVCn90pDeJfeaw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/querystring-builder@3.329.0: + resolution: {integrity: sha512-UWgMKkS5trliaDJG4nPv3onu8Y0aBuwRo7RdIgggguOiU8pU6pq1I113nH2FBNWy+Me1bwf+bcviJh0pCo6bEg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-uri-escape': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/querystring-parser@3.329.0: + resolution: {integrity: sha512-9mkK+FB7snJ2G7H3CqtprDwYIRhzm6jEezffCwUWrC+lbqHBbErbhE9IeU/MKxILmf0RbC2riXEY1MHGspjRrQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/service-error-classification@3.329.0: + resolution: {integrity: sha512-TSNr0flOcCLe71aPp7MjblKNGsmxpTU4xR5772MDX9Cz9GUTNZCPFtvrcqd+wzEPP/AC7XwNXe8KjoXooZImUQ==} + engines: {node: '>=14.0.0'} + dev: false + + /@aws-sdk/shared-ini-file-loader@3.329.0: + resolution: {integrity: sha512-e0hyd75fbjMd4aCoRwpP2/HR+0oScwogErVArIkq3F42c/hyNCQP3sph4JImuXIjuo6HNnpKpf20CEPPhNna8A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/signature-v4-multi-region@3.329.0: + resolution: {integrity: sha512-SiK1ez8Ns61ulDm0MJsTOSGNJNOMNoPgfA9i+Uu/VMCBkotZASuxrcSWW8seQnLEynWLerjUF9CYpCQuCqKn9w==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@aws-sdk/signature-v4-crt': ^3.118.0 + peerDependenciesMeta: + '@aws-sdk/signature-v4-crt': + optional: true + dependencies: + '@aws-sdk/protocol-http': 3.329.0 + '@aws-sdk/signature-v4': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/signature-v4@3.329.0: + resolution: {integrity: sha512-9EnLoyOD5nFtCRAp+QRllDgQASCfY7jLHVhwht7jzwE80wE65Z9Ym5Z/mwTd4IyTz/xXfCvcE2VwClsBt0Ybdw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.310.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-hex-encoding': 3.310.0 + '@aws-sdk/util-middleware': 3.329.0 + '@aws-sdk/util-uri-escape': 3.310.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/smithy-client@3.329.0: + resolution: {integrity: sha512-7E0fGpBKxwFqHHAOqNbgNsHSEmCZLuvmU9yvG9DXKVzrS4P48O/PfOro123WpcFZs3STyOVgH8wjUPftHAVKmg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/token-providers@3.332.0: + resolution: {integrity: sha512-fccbg6OSl0l658pxl2p1MoU9gEePo5B361+JNaN0zfRMu7c5HBXCpdl4djlFxAHjltrX9f1+BKqfGHYgI3h8SQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso-oidc': 3.332.0 + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/shared-ini-file-loader': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/types@3.329.0: + resolution: {integrity: sha512-wFBW4yciDfzQBSFmWNaEvHShnSGLMxSu9Lls6EUf6xDMavxSB36bsrVRX6CyAo/W0NeIIyEOW1LclGPgJV1okg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/url-parser@3.329.0: + resolution: {integrity: sha512-/VcfL7vNJKJGSjYYHVQF3bYCDFs4fSzB7j5qeVDwRdWr870gE7O1Dar+sLWBRKFF3AX+4VzplqzUfpu9t44JVA==} + dependencies: + '@aws-sdk/querystring-parser': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-arn-parser@3.310.0: + resolution: {integrity: sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-base64@3.310.0: + resolution: {integrity: sha512-v3+HBKQvqgdzcbL+pFswlx5HQsd9L6ZTlyPVL2LS9nNXnCcR3XgGz9jRskikRUuUvUXtkSG1J88GAOnJ/apTPg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-body-length-browser@3.310.0: + resolution: {integrity: sha512-sxsC3lPBGfpHtNTUoGXMQXLwjmR0zVpx0rSvzTPAuoVILVsp5AU/w5FphNPxD5OVIjNbZv9KsKTuvNTiZjDp9g==} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-body-length-node@3.310.0: + resolution: {integrity: sha512-2tqGXdyKhyA6w4zz7UPoS8Ip+7sayOg9BwHNidiGm2ikbDxm1YrCfYXvCBdwaJxa4hJfRVz+aL9e+d3GqPI9pQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-buffer-from@3.310.0: + resolution: {integrity: sha512-i6LVeXFtGih5Zs8enLrt+ExXY92QV25jtEnTKHsmlFqFAuL3VBeod6boeMXkN2p9lbSVVQ1sAOOYZOHYbYkntw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-config-provider@3.310.0: + resolution: {integrity: sha512-xIBaYo8dwiojCw8vnUcIL4Z5tyfb1v3yjqyJKJWV/dqKUFOOS0U591plmXbM+M/QkXyML3ypon1f8+BoaDExrg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-defaults-mode-browser@3.329.0: + resolution: {integrity: sha512-2iSiy/pzX3OXMhtSxtAzOiEFr3viQEFnYOTeZuiheuyS+cea2L79F6SlZ1110b/nOIU/UOrxxtz83HVad8YFMQ==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/types': 3.329.0 + bowser: 2.11.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-defaults-mode-node@3.329.0: + resolution: {integrity: sha512-7A6C7YKjkZtmKtH29isYEtOCbhd7IcXPP8lftN8WAWlLOiZE4gV7PHveagUj7QserJzgRKGwwTQbBj53n18HYg==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/config-resolver': 3.329.0 + '@aws-sdk/credential-provider-imds': 3.329.0 + '@aws-sdk/node-config-provider': 3.329.0 + '@aws-sdk/property-provider': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-endpoints@3.332.0: + resolution: {integrity: sha512-nQx7AiOroMU2hj6h+umWOSZ+WECwxupaxFUK/PPKGW6NY/VdQE6LluYnXOtF5awlr8w1nPksT0Lq05PZutMDLA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-hex-encoding@3.310.0: + resolution: {integrity: sha512-sVN7mcCCDSJ67pI1ZMtk84SKGqyix6/0A1Ab163YKn+lFBQRMKexleZzpYzNGxYzmQS6VanP/cfU7NiLQOaSfA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-locate-window@3.310.0: + resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-middleware@3.329.0: + resolution: {integrity: sha512-RhBOBaxzkTUghi4MSqr8S5qeeBCjgJ0XPJ6jIYkVkj1saCmqkuZCgl3zFaYdyhdxxPV6nflkFer+1HUoqT+Fqw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-retry@3.329.0: + resolution: {integrity: sha512-+3VQ9HZLinysnmryUs9Xjt1YVh4TYYHLt30ilu4iUnIHFQoamdzIbRCWseSVFPCxGroen9M9qmAleAsytHEKuA==} + engines: {node: '>= 14.0.0'} + dependencies: + '@aws-sdk/service-error-classification': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-stream-browser@3.329.0: + resolution: {integrity: sha512-UF1fJNfgrdJLMxn8ZlfPkYdv7hoLvVgSk3GHgxYA4OQs5zKCzeZgVrbxtE147LxWwJbxi3Qf04vnaEHwzVESpg==} + dependencies: + '@aws-sdk/fetch-http-handler': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-base64': 3.310.0 + '@aws-sdk/util-hex-encoding': 3.310.0 + '@aws-sdk/util-utf8': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-stream-node@3.331.0: + resolution: {integrity: sha512-5YUatdh4vgkv7VFY+lSkF+b+6EFkiHvy+dlucfGoJEOcEzuA/NBZYebWbcJ5TiR6z3cQdA23OTyZz3ZofZY1hw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/node-http-handler': 3.329.0 + '@aws-sdk/types': 3.329.0 + '@aws-sdk/util-buffer-from': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-uri-escape@3.310.0: + resolution: {integrity: sha512-drzt+aB2qo2LgtDoiy/3sVG8w63cgLkqFIa2NFlGpUgHFWTXkqtbgf4L5QdjRGKWhmZsnqkbtL7vkSWEcYDJ4Q==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-user-agent-browser@3.329.0: + resolution: {integrity: sha512-8hLSmMCl8aw2++0Zuba8ELq8FkK6/VNyx470St201IpMn2GMbQMDl/rLolRKiTgji6wc+T3pOTidkJkz8/cIXA==} + dependencies: + '@aws-sdk/types': 3.329.0 + bowser: 2.11.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-user-agent-node@3.329.0: + resolution: {integrity: sha512-C50Zaeodc0+psEP+L4WpElrH8epuLWJPVN4hDOTORcM0cSoU2o025Ost9mbcU7UdoHNxF9vitLnzORGN9SHolg==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/node-config-provider': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-utf8-browser@3.259.0: + resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} + dependencies: + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-utf8@3.310.0: + resolution: {integrity: sha512-DnLfFT8uCO22uOJc0pt0DsSNau1GTisngBCDw8jQuWT5CqogMJu4b/uXmwEqfj8B3GX6Xsz8zOd6JpRlPftQoA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.310.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/util-waiter@3.329.0: + resolution: {integrity: sha512-MIGs7snNL0ZV55zo1BDVPlrmbinUGV3260hp6HrW4zUbpYVoeIOGeewtrwAsF6FJ+vpZCxljPBB0X2jYR7Q7ZQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.329.0 + '@aws-sdk/types': 3.329.0 + tslib: 2.5.0 + dev: false + + /@aws-sdk/xml-builder@3.310.0: + resolution: {integrity: sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.5.0 + dev: false + /@babel/code-frame@7.21.4: resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} engines: {node: '>=6.9.0'} @@ -5545,6 +6497,7 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - supports-color + dev: false /@saleor/app-sdk@0.38.0(next@13.3.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JS/E3YODFHc+1DI+PczbV8jB09nLwzdQcwNs681RlwvR3JUC892hdBYYRdBKG5lauAcr4IxKw1IbrsxJKngtWA==} @@ -8267,6 +9220,10 @@ packages: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: false + /bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: