From 35efdd0b5bafb4d431d073750612b133b1847f0d Mon Sep 17 00:00:00 2001 From: Michal Cieslar Date: Tue, 5 Mar 2024 22:35:30 +0100 Subject: [PATCH 1/4] install multipart fastify --- apps/backend/package.json | 1 + apps/backend/src/core/httpServer.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/apps/backend/package.json b/apps/backend/package.json index ce6939d..e03f0ee 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -23,6 +23,7 @@ "@common/contracts": "*", "@fastify/cors": "^8.4.2", "@fastify/helmet": "^11.1.1", + "@fastify/multipart": "^8.1.0", "@fastify/swagger": "^8.12.1", "@fastify/swagger-ui": "^1.10.2", "@fastify/type-provider-typebox": "^3.5.0", diff --git a/apps/backend/src/core/httpServer.ts b/apps/backend/src/core/httpServer.ts index b625a3d..3121221 100644 --- a/apps/backend/src/core/httpServer.ts +++ b/apps/backend/src/core/httpServer.ts @@ -2,6 +2,7 @@ import { fastifyCors } from '@fastify/cors'; import { fastifyHelmet } from '@fastify/helmet'; +import { fastifyMultipart } from '@fastify/multipart'; import { fastifySwagger } from '@fastify/swagger'; import { fastifySwaggerUi } from '@fastify/swagger-ui'; import { type TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; @@ -63,6 +64,8 @@ export class HttpServer { await this.initSwagger(); + this.fastifyInstance.register(fastifyMultipart); + await this.fastifyInstance.register(fastifyHelmet); await this.fastifyInstance.register(fastifyCors, { From 4b071cd99947e82e7859eb6c4eefb8fc955fe23d Mon Sep 17 00:00:00 2001 From: Michal Cieslar Date: Tue, 5 Mar 2024 23:06:25 +0100 Subject: [PATCH 2/4] add upload resource command handler --- .../src/common/types/http/httpRequest.ts | 9 +++ .../resourceHttpController.ts | 68 ++++++++++++++++++- .../schemas/uploadResourceSchema.ts | 22 ++++++ .../uploadResourceCommandHandler.ts | 13 ++++ .../uploadResourceCommandHandlerImpl.ts | 66 ++++++++++++++++++ .../resourceBlobService.ts | 1 + ...esourceBlobServiceImpl.integration.test.ts | 22 +----- .../resourceBlobServiceImpl.ts | 16 +---- .../modules/resourceModule/resourceModule.ts | 13 ++++ .../src/modules/resourceModule/symbols.ts | 1 + common/contracts/src/index.ts | 2 + .../src/schemas/resource/uploadResource.ts | 3 + package-lock.json | 38 +++++++++++ 13 files changed, 239 insertions(+), 35 deletions(-) create mode 100644 apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts create mode 100644 apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandler.ts create mode 100644 apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandlerImpl.ts create mode 100644 common/contracts/src/schemas/resource/uploadResource.ts diff --git a/apps/backend/src/common/types/http/httpRequest.ts b/apps/backend/src/common/types/http/httpRequest.ts index a0a6d0d..3a56ad8 100644 --- a/apps/backend/src/common/types/http/httpRequest.ts +++ b/apps/backend/src/common/types/http/httpRequest.ts @@ -1,8 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { type Readable } from 'node:stream'; + +export interface AttachedFile { + readonly name: string; + readonly type: string; + readonly data: Readable; +} + export interface HttpRequest { readonly body: Body; readonly queryParams: QueryParams; readonly pathParams: PathParams; readonly headers: Record; + readonly file?: AttachedFile; } diff --git a/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts index 13084c1..9d77d8d 100644 --- a/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts +++ b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts @@ -40,17 +40,31 @@ import { type FindUserBucketsResponseBodyDTO, } from './schemas/findUserBucketsSchema.js'; import { type ResourceMetadataDTO } from './schemas/resourceMetadataDTO.js'; +import { + type UploadResourceResponseBodyDTO, + type UploadResourceBodyDTO, + type UploadResourcePathParamsDTO, + uploadResourceBodyDTOSchema, + uploadResourceResponseBodyDTOSchema, + uploadResourcePathParamsDTOSchema, +} from './schemas/uploadResourceSchema.js'; +import { OperationNotValidError } from '../../../../../common/errors/common/operationNotValidError.js'; import { type HttpController } from '../../../../../common/types/http/httpController.js'; import { HttpHeader } from '../../../../../common/types/http/httpHeader.js'; import { HttpMethodName } from '../../../../../common/types/http/httpMethodName.js'; import { type HttpRequest } from '../../../../../common/types/http/httpRequest.js'; -import { type HttpOkResponse, type HttpNoContentResponse } from '../../../../../common/types/http/httpResponse.js'; +import { + type HttpOkResponse, + type HttpNoContentResponse, + type HttpCreatedResponse, +} from '../../../../../common/types/http/httpResponse.js'; import { HttpRoute } from '../../../../../common/types/http/httpRoute.js'; import { HttpStatusCode } from '../../../../../common/types/http/httpStatusCode.js'; import { SecurityMode } from '../../../../../common/types/http/securityMode.js'; import { type AccessControlService } from '../../../../authModule/application/services/accessControlService/accessControlService.js'; import { type FindUserBucketsQueryHandler } from '../../../../userModule/application/queryHandlers/findUserBucketsQueryHandler/findUserBucketsQueryHandler.js'; import { type DeleteResourceCommandHandler } from '../../../application/commandHandlers/deleteResourceCommandHandler/deleteResourceCommandHandler.js'; +import { type UploadResourceCommandHandler } from '../../../application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandler.js'; import { type DownloadImageQueryHandler } from '../../../application/queryHandlers/downloadImageQueryHandler/downloadImageQueryHandler.js'; import { type DownloadResourceQueryHandler } from '../../../application/queryHandlers/downloadResourceQueryHandler/downloadResourceQueryHandler.js'; import { type DownloadResourcesQueryHandler } from '../../../application/queryHandlers/downloadResourcesQueryHandler/downloadResourcesQueryHandler.js'; @@ -65,6 +79,7 @@ export class ResourceHttpController implements HttpController { private readonly deleteResourceCommandHandler: DeleteResourceCommandHandler, private readonly findResourcesMetadataQueryHandler: FindResourcesMetadataQueryHandler, private readonly downloadResourceQueryHandler: DownloadResourceQueryHandler, + private readonly uploadResourceCommandHandler: UploadResourceCommandHandler, private readonly downloadResourcesQueryHandler: DownloadResourcesQueryHandler, private readonly downloadImageQueryHandler: DownloadImageQueryHandler, private readonly downloadVideoPreviewQueryHandler: DownloadVideoPreviewQueryHandler, @@ -110,6 +125,26 @@ export class ResourceHttpController implements HttpController { tags: ['Resource'], description: `Find bucket's resources metadata.`, }), + new HttpRoute({ + method: HttpMethodName.post, + path: ':bucketName/resources', + handler: this.uploadResource.bind(this), + schema: { + request: { + body: uploadResourceBodyDTOSchema, + pathParams: uploadResourcePathParamsDTOSchema, + }, + response: { + [HttpStatusCode.created]: { + schema: uploadResourceResponseBodyDTOSchema, + description: 'Resource uploaded.', + }, + }, + }, + securityMode: SecurityMode.bearer, + tags: ['Resource'], + description: `Upload a Resource.`, + }), new HttpRoute({ method: HttpMethodName.post, path: ':bucketName/resources/export', @@ -290,6 +325,37 @@ export class ResourceHttpController implements HttpController { }; } + private async uploadResource( + request: HttpRequest, + ): Promise> { + const { userId } = await this.accessControlService.verifyBearerToken({ + authorizationHeader: request.headers['authorization'], + }); + + if (!request.file) { + throw new OperationNotValidError({ + reason: 'File is required.', + }); + } + + const { bucketName } = request.pathParams; + + const { name, type, data } = request.file; + + await this.uploadResourceCommandHandler.execute({ + userId, + resourceName: name, + bucketName, + contentType: type, + data, + }); + + return { + statusCode: HttpStatusCode.created, + body: null, + }; + } + private async downloadResource( request: HttpRequest, ): Promise> { diff --git a/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts new file mode 100644 index 0000000..f15b72b --- /dev/null +++ b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts @@ -0,0 +1,22 @@ +import { Type, type Static } from '@sinclair/typebox'; + +import type * as contracts from '@common/contracts'; + +import { type TypeExtends } from '../../../../../../common/types/schemaExtends.js'; + +export const uploadResourcePathParamsDTOSchema = Type.Object({ + bucketName: Type.String({ minLength: 1 }), +}); + +export type UploadResourcePathParamsDTO = TypeExtends< + Static, + contracts.UploadResourcePathParams +>; + +export const uploadResourceBodyDTOSchema = Type.Any(); + +export type UploadResourceBodyDTO = Static; + +export const uploadResourceResponseBodyDTOSchema = Type.Any(); + +export type UploadResourceResponseBodyDTO = Static; diff --git a/apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandler.ts b/apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandler.ts new file mode 100644 index 0000000..ace6ac4 --- /dev/null +++ b/apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandler.ts @@ -0,0 +1,13 @@ +import { type Readable } from 'node:stream'; + +import { type CommandHandler } from '../../../../../common/types/commandHandler.js'; + +export interface UploadResourceCommandHandlerPayload { + readonly userId: string; + readonly resourceName: string; + readonly contentType: string; + readonly data: Readable; + readonly bucketName: string; +} + +export type UploadResourceCommandHandler = CommandHandler; diff --git a/apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandlerImpl.ts b/apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandlerImpl.ts new file mode 100644 index 0000000..2b1a92e --- /dev/null +++ b/apps/backend/src/modules/resourceModule/application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandlerImpl.ts @@ -0,0 +1,66 @@ +import { + type UploadResourceCommandHandler, + type UploadResourceCommandHandlerPayload, +} from './uploadResourceCommandHandler.js'; +import { OperationNotValidError } from '../../../../../common/errors/common/operationNotValidError.js'; +import { type LoggerService } from '../../../../../libs/logger/services/loggerService/loggerService.js'; +import { type FindUserBucketsQueryHandler } from '../../../../userModule/application/queryHandlers/findUserBucketsQueryHandler/findUserBucketsQueryHandler.js'; +import { type ResourceBlobService } from '../../../domain/services/resourceBlobService/resourceBlobService.js'; + +export class UploadResourceCommandHandlerImpl implements UploadResourceCommandHandler { + public constructor( + private readonly resourceBlobSerice: ResourceBlobService, + private readonly loggerService: LoggerService, + private readonly findUserBucketsQueryHandler: FindUserBucketsQueryHandler, + ) {} + + public async execute(payload: UploadResourceCommandHandlerPayload): Promise { + const { userId, resourceName, bucketName, contentType, data } = payload; + + const { buckets } = await this.findUserBucketsQueryHandler.execute({ userId }); + + if (!buckets.includes(bucketName)) { + throw new OperationNotValidError({ + reason: 'Bucket does not exist.', + userId, + bucketName, + }); + } + + this.loggerService.debug({ + message: 'Uploading Resource...', + userId, + bucketName, + resourceName, + contentType, + }); + + const existingResource = await this.resourceBlobSerice.resourceExists({ + bucketName, + resourceName, + }); + + if (existingResource) { + throw new OperationNotValidError({ + reason: 'Cannot create resource because it already exists.', + resourceName, + bucketName, + }); + } + + await this.resourceBlobSerice.uploadResource({ + bucketName, + resourceName, + data, + contentType, + }); + + this.loggerService.debug({ + message: 'Resource uploaded.', + userId, + bucketName, + resourceName, + contentType, + }); + } +} diff --git a/apps/backend/src/modules/resourceModule/domain/services/resourceBlobService/resourceBlobService.ts b/apps/backend/src/modules/resourceModule/domain/services/resourceBlobService/resourceBlobService.ts index d2b5fe1..3494ea3 100644 --- a/apps/backend/src/modules/resourceModule/domain/services/resourceBlobService/resourceBlobService.ts +++ b/apps/backend/src/modules/resourceModule/domain/services/resourceBlobService/resourceBlobService.ts @@ -7,6 +7,7 @@ export interface UploadResourcePayload { readonly resourceName: string; readonly bucketName: string; readonly data: Readable; + readonly contentType: string; } export interface DownloadResourcePayload { diff --git a/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.integration.test.ts b/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.integration.test.ts index d43e78a..51763b2 100644 --- a/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.integration.test.ts +++ b/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.integration.test.ts @@ -306,26 +306,6 @@ describe('ResourceBlobServiceImpl', () => { }); describe('upload', () => { - it('throws an error - when object already exists', async () => { - const filePath = path.join(resourcesDirectory, sampleFileName1); - - await s3TestUtils.uploadObject(bucketName, sampleFileName1, filePath); - - try { - await resourceBlobService.uploadResource({ - bucketName, - resourceName: sampleFileName1, - data: createReadStream(filePath), - }); - } catch (error) { - expect(error).toBeDefined(); - - return; - } - - expect.fail(); - }); - it('throws an error - when bucket does not exist', async () => { const filePath = path.join(resourcesDirectory, sampleFileName1); @@ -336,6 +316,7 @@ describe('ResourceBlobServiceImpl', () => { bucketName: nonExistingBucketName, resourceName: sampleFileName1, data: createReadStream(filePath), + contentType: 'video/mp4', }); } catch (error) { expect(error).toBeDefined(); @@ -353,6 +334,7 @@ describe('ResourceBlobServiceImpl', () => { bucketName, resourceName: sampleFileName1, data: createReadStream(filePath), + contentType: 'video/mp4', }); const exists = await s3TestUtils.objectExists(bucketName, sampleFileName1); diff --git a/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts b/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts index 65c00cc..c652b51 100644 --- a/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts +++ b/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts @@ -28,25 +28,13 @@ export class ResourceBlobServiceImpl implements ResourceBlobService { public constructor(private readonly s3Client: S3Client) {} public async uploadResource(payload: UploadResourcePayload): Promise { - const { bucketName, resourceName, data } = payload; - - const exists = await this.resourceExists({ - resourceName, - bucketName, - }); - - if (exists) { - throw new OperationNotValidError({ - reason: 'Resource already exists in bucket.', - resourceName, - bucketName, - }); - } + const { bucketName, resourceName, data, contentType } = payload; const command = new PutObjectCommand({ Bucket: bucketName, Key: resourceName, Body: data, + ContentType: contentType, }); await this.s3Client.send(command); diff --git a/apps/backend/src/modules/resourceModule/resourceModule.ts b/apps/backend/src/modules/resourceModule/resourceModule.ts index 73b6749..bafcf01 100644 --- a/apps/backend/src/modules/resourceModule/resourceModule.ts +++ b/apps/backend/src/modules/resourceModule/resourceModule.ts @@ -6,6 +6,8 @@ import { type DeleteBucketCommandHandler } from './application/commandHandlers/d import { DeleteBucketCommandHandlerImpl } from './application/commandHandlers/deleteBucketCommandHandler/deleteBucketCommandHandlerImpl.js'; import { type DeleteResourceCommandHandler } from './application/commandHandlers/deleteResourceCommandHandler/deleteResourceCommandHandler.js'; import { DeleteResourceCommandHandlerImpl } from './application/commandHandlers/deleteResourceCommandHandler/deleteResourceCommandHandlerImpl.js'; +import { type UploadResourceCommandHandler } from './application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandler.js'; +import { UploadResourceCommandHandlerImpl } from './application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandlerImpl.js'; import { type DownloadImageQueryHandler } from './application/queryHandlers/downloadImageQueryHandler/downloadImageQueryHandler.js'; import { DownloadImageQueryHandlerImpl } from './application/queryHandlers/downloadImageQueryHandler/downloadImageQueryHandlerImpl.js'; import { type DownloadResourceQueryHandler } from './application/queryHandlers/downloadResourceQueryHandler/downloadResourceQueryHandler.js'; @@ -48,6 +50,16 @@ export class ResourceModule implements DependencyInjectionModule { ), ); + container.bind( + symbols.uploadResourceCommandHandler, + () => + new UploadResourceCommandHandlerImpl( + container.get(symbols.resourceBlobService), + container.get(coreSymbols.loggerService), + container.get(userSymbols.findUserBucketsQueryHandler), + ), + ); + container.bind( symbols.findResourcesMetadataQueryHandler, () => @@ -128,6 +140,7 @@ export class ResourceModule implements DependencyInjectionModule { container.get(symbols.deleteResourceCommandHandler), container.get(symbols.findResourcesMetadataQueryHandler), container.get(symbols.downloadResourceQueryHandler), + container.get(symbols.uploadResourceCommandHandler), container.get(symbols.downloadResourcesQueryHandler), container.get(symbols.downloadImageQueryHandler), container.get(symbols.downloadVideoPreviewQueryHandler), diff --git a/apps/backend/src/modules/resourceModule/symbols.ts b/apps/backend/src/modules/resourceModule/symbols.ts index 72cca28..5d0917e 100644 --- a/apps/backend/src/modules/resourceModule/symbols.ts +++ b/apps/backend/src/modules/resourceModule/symbols.ts @@ -7,6 +7,7 @@ export const symbols = { downloadImageQueryHandler: Symbol('downloadImageQueryHandler'), downloadVideoPreviewQueryHandler: Symbol('downloadVideoPreviewQueryHandler'), deleteResourceCommandHandler: Symbol('deleteResourceCommandHandler'), + uploadResourceCommandHandler: Symbol('uploadResourceCommandHandler'), findBucketsQueryHandler: Symbol('findBucketsQueryHandler'), createBucketCommandHandler: Symbol('createBucketCommandHandler'), diff --git a/common/contracts/src/index.ts b/common/contracts/src/index.ts index 0dde98c..9e4d791 100644 --- a/common/contracts/src/index.ts +++ b/common/contracts/src/index.ts @@ -39,3 +39,5 @@ export * from './schemas/resource/createBucket.js'; export * from './schemas/resource/deleteBucket.js'; export * from './schemas/resource/downloadVideoPreview.js'; + +export * from './schemas/resource/uploadResource.js'; diff --git a/common/contracts/src/schemas/resource/uploadResource.ts b/common/contracts/src/schemas/resource/uploadResource.ts new file mode 100644 index 0000000..454a65b --- /dev/null +++ b/common/contracts/src/schemas/resource/uploadResource.ts @@ -0,0 +1,3 @@ +export interface UploadResourcePathParams { + readonly bucketName: string; +} diff --git a/package-lock.json b/package-lock.json index 45ccc8e..55c9c8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@common/contracts": "*", "@fastify/cors": "^8.4.2", "@fastify/helmet": "^11.1.1", + "@fastify/multipart": "^8.1.0", "@fastify/swagger": "^8.12.1", "@fastify/swagger-ui": "^1.10.2", "@fastify/type-provider-typebox": "^3.5.0", @@ -2043,6 +2044,17 @@ "fast-uri": "^2.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@fastify/cors": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz", @@ -2079,6 +2091,19 @@ "helmet": "^7.0.0" } }, + "node_modules/@fastify/multipart": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-8.1.0.tgz", + "integrity": "sha512-sRX9X4ZhAqRbe2kDvXY2NK7i6Wf1Rm2g/CjpGYYM7+Np8E6uWQXcj761j08qPfPO8PJXM+vJ7yrKbK1GPB+OeQ==", + "dependencies": { + "@fastify/busboy": "^1.0.0", + "@fastify/deepmerge": "^1.0.0", + "@fastify/error": "^3.0.0", + "fastify-plugin": "^4.0.0", + "secure-json-parse": "^2.4.0", + "stream-wormhole": "^1.1.0" + } + }, "node_modules/@fastify/send": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", @@ -13254,6 +13279,14 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, + "node_modules/stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/streamx": { "version": "2.15.6", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", @@ -13708,6 +13741,11 @@ "node": ">=8.0.0" } }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", From c36c26f6b425d35743e9be58b5387b1b0b27d4bf Mon Sep 17 00:00:00 2001 From: Michal Cieslar Date: Tue, 5 Mar 2024 23:11:18 +0100 Subject: [PATCH 3/4] remove schema for body in upload resource schema --- .../resourceHttpController/resourceHttpController.ts | 5 +---- .../resourceHttpController/schemas/uploadResourceSchema.ts | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts index 9d77d8d..c476a04 100644 --- a/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts +++ b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/resourceHttpController.ts @@ -42,9 +42,7 @@ import { import { type ResourceMetadataDTO } from './schemas/resourceMetadataDTO.js'; import { type UploadResourceResponseBodyDTO, - type UploadResourceBodyDTO, type UploadResourcePathParamsDTO, - uploadResourceBodyDTOSchema, uploadResourceResponseBodyDTOSchema, uploadResourcePathParamsDTOSchema, } from './schemas/uploadResourceSchema.js'; @@ -131,7 +129,6 @@ export class ResourceHttpController implements HttpController { handler: this.uploadResource.bind(this), schema: { request: { - body: uploadResourceBodyDTOSchema, pathParams: uploadResourcePathParamsDTOSchema, }, response: { @@ -326,7 +323,7 @@ export class ResourceHttpController implements HttpController { } private async uploadResource( - request: HttpRequest, + request: HttpRequest, ): Promise> { const { userId } = await this.accessControlService.verifyBearerToken({ authorizationHeader: request.headers['authorization'], diff --git a/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts index f15b72b..f7d1bed 100644 --- a/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts +++ b/apps/backend/src/modules/resourceModule/api/httpControllers/resourceHttpController/schemas/uploadResourceSchema.ts @@ -13,10 +13,6 @@ export type UploadResourcePathParamsDTO = TypeExtends< contracts.UploadResourcePathParams >; -export const uploadResourceBodyDTOSchema = Type.Any(); - -export type UploadResourceBodyDTO = Static; - export const uploadResourceResponseBodyDTOSchema = Type.Any(); export type UploadResourceResponseBodyDTO = Static; From 95bd28908326573cedfcf2abc245bb432bb0a755 Mon Sep 17 00:00:00 2001 From: Michal Cieslar Date: Tue, 5 Mar 2024 23:23:53 +0100 Subject: [PATCH 4/4] add multipart upload --- apps/backend/package.json | 1 + .../src/common/types/http/httpRequest.ts | 2 +- apps/backend/src/core/httpRouter.ts | 16 ++ .../resourceBlobServiceImpl.ts | 18 +- package-lock.json | 215 +++++++++++------- 5 files changed, 155 insertions(+), 97 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index e03f0ee..d998294 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.441.0", + "@aws-sdk/lib-storage": "^3.525.1", "@common/contracts": "*", "@fastify/cors": "^8.4.2", "@fastify/helmet": "^11.1.1", diff --git a/apps/backend/src/common/types/http/httpRequest.ts b/apps/backend/src/common/types/http/httpRequest.ts index 3a56ad8..1c8c56e 100644 --- a/apps/backend/src/common/types/http/httpRequest.ts +++ b/apps/backend/src/common/types/http/httpRequest.ts @@ -13,5 +13,5 @@ export interface HttpRequest { readonly queryParams: QueryParams; readonly pathParams: PathParams; readonly headers: Record; - readonly file?: AttachedFile; + readonly file?: AttachedFile | undefined; } diff --git a/apps/backend/src/core/httpRouter.ts b/apps/backend/src/core/httpRouter.ts index 3b7f5af..e02991d 100644 --- a/apps/backend/src/core/httpRouter.ts +++ b/apps/backend/src/core/httpRouter.ts @@ -12,6 +12,7 @@ import { ResourceAlreadyExistsError } from '../common/errors/common/resourceAlre import { ResourceNotFoundError } from '../common/errors/common/resourceNotFoundError.js'; import { type HttpController } from '../common/types/http/httpController.js'; import { HttpHeader } from '../common/types/http/httpHeader.js'; +import { type AttachedFile } from '../common/types/http/httpRequest.js'; import { type HttpRouteSchema, type HttpRoute } from '../common/types/http/httpRoute.js'; import { HttpStatusCode } from '../common/types/http/httpStatusCode.js'; import { type DependencyInjectionContainer } from '../libs/dependencyInjection/dependencyInjectionContainer.js'; @@ -81,6 +82,20 @@ export class HttpRouter { headers: fastifyRequest.headers, }); + let attachedFile: AttachedFile | undefined; + + if (fastifyRequest.isMultipart()) { + const file = await fastifyRequest.file(); + + if (file) { + attachedFile = { + name: file.filename, + type: file.mimetype, + data: file.file, + }; + } + } + const { statusCode, body: responseBody, @@ -90,6 +105,7 @@ export class HttpRouter { pathParams: fastifyRequest.params, queryParams: fastifyRequest.query, headers: fastifyRequest.headers as Record, + file: attachedFile, }); fastifyReply.status(statusCode); diff --git a/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts b/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts index c652b51..6766d83 100644 --- a/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts +++ b/apps/backend/src/modules/resourceModule/infrastructure/services/resourceBlobService/resourceBlobServiceImpl.ts @@ -4,9 +4,9 @@ import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, - PutObjectCommand, type ListObjectsV2CommandInput, } from '@aws-sdk/client-s3'; +import { Upload } from '@aws-sdk/lib-storage'; import { type Readable } from 'node:stream'; import { OperationNotValidError } from '../../../../../common/errors/common/operationNotValidError.js'; @@ -28,16 +28,18 @@ export class ResourceBlobServiceImpl implements ResourceBlobService { public constructor(private readonly s3Client: S3Client) {} public async uploadResource(payload: UploadResourcePayload): Promise { - const { bucketName, resourceName, data, contentType } = payload; + const { bucketName, resourceName, data } = payload; - const command = new PutObjectCommand({ - Bucket: bucketName, - Key: resourceName, - Body: data, - ContentType: contentType, + const upload = new Upload({ + client: this.s3Client, + params: { + Bucket: bucketName, + Key: resourceName, + Body: data, + }, }); - await this.s3Client.send(command); + await upload.done(); } public async downloadResource(payload: DownloadResourcePayload): Promise { diff --git a/package-lock.json b/package-lock.json index 55c9c8a..a0aa69c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "name": "@apps/backend", "dependencies": { "@aws-sdk/client-s3": "^3.441.0", + "@aws-sdk/lib-storage": "^3.525.1", "@common/contracts": "*", "@fastify/cors": "^8.4.2", "@fastify/helmet": "^11.1.1", @@ -719,6 +720,35 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.525.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.525.1.tgz", + "integrity": "sha512-q0y4+bc5GsE10F1HyA1D24maRyy2H3Ph3o+1eK7/kzKrk0nBbISLGbZ8XNqtWwi+9KdsqWNKMoN9+zsDaE6d/w==", + "dependencies": { + "@smithy/abort-controller": "^2.1.3", + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/smithy-client": "^2.4.2", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage/node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.502.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.502.0.tgz", @@ -3943,11 +3973,11 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@smithy/abort-controller": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.1.tgz", - "integrity": "sha512-1+qdrUqLhaALYL0iOcN43EP6yAXXQ2wWZ6taf4S2pNGowmOc5gx+iMQv+E42JizNJjB0+gEadOXeV1Bf7JWL1Q==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", + "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4082,13 +4112,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.1.tgz", - "integrity": "sha512-VYGLinPsFqH68lxfRhjQaSkjXM7JysUOJDTNjHBuN/ykyRb2f1gyavN9+VhhPTWCy32L4yZ2fdhpCs/nStEicg==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", + "integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", "dependencies": { - "@smithy/protocol-http": "^3.1.1", - "@smithy/querystring-builder": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", "@smithy/util-base64": "^2.1.1", "tslib": "^2.5.0" } @@ -4175,16 +4205,16 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.1.tgz", - "integrity": "sha512-XPZTb1E2Oav60Ven3n2PFx+rX9EDsU/jSTA8VDamt7FXks67ekjPY/XrmmPDQaFJOTUHJNKjd8+kZxVO5Ael4Q==", - "dependencies": { - "@smithy/middleware-serde": "^2.1.1", - "@smithy/node-config-provider": "^2.2.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", - "@smithy/url-parser": "^2.1.1", - "@smithy/util-middleware": "^2.1.1", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", + "integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", + "dependencies": { + "@smithy/middleware-serde": "^2.1.3", + "@smithy/node-config-provider": "^2.2.4", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", + "@smithy/url-parser": "^2.1.3", + "@smithy/util-middleware": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -4219,11 +4249,11 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.1.tgz", - "integrity": "sha512-D8Gq0aQBeE1pxf3cjWVkRr2W54t+cdM2zx78tNrVhqrDykRA7asq8yVJij1u5NDtKzKqzBSPYh7iW0svUKg76g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", + "integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4231,11 +4261,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.1.tgz", - "integrity": "sha512-KPJhRlhsl8CjgGXK/DoDcrFGfAqoqvuwlbxy+uOO4g2Azn1dhH+GVfC3RAp+6PoL5PWPb+vt6Z23FP+Mr6qeCw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", + "integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4243,13 +4273,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.1.tgz", - "integrity": "sha512-epzK3x1xNxA9oJgHQ5nz+2j6DsJKdHfieb+YgJ7ATWxzNcB7Hc+Uya2TUck5MicOPhDV8HZImND7ZOecVr+OWg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", + "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", "dependencies": { - "@smithy/property-provider": "^2.1.1", - "@smithy/shared-ini-file-loader": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/property-provider": "^2.1.3", + "@smithy/shared-ini-file-loader": "^2.3.4", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4257,14 +4287,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.3.1.tgz", - "integrity": "sha512-gLA8qK2nL9J0Rk/WEZSvgin4AppvuCYRYg61dcUo/uKxvMZsMInL5I5ZdJTogOvdfVug3N2dgI5ffcUfS4S9PA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", + "integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", "dependencies": { - "@smithy/abort-controller": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/querystring-builder": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/abort-controller": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/querystring-builder": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4272,11 +4302,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.1.tgz", - "integrity": "sha512-FX7JhhD/o5HwSwg6GLK9zxrMUrGnb3PzNBrcthqHKBc3dH0UfgEAU24xnJ8F0uow5mj17UeBEOI6o3CF2k7Mhw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", + "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4284,11 +4314,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.1.1.tgz", - "integrity": "sha512-6ZRTSsaXuSL9++qEwH851hJjUA0OgXdQFCs+VDw4tGH256jQ3TjYY/i34N4vd24RV3nrjNsgd1yhb57uMoKbzQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", + "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4296,11 +4326,11 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.1.tgz", - "integrity": "sha512-C/ko/CeEa8jdYE4gt6nHO5XDrlSJ3vdCG0ZAc6nD5ZIE7LBp0jCx4qoqp7eoutBu7VrGMXERSRoPqwi1WjCPbg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", + "integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "@smithy/util-uri-escape": "^2.1.1", "tslib": "^2.5.0" }, @@ -4309,11 +4339,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.1.tgz", - "integrity": "sha512-H4+6jKGVhG1W4CIxfBaSsbm98lOO88tpDWmZLgkJpt8Zkk/+uG0FmmqMuCAc3HNM2ZDV+JbErxr0l5BcuIf/XQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", + "integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4332,11 +4362,11 @@ } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.1.tgz", - "integrity": "sha512-2E2kh24igmIznHLB6H05Na4OgIEilRu0oQpYXo3LCNRrawHAcfDKq9004zJs+sAMt2X5AbY87CUCJ7IpqpSgdw==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", + "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4362,15 +4392,15 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.3.1.tgz", - "integrity": "sha512-YsTdU8xVD64r2pLEwmltrNvZV6XIAC50LN6ivDopdt+YiF/jGH6PY9zUOu0CXD/d8GMB8gbhnpPsdrjAXHS9QA==", - "dependencies": { - "@smithy/middleware-endpoint": "^2.4.1", - "@smithy/middleware-stack": "^2.1.1", - "@smithy/protocol-http": "^3.1.1", - "@smithy/types": "^2.9.1", - "@smithy/util-stream": "^2.1.1", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", + "integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.4.4", + "@smithy/middleware-stack": "^2.1.3", + "@smithy/protocol-http": "^3.2.1", + "@smithy/types": "^2.10.1", + "@smithy/util-stream": "^2.1.3", "tslib": "^2.5.0" }, "engines": { @@ -4378,9 +4408,9 @@ } }, "node_modules/@smithy/types": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.9.1.tgz", - "integrity": "sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", + "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", "dependencies": { "tslib": "^2.5.0" }, @@ -4389,12 +4419,12 @@ } }, "node_modules/@smithy/url-parser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.1.tgz", - "integrity": "sha512-qC9Bv8f/vvFIEkHsiNrUKYNl8uKQnn4BdhXl7VzQRP774AwIjiSMMwkbT+L7Fk8W8rzYVifzJNYxv1HwvfBo3Q==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", + "integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", "dependencies": { - "@smithy/querystring-parser": "^2.1.1", - "@smithy/types": "^2.9.1", + "@smithy/querystring-parser": "^2.1.3", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" } }, @@ -4509,11 +4539,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.1.tgz", - "integrity": "sha512-mKNrk8oz5zqkNcbcgAAepeJbmfUW6ogrT2Z2gDbIUzVzNAHKJQTYmH9jcy0jbWb+m7ubrvXKb6uMjkSgAqqsFA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", + "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", "dependencies": { - "@smithy/types": "^2.9.1", + "@smithy/types": "^2.10.1", "tslib": "^2.5.0" }, "engines": { @@ -4534,13 +4564,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.1.tgz", - "integrity": "sha512-J7SMIpUYvU4DQN55KmBtvaMc7NM3CZ2iWICdcgaovtLzseVhAqFRYqloT3mh0esrFw+3VEK6nQFteFsTqZSECQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", + "integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", "dependencies": { - "@smithy/fetch-http-handler": "^2.4.1", - "@smithy/node-http-handler": "^2.3.1", - "@smithy/types": "^2.9.1", + "@smithy/fetch-http-handler": "^2.4.3", + "@smithy/node-http-handler": "^2.4.1", + "@smithy/types": "^2.10.1", "@smithy/util-base64": "^2.1.1", "@smithy/util-buffer-from": "^2.1.1", "@smithy/util-hex-encoding": "^2.1.1", @@ -13279,6 +13309,15 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, "node_modules/stream-wormhole": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz",