From cfb6b642075d2fc4aa962346d8cd979374bfb14e Mon Sep 17 00:00:00 2001 From: Ivan Passalia Date: Sun, 17 Mar 2024 14:07:28 -0300 Subject: [PATCH] feat(nexp): add file system and adapt to fastify (#239) * feat(nexp): add file system and adapt to fastify Co-authored-by: Natanael Russo --- docker-compose.yml | 37 ++- package.json | 1 + pnpm-lock.yaml | 129 ++++++-- src/Config/Permissions.ts | 18 ++ src/File/Domain/Entities/File.ts | 15 + src/File/Domain/Entities/FileVersion.ts | 65 ++++ src/File/Domain/Entities/IFileBuild.ts | 16 + src/File/Domain/Entities/IFileDomain.ts | 8 + src/File/Domain/Entities/IFileDomainDto.ts | 8 + .../Domain/Entities/IFileVersionDomain.ts | 11 + .../Domain/Entities/IFileVersionPayload.ts | 17 + src/File/Domain/Models/FileDTO.ts | 27 ++ src/File/Domain/Models/FileVersionDTO.ts | 36 +++ src/File/Domain/Models/IFileDTO.ts | 10 + src/File/Domain/Models/IFileVersionDTO.ts | 10 + .../Domain/Payloads/CreateBucketPayload.ts | 10 + src/File/Domain/Payloads/DownloadPayload.ts | 6 + .../Domain/Payloads/FileBase64RepPayload.ts | 8 + .../Payloads/FileMultipartRepPayload.ts | 10 + .../Payloads/FileOptionsQueryPayload.ts | 10 + src/File/Domain/Payloads/FilePayload.ts | 9 + src/File/Domain/Payloads/FileRepPayload.ts | 14 + .../Payloads/FileUpdateBase64Payload.ts | 6 + .../Payloads/FileUpdateMultipartPayload.ts | 6 + .../Payloads/IFileVersionOptimizeDTO.ts | 9 + .../Domain/Payloads/ListObjectsPayload.ts | 9 + src/File/Domain/Payloads/OptimizePayload.ts | 6 + .../Payloads/PresignedFileRepPayload.ts | 10 + src/File/Domain/Payloads/VersionPayload.ts | 7 + src/File/Domain/Services/FileService.ts | 269 ++++++++++++++++ .../Domain/UseCases/CreateBucketUseCase.ts | 14 + src/File/Domain/UseCases/DownloadUseCase.ts | 19 ++ .../Domain/UseCases/GetFileMetadataUseCase.ts | 24 ++ .../UseCases/GetPresignedGetObjectUseCase.ts | 18 ++ src/File/Domain/UseCases/ListFilesUseCase.ts | 15 + .../Domain/UseCases/ListObjectsUseCase.ts | 18 ++ src/File/Domain/UseCases/OptimizeUseCase.ts | 53 ++++ src/File/Domain/UseCases/RemoveFileUseCase.ts | 20 ++ .../UseCases/UpdateFileBase64UseCase.ts | 49 +++ .../UseCases/UpdateFileMultipartUseCase.ts | 49 +++ .../Domain/UseCases/UploadBase64UseCase.ts | 41 +++ .../Domain/UseCases/UploadMultipartUseCase.ts | 46 +++ .../Repositories/FileMongooseRepository.ts | 37 +++ .../FileVersionMongooseRepository.ts | 78 +++++ .../Repositories/IFileRepository.ts | 12 + .../Repositories/IFileVersionRepository.ts | 15 + .../Infrastructure/Schemas/FileMongoose.ts | 15 + .../Schemas/FileVersionMongoose.ts | 33 ++ .../Commands/CreateBucketCommand.ts | 24 ++ .../Controllers/FileFastifyController.ts | 291 ++++++++++++++++++ src/File/Presentation/Criterias/FileFilter.ts | 20 ++ src/File/Presentation/Criterias/FileSort.ts | 22 ++ .../Presentation/DTO/FileBase64OptimizeDTO.ts | 65 ++++ .../DTO/FileMultipartOptimizeDTO.ts | 17 + .../DTO/FileUpdateBase64OptimizeDTO.ts | 20 ++ .../DTO/FileUpdateMultipartOptimizeDTO.ts | 22 ++ .../DTO/FileVersionOptimizeDTO.ts | 57 ++++ .../Middlewares/FileFastifyReqMiddleware.ts | 48 +++ .../RequestTypes/FileRequestTypes.ts | 2 + .../Requests/CreateBucketCommandRepRequest.ts | 67 ++++ .../Presentation/Routes/FileFastifyRouter.ts | 24 ++ .../Transformers/FileTransformer.ts | 31 ++ .../Transformers/FileVersionTransformer.ts | 29 ++ .../Transformers/IFileTransformer.ts | 11 + .../Transformers/IFileVersionTransformer.ts | 6 + .../Transformers/ObjectTransformer.ts | 19 ++ .../Validations/DownloadSchemaValidation.ts | 10 + .../Validations/FileBase64SchemaValidation.ts | 8 + .../FileBase64UpdateSchemaValidation.ts | 6 + .../FileMultipartSchemaValidation.ts | 10 + .../FileMultipartUpdateSchemaValidation.ts | 6 + .../FileOptionsQuerySchemaValidation.ts | 10 + .../Validations/FileRepSchemaValidation.ts | 16 + .../ListObjectsSchemaValidation.ts | 9 + .../Validations/OptimizeSchemaValidation.ts | 7 + .../PartialFileSchemaValidation.ts | 13 + .../PresignedFileSchemaValidation.ts | 11 + src/File/Tests/file.handler.spec.ts | 188 +++++++++++ src/File/Tests/fixture.ts | 5 + src/File/Tests/types.ts | 24 ++ src/Main/Infrastructure/Crons/Cron.ts | 1 - .../Database/CreateMongooseConnection.ts | 5 +- .../Presentation/Http/FastifyBootstrapping.ts | 3 +- .../Presentation/Utils/FastifyResponder.ts | 13 + src/Shared/DI/Injects/index.ts | 3 + src/Shared/DI/container.ts | 8 + src/command.ts | 4 +- src/index.ts | 2 +- tools/dev.init.sh | 3 +- 89 files changed, 2456 insertions(+), 37 deletions(-) create mode 100644 src/File/Domain/Entities/File.ts create mode 100644 src/File/Domain/Entities/FileVersion.ts create mode 100644 src/File/Domain/Entities/IFileBuild.ts create mode 100644 src/File/Domain/Entities/IFileDomain.ts create mode 100644 src/File/Domain/Entities/IFileDomainDto.ts create mode 100644 src/File/Domain/Entities/IFileVersionDomain.ts create mode 100644 src/File/Domain/Entities/IFileVersionPayload.ts create mode 100644 src/File/Domain/Models/FileDTO.ts create mode 100644 src/File/Domain/Models/FileVersionDTO.ts create mode 100644 src/File/Domain/Models/IFileDTO.ts create mode 100644 src/File/Domain/Models/IFileVersionDTO.ts create mode 100644 src/File/Domain/Payloads/CreateBucketPayload.ts create mode 100644 src/File/Domain/Payloads/DownloadPayload.ts create mode 100644 src/File/Domain/Payloads/FileBase64RepPayload.ts create mode 100644 src/File/Domain/Payloads/FileMultipartRepPayload.ts create mode 100644 src/File/Domain/Payloads/FileOptionsQueryPayload.ts create mode 100644 src/File/Domain/Payloads/FilePayload.ts create mode 100644 src/File/Domain/Payloads/FileRepPayload.ts create mode 100644 src/File/Domain/Payloads/FileUpdateBase64Payload.ts create mode 100644 src/File/Domain/Payloads/FileUpdateMultipartPayload.ts create mode 100644 src/File/Domain/Payloads/IFileVersionOptimizeDTO.ts create mode 100644 src/File/Domain/Payloads/ListObjectsPayload.ts create mode 100644 src/File/Domain/Payloads/OptimizePayload.ts create mode 100644 src/File/Domain/Payloads/PresignedFileRepPayload.ts create mode 100644 src/File/Domain/Payloads/VersionPayload.ts create mode 100644 src/File/Domain/Services/FileService.ts create mode 100644 src/File/Domain/UseCases/CreateBucketUseCase.ts create mode 100644 src/File/Domain/UseCases/DownloadUseCase.ts create mode 100644 src/File/Domain/UseCases/GetFileMetadataUseCase.ts create mode 100644 src/File/Domain/UseCases/GetPresignedGetObjectUseCase.ts create mode 100644 src/File/Domain/UseCases/ListFilesUseCase.ts create mode 100644 src/File/Domain/UseCases/ListObjectsUseCase.ts create mode 100644 src/File/Domain/UseCases/OptimizeUseCase.ts create mode 100644 src/File/Domain/UseCases/RemoveFileUseCase.ts create mode 100644 src/File/Domain/UseCases/UpdateFileBase64UseCase.ts create mode 100644 src/File/Domain/UseCases/UpdateFileMultipartUseCase.ts create mode 100644 src/File/Domain/UseCases/UploadBase64UseCase.ts create mode 100644 src/File/Domain/UseCases/UploadMultipartUseCase.ts create mode 100644 src/File/Infrastructure/Repositories/FileMongooseRepository.ts create mode 100644 src/File/Infrastructure/Repositories/FileVersionMongooseRepository.ts create mode 100644 src/File/Infrastructure/Repositories/IFileRepository.ts create mode 100644 src/File/Infrastructure/Repositories/IFileVersionRepository.ts create mode 100644 src/File/Infrastructure/Schemas/FileMongoose.ts create mode 100644 src/File/Infrastructure/Schemas/FileVersionMongoose.ts create mode 100644 src/File/Presentation/Commands/CreateBucketCommand.ts create mode 100644 src/File/Presentation/Controllers/FileFastifyController.ts create mode 100644 src/File/Presentation/Criterias/FileFilter.ts create mode 100644 src/File/Presentation/Criterias/FileSort.ts create mode 100644 src/File/Presentation/DTO/FileBase64OptimizeDTO.ts create mode 100644 src/File/Presentation/DTO/FileMultipartOptimizeDTO.ts create mode 100644 src/File/Presentation/DTO/FileUpdateBase64OptimizeDTO.ts create mode 100644 src/File/Presentation/DTO/FileUpdateMultipartOptimizeDTO.ts create mode 100644 src/File/Presentation/DTO/FileVersionOptimizeDTO.ts create mode 100644 src/File/Presentation/Middlewares/FileFastifyReqMiddleware.ts create mode 100644 src/File/Presentation/RequestTypes/FileRequestTypes.ts create mode 100644 src/File/Presentation/Requests/CreateBucketCommandRepRequest.ts create mode 100644 src/File/Presentation/Routes/FileFastifyRouter.ts create mode 100644 src/File/Presentation/Transformers/FileTransformer.ts create mode 100644 src/File/Presentation/Transformers/FileVersionTransformer.ts create mode 100644 src/File/Presentation/Transformers/IFileTransformer.ts create mode 100644 src/File/Presentation/Transformers/IFileVersionTransformer.ts create mode 100644 src/File/Presentation/Transformers/ObjectTransformer.ts create mode 100644 src/File/Presentation/Validations/DownloadSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/FileBase64SchemaValidation.ts create mode 100644 src/File/Presentation/Validations/FileBase64UpdateSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/FileMultipartSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/FileMultipartUpdateSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/FileOptionsQuerySchemaValidation.ts create mode 100644 src/File/Presentation/Validations/FileRepSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/ListObjectsSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/OptimizeSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/PartialFileSchemaValidation.ts create mode 100644 src/File/Presentation/Validations/PresignedFileSchemaValidation.ts create mode 100644 src/File/Tests/file.handler.spec.ts create mode 100644 src/File/Tests/fixture.ts create mode 100644 src/File/Tests/types.ts diff --git a/docker-compose.yml b/docker-compose.yml index 99be2199..9e0290fe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: - ./config:/home/node/config - ./.env:/home/node/.env networks: - - experiencenet + - experiencenet worker: container_name: experience_worker_1 @@ -37,7 +37,7 @@ services: - ./config:/home/node/config - ./.env:/home/node/.env networks: - - experiencenet + - experiencenet db: container_name: experience_db_1 @@ -55,7 +55,29 @@ services: volumes: - data:/bitnami/mongodb networks: - - experiencenet + - experiencenet + + minio: + container_name: experience_minio_1 + restart: always + image: minio/minio + ports: + - "9000:9000" + - "9001:9001" + labels: + - traefik.http.routers.api.rule=Host(`minio.domain.com`) + - traefik.http.routers.api.tls=true + - traefik.http.routers.api.tls.certresolver=lets-encrypt + - traefik.port=80 + networks: + - experiencenet + environment: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: 12345678 + MINIO_DOMAIN: localhost + volumes: + - minio:/data + command: server --address 0.0.0.0:9000 --console-address 0.0.0.0:9001 /data rabbitmq: image: rabbitmq:3.9-management-alpine @@ -68,7 +90,7 @@ services: RABBITMQ_DEFAULT_USER: user RABBITMQ_DEFAULT_PASS: password networks: - - experiencenet + - experiencenet mail: container_name: experience_mail_1 @@ -78,7 +100,7 @@ services: - "1025:1025" - "8025:8025" networks: - - experiencenet + - experiencenet cache: image: docker.dragonflydb.io/dragonflydb/dragonfly @@ -93,7 +115,7 @@ services: - DRAGONFLY_PASSWORD=ewsua132435 - DISABLE_COMMANDS=FLUSHDB,FLUSHALL,CONFIG networks: - - experiencenet + - experiencenet volumes: - cache:/data @@ -108,3 +130,6 @@ volumes: driver: "local" cache: driver: "local" + minio: + driver: "local" + diff --git a/package.json b/package.json index 51551b55..8c7726d7 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@fastify/compress": "^7.0.0", "@fastify/cors": "^9.0.1", "@fastify/helmet": "^11.1.1", + "@fastify/multipart": "^8.1.0", "@godaddy/terminus": "^4.12.1", "@mikro-orm/core": "^6.1.5", "@mikro-orm/postgresql": "^6.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09563ff4..0c80e032 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@fastify/helmet': specifier: ^11.1.1 version: 11.1.1 + '@fastify/multipart': + specifier: ^8.1.0 + version: 8.1.0 '@godaddy/terminus': specifier: ^4.12.1 version: 4.12.1 @@ -47,6 +50,9 @@ dependencies: config: specifier: ^3.3.11 version: 3.3.11 + cwebp: + specifier: ^3.0.0 + version: 3.0.0 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -62,6 +68,9 @@ dependencies: fastify: specifier: ^4.26.1 version: 4.26.1 + fastify-multer: + specifier: ^2.0.3 + version: 2.0.3 handlebars: specifier: ^4.7.8 version: 4.7.8 @@ -895,6 +904,17 @@ packages: helmet: 7.1.0 dev: false + /@fastify/multipart@8.1.0: + resolution: {integrity: sha512-sRX9X4ZhAqRbe2kDvXY2NK7i6Wf1Rm2g/CjpGYYM7+Np8E6uWQXcj761j08qPfPO8PJXM+vJ7yrKbK1GPB+OeQ==} + dependencies: + '@fastify/busboy': 1.2.1 + '@fastify/deepmerge': 1.3.0 + '@fastify/error': 3.4.1 + fastify-plugin: 4.5.1 + secure-json-parse: 2.7.0 + stream-wormhole: 1.1.0 + dev: false + /@godaddy/terminus@4.12.1: resolution: {integrity: sha512-Tm+wVu1/V37uZXcT7xOhzdpFoovQReErff8x3y82k6YyWa1gzxWBjTyrx4G2enjEqoXPnUUmJ3MOmwH+TiP6Sw==} dependencies: @@ -1214,7 +1234,7 @@ packages: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.3.8 + semver: 7.6.0 tar: 6.1.12 transitivePeerDependencies: - encoding @@ -1746,7 +1766,7 @@ packages: debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.0 tsutils: 3.21.0(typescript@3.9.10) typescript: 3.9.10 transitivePeerDependencies: @@ -1767,7 +1787,7 @@ packages: debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -1789,7 +1809,7 @@ packages: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.5.4 + semver: 7.6.0 ts-api-utils: 1.0.3(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -1809,7 +1829,7 @@ packages: '@typescript-eslint/types': 7.0.2 '@typescript-eslint/typescript-estree': 7.0.2(typescript@5.3.3) eslint: 8.57.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript @@ -2052,6 +2072,10 @@ packages: resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==} dev: true + /append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + dev: false + /append-field@2.0.0: resolution: {integrity: sha512-yUPXgerKgcuwakzrRyklfhX+Ma2aYYMjb+BO2RPUwq+tk928V/i5DFWcCUS3hQhj468N+Ktmwb0tfbEtmfC6WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2694,6 +2718,16 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + dev: false + /config@3.3.11: resolution: {integrity: sha512-Dhn63ZoWCW5EMg4P0Sl/XNsj/7RLiUIA1x1npCy+m2cRwRHzLnt3UtYtxRDMZW/6oOMdWhCzaGYkOcajGgrAOA==} engines: {node: '>= 10.0.0'} @@ -2876,6 +2910,13 @@ packages: cssom: 0.3.8 dev: true + /cwebp@3.0.0: + resolution: {integrity: sha512-0PW8NmniiSSN2YgD01TWzbW7n6J6YC9y6AxgniEuFMavui9xRBds7eWznps3aEfz77T0oCryCyta8asqVLInfQ==} + engines: {node: '>=12'} + dependencies: + raw-body: 2.5.2 + dev: false + /dargs@7.0.0: resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} engines: {node: '>=8'} @@ -3815,6 +3856,26 @@ packages: strnum: 1.0.5 dev: false + /fastify-multer@2.0.3: + resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==} + engines: {node: '>=10.17.0'} + dependencies: + '@fastify/busboy': 1.2.1 + append-field: 1.0.0 + concat-stream: 2.0.0 + fastify-plugin: 2.3.4 + mkdirp: 1.0.4 + on-finished: 2.4.1 + type-is: 1.6.18 + xtend: 4.0.2 + dev: false + + /fastify-plugin@2.3.4: + resolution: {integrity: sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ==} + dependencies: + semver: 7.6.0 + dev: false + /fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} dev: false @@ -4472,6 +4533,13 @@ packages: hasBin: true dev: true + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -4904,7 +4972,7 @@ packages: '@babel/parser': 7.23.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color dev: true @@ -5297,7 +5365,7 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color dev: true @@ -5856,7 +5924,7 @@ packages: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} dependencies: - semver: 6.3.0 + semver: 6.3.1 /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -6117,7 +6185,7 @@ packages: https-proxy-agent: 7.0.2 mongodb: 5.9.2 new-find-package-json: 2.0.0 - semver: 7.5.4 + semver: 7.6.0 tar-stream: 3.1.6 tslib: 2.6.2 yauzl: 2.10.0 @@ -6411,7 +6479,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.13.1 - semver: 7.5.4 + semver: 7.6.0 validate-npm-package-license: 3.0.4 dev: true @@ -7258,6 +7326,16 @@ packages: murmur-32: 1.0.0 dev: false + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -7594,22 +7672,9 @@ packages: hasBin: true dev: true - /semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true - /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - dev: true - - /semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: false /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} @@ -7701,7 +7766,7 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} dependencies: - semver: 7.5.4 + semver: 7.6.0 dev: true /sisteransi@1.0.5: @@ -7863,6 +7928,11 @@ packages: any-promise: 1.3.0 dev: true + /stream-wormhole@1.1.0: + resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} + engines: {node: '>=4.0.0'} + dev: false + /streamx@2.15.6: resolution: {integrity: sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==} dependencies: @@ -8036,7 +8106,7 @@ packages: methods: 1.1.2 mime: 2.6.0 qs: 6.11.2 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color dev: true @@ -8457,6 +8527,10 @@ packages: is-typed-array: 1.1.12 dev: true + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + /typescript@3.9.10: resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==} engines: {node: '>=4.2.0'} @@ -8513,6 +8587,11 @@ packages: engines: {node: '>= 10.0.0'} dev: false + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + /update-browserslist-db@1.0.10(browserslist@4.21.4): resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} hasBin: true diff --git a/src/Config/Permissions.ts b/src/Config/Permissions.ts index 130c1798..14d0899d 100644 --- a/src/Config/Permissions.ts +++ b/src/Config/Permissions.ts @@ -14,6 +14,13 @@ class Permissions static readonly ITEMS_LIST: string = 'res:items#scopes:list'; static readonly ITEMS_DELETE: string = 'res:items#scopes:delete'; + // Files + static readonly FILES_UPLOAD: string = 'res:files#scopes:upload'; + static readonly FILES_UPDATE: string = 'res:files#scopes:update'; + static readonly FILES_DOWNLOAD: string = 'res:files#scopes:download'; + static readonly FILES_DELETE: string = 'res:files#scopes:delete'; + static readonly FILES_LIST: string = 'res:files#scopes:list'; + static readonly FILES_SHOW_METADATA: string = 'res:files#scopes:showMetadata'; static groupPermissions(): IGroupPermission[] { return [ @@ -34,6 +41,17 @@ class Permissions Permissions.ITEMS_LIST, Permissions.ITEMS_DELETE ] + }, + { + group: 'FILES', + permissions: [ + Permissions.FILES_UPLOAD, + Permissions.FILES_UPDATE, + Permissions.FILES_DELETE, + Permissions.FILES_DOWNLOAD, + Permissions.FILES_LIST, + Permissions.FILES_SHOW_METADATA + ] } ]; } diff --git a/src/File/Domain/Entities/File.ts b/src/File/Domain/Entities/File.ts new file mode 100644 index 00000000..a9ffb981 --- /dev/null +++ b/src/File/Domain/Entities/File.ts @@ -0,0 +1,15 @@ +import { Base } from '@digichanges/shared-experience'; +import IFileDomain from './IFileDomain'; + +class File extends Base implements IFileDomain +{ + currentVersion: number; + + constructor() + { + super(); + this.currentVersion = 0; + } +} + +export default File; diff --git a/src/File/Domain/Entities/FileVersion.ts b/src/File/Domain/Entities/FileVersion.ts new file mode 100644 index 00000000..eab10db5 --- /dev/null +++ b/src/File/Domain/Entities/FileVersion.ts @@ -0,0 +1,65 @@ +import { Base } from '@digichanges/shared-experience'; +import IFileVersionDomain from './IFileVersionDomain'; +import IFileBuild from './IFileBuild'; +import IFileDomain from './IFileDomain'; + +class FileVersion extends Base implements IFileVersionDomain +{ + name: string; + originalName: string; + mimeType: string; + path: string; + extension: string; + size: number; + version: number; + isPublic: boolean; + isOptimized: boolean; + file: IFileDomain; + + constructor(data?: IFileBuild) + { + super(); + this.file = data?.file; + this.originalName = data?.originalName; + this.version = data?.file.currentVersion + 1; + this.isPublic = data?.isPublic ?? false; + this.isOptimized = data?.isOptimized; + this.mimeType = data.mimeType; + this.path = data?.path ?? '/'; + this.extension = data.extension; + this.size = data.size; + this.setName(data?.isOriginalName ?? false); + this.setPath(); + } + + private setPath() + { + this.path = `/${this.file?.getId()}/${this.version}/`; + } + + public setName(hasOriginalName: boolean) + { + this.name = this._id; + + if (hasOriginalName) + { + this.name = this.originalName + .toLowerCase() + .replace(/^\s+|\s+$/gm, '') + .replace(/\s+/g, ' ') + .replace(/ /g, '_'); + } + } + + public get objectPath(): string + { + return `${this.path}${this.name}`; + } + + public get isImage(): boolean + { + return this.mimeType?.includes('image'); + } +} + +export default FileVersion; diff --git a/src/File/Domain/Entities/IFileBuild.ts b/src/File/Domain/Entities/IFileBuild.ts new file mode 100644 index 00000000..5ee8fefd --- /dev/null +++ b/src/File/Domain/Entities/IFileBuild.ts @@ -0,0 +1,16 @@ +import IFileDomain from './IFileDomain'; + +interface IFileBuild +{ + originalName: string; + isOriginalName: boolean; + isOptimized: boolean; + mimeType: string; + path: string; + extension: string; + size: number; + isPublic: boolean; + file: IFileDomain; +} + +export default IFileBuild; diff --git a/src/File/Domain/Entities/IFileDomain.ts b/src/File/Domain/Entities/IFileDomain.ts new file mode 100644 index 00000000..999c2511 --- /dev/null +++ b/src/File/Domain/Entities/IFileDomain.ts @@ -0,0 +1,8 @@ +import { IBaseDomain } from '@digichanges/shared-experience'; + +interface IFileDomain extends IBaseDomain +{ + currentVersion: number; +} + +export default IFileDomain; diff --git a/src/File/Domain/Entities/IFileDomainDto.ts b/src/File/Domain/Entities/IFileDomainDto.ts new file mode 100644 index 00000000..ea4763e9 --- /dev/null +++ b/src/File/Domain/Entities/IFileDomainDto.ts @@ -0,0 +1,8 @@ + +interface IFileDomainDto +{ + id: string; + url: string; +} + +export default IFileDomainDto; diff --git a/src/File/Domain/Entities/IFileVersionDomain.ts b/src/File/Domain/Entities/IFileVersionDomain.ts new file mode 100644 index 00000000..60c4508e --- /dev/null +++ b/src/File/Domain/Entities/IFileVersionDomain.ts @@ -0,0 +1,11 @@ +import { IBaseDomain } from '@digichanges/shared-experience'; +import IFileDomain from './IFileDomain'; +import FileRepPayload from '../Payloads/FileRepPayload'; + +interface IFileVersionDomain extends IBaseDomain, FileRepPayload +{ + file: IFileDomain; + setName(hasOriginalName: boolean): void; +} + +export default IFileVersionDomain; diff --git a/src/File/Domain/Entities/IFileVersionPayload.ts b/src/File/Domain/Entities/IFileVersionPayload.ts new file mode 100644 index 00000000..0543dea7 --- /dev/null +++ b/src/File/Domain/Entities/IFileVersionPayload.ts @@ -0,0 +1,17 @@ + +interface IFileVersionPayload +{ + _id?: string; + originalName: string; + mimeType: string; + base64?: string; + destination?: string; + extension: string; + filename: string; + path: string; + size: number; + encoding: string; + isImage: boolean; +} + +export default IFileVersionPayload; diff --git a/src/File/Domain/Models/FileDTO.ts b/src/File/Domain/Models/FileDTO.ts new file mode 100644 index 00000000..5d10b2f8 --- /dev/null +++ b/src/File/Domain/Models/FileDTO.ts @@ -0,0 +1,27 @@ +import IFileDomain from '../Entities/IFileDomain'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import IFileDTO from './IFileDTO'; + +class FileDTO implements IFileDTO +{ + private readonly _file: IFileDomain; + private readonly _versions: IFileVersionDomain[]; + + constructor(file: IFileDomain, fileVersions: IFileVersionDomain[]) + { + this._file = file; + this._versions = fileVersions; + } + + get file(): IFileDomain + { + return this._file; + } + + get versions(): IFileVersionDomain[] + { + return this._versions; + } +} + +export default FileDTO; diff --git a/src/File/Domain/Models/FileVersionDTO.ts b/src/File/Domain/Models/FileVersionDTO.ts new file mode 100644 index 00000000..053a2f0e --- /dev/null +++ b/src/File/Domain/Models/FileVersionDTO.ts @@ -0,0 +1,36 @@ +import internal from 'stream'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import IFileVersionDTO from './IFileVersionDTO'; + +class FileVersionDTO implements IFileVersionDTO +{ + private _metadata: IFileVersionDomain; + private _stream: internal.Readable; + + constructor(metadata: IFileVersionDomain, stream: internal.Readable) + { + this._metadata = metadata; + this._stream = stream; + } + + public get metadata(): IFileVersionDomain + { + return this._metadata; + } + + public set metadata(v: IFileVersionDomain) + { + this._metadata = v; + } + + public get stream(): internal.Readable + { + return this._stream; + } + public set stream(v: internal.Readable) + { + this._stream = v; + } +} + +export default FileVersionDTO; diff --git a/src/File/Domain/Models/IFileDTO.ts b/src/File/Domain/Models/IFileDTO.ts new file mode 100644 index 00000000..06e812d4 --- /dev/null +++ b/src/File/Domain/Models/IFileDTO.ts @@ -0,0 +1,10 @@ +import IFileDomain from '../Entities/IFileDomain'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; + +interface IFileDTO +{ + file: IFileDomain; + versions: IFileVersionDomain[]; +} + +export default IFileDTO; diff --git a/src/File/Domain/Models/IFileVersionDTO.ts b/src/File/Domain/Models/IFileVersionDTO.ts new file mode 100644 index 00000000..72a5ffbb --- /dev/null +++ b/src/File/Domain/Models/IFileVersionDTO.ts @@ -0,0 +1,10 @@ +import internal from 'stream'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; + +interface IFileVersionDTO +{ + metadata: IFileVersionDomain; + stream: internal.Readable; +} + +export default IFileVersionDTO; diff --git a/src/File/Domain/Payloads/CreateBucketPayload.ts b/src/File/Domain/Payloads/CreateBucketPayload.ts new file mode 100644 index 00000000..7edfc787 --- /dev/null +++ b/src/File/Domain/Payloads/CreateBucketPayload.ts @@ -0,0 +1,10 @@ + +interface CreateBucketPayload +{ + name: string; + region: string; + publicBucketPolicy: string; + privateBucketPolicy: string; +} + +export default CreateBucketPayload; diff --git a/src/File/Domain/Payloads/DownloadPayload.ts b/src/File/Domain/Payloads/DownloadPayload.ts new file mode 100644 index 00000000..d9a45421 --- /dev/null +++ b/src/File/Domain/Payloads/DownloadPayload.ts @@ -0,0 +1,6 @@ +import VersionPayload from './VersionPayload'; +import { IdPayload } from '@digichanges/shared-experience'; + +interface DownloadPayload extends VersionPayload, IdPayload {} + +export default DownloadPayload; diff --git a/src/File/Domain/Payloads/FileBase64RepPayload.ts b/src/File/Domain/Payloads/FileBase64RepPayload.ts new file mode 100644 index 00000000..b4d9f7cc --- /dev/null +++ b/src/File/Domain/Payloads/FileBase64RepPayload.ts @@ -0,0 +1,8 @@ +import FilePayload from './FilePayload'; + +interface FileBase64RepPayload extends Omit +{ + base64: string, +} + +export default FileBase64RepPayload; diff --git a/src/File/Domain/Payloads/FileMultipartRepPayload.ts b/src/File/Domain/Payloads/FileMultipartRepPayload.ts new file mode 100644 index 00000000..e1761019 --- /dev/null +++ b/src/File/Domain/Payloads/FileMultipartRepPayload.ts @@ -0,0 +1,10 @@ +import IFileVersionPayload from '../Entities/IFileVersionPayload'; +import FileOptionsQueryPayload from './FileOptionsQueryPayload'; + +interface FileMultipartRepPayload +{ + file: IFileVersionPayload; + query: FileOptionsQueryPayload; +} + +export default FileMultipartRepPayload; diff --git a/src/File/Domain/Payloads/FileOptionsQueryPayload.ts b/src/File/Domain/Payloads/FileOptionsQueryPayload.ts new file mode 100644 index 00000000..9e3463ce --- /dev/null +++ b/src/File/Domain/Payloads/FileOptionsQueryPayload.ts @@ -0,0 +1,10 @@ + +interface FileOptionsQueryPayload +{ + isOriginalName: boolean; + isPublic: boolean; + isOverwrite: boolean; + isOptimize: boolean; +} + +export default FileOptionsQueryPayload; diff --git a/src/File/Domain/Payloads/FilePayload.ts b/src/File/Domain/Payloads/FilePayload.ts new file mode 100644 index 00000000..5ae85310 --- /dev/null +++ b/src/File/Domain/Payloads/FilePayload.ts @@ -0,0 +1,9 @@ +import FileRepPayload from './FileRepPayload'; +import FileOptionsQueryPayload from './FileOptionsQueryPayload'; + +interface FilePayload extends FileRepPayload +{ + query: FileOptionsQueryPayload; +} + +export default FilePayload; diff --git a/src/File/Domain/Payloads/FileRepPayload.ts b/src/File/Domain/Payloads/FileRepPayload.ts new file mode 100644 index 00000000..d6c00c3c --- /dev/null +++ b/src/File/Domain/Payloads/FileRepPayload.ts @@ -0,0 +1,14 @@ +import { FileVersionPayload } from '@digichanges/shared-experience'; + +interface FileRepPayload extends FileVersionPayload +{ + originalName: string; + mimeType: string; + path: string; + extension: string; + size: number; + isPublic: boolean; + isImage: boolean; +} + +export default FileRepPayload; diff --git a/src/File/Domain/Payloads/FileUpdateBase64Payload.ts b/src/File/Domain/Payloads/FileUpdateBase64Payload.ts new file mode 100644 index 00000000..1a2c542b --- /dev/null +++ b/src/File/Domain/Payloads/FileUpdateBase64Payload.ts @@ -0,0 +1,6 @@ +import { IdPayload } from '@digichanges/shared-experience'; +import FileBase64RepPayload from './FileBase64RepPayload'; + +interface FileUpdateBase64Payload extends IdPayload, FileBase64RepPayload {} + +export default FileUpdateBase64Payload; diff --git a/src/File/Domain/Payloads/FileUpdateMultipartPayload.ts b/src/File/Domain/Payloads/FileUpdateMultipartPayload.ts new file mode 100644 index 00000000..d7ac889c --- /dev/null +++ b/src/File/Domain/Payloads/FileUpdateMultipartPayload.ts @@ -0,0 +1,6 @@ +import { IdPayload } from '@digichanges/shared-experience'; +import FileMultipartRepPayload from './FileMultipartRepPayload'; + +interface FileUpdateMultipartPayload extends IdPayload, FileMultipartRepPayload {} + +export default FileUpdateMultipartPayload; diff --git a/src/File/Domain/Payloads/IFileVersionOptimizeDTO.ts b/src/File/Domain/Payloads/IFileVersionOptimizeDTO.ts new file mode 100644 index 00000000..861f94e5 --- /dev/null +++ b/src/File/Domain/Payloads/IFileVersionOptimizeDTO.ts @@ -0,0 +1,9 @@ +import IFileVersionPayload from '../Entities/IFileVersionPayload'; +import FileRepPayload from './FileRepPayload'; + +interface IFileVersionOptimizeDTO extends Omit +{ + file: IFileVersionPayload; +} + +export default IFileVersionOptimizeDTO; diff --git a/src/File/Domain/Payloads/ListObjectsPayload.ts b/src/File/Domain/Payloads/ListObjectsPayload.ts new file mode 100644 index 00000000..da979703 --- /dev/null +++ b/src/File/Domain/Payloads/ListObjectsPayload.ts @@ -0,0 +1,9 @@ +import FileOptionsQueryPayload from './FileOptionsQueryPayload'; + +interface ListObjectsPayload extends FileOptionsQueryPayload +{ + recursive: boolean, + prefix: string, +} + +export default ListObjectsPayload; diff --git a/src/File/Domain/Payloads/OptimizePayload.ts b/src/File/Domain/Payloads/OptimizePayload.ts new file mode 100644 index 00000000..5651fb5a --- /dev/null +++ b/src/File/Domain/Payloads/OptimizePayload.ts @@ -0,0 +1,6 @@ +import { IdPayload } from '@digichanges/shared-experience'; +import FileOptionsQueryPayload from './FileOptionsQueryPayload'; + +interface OptimizePayload extends IdPayload, FileOptionsQueryPayload {} + +export default OptimizePayload; diff --git a/src/File/Domain/Payloads/PresignedFileRepPayload.ts b/src/File/Domain/Payloads/PresignedFileRepPayload.ts new file mode 100644 index 00000000..45202669 --- /dev/null +++ b/src/File/Domain/Payloads/PresignedFileRepPayload.ts @@ -0,0 +1,10 @@ +import FileOptionsQueryPayload from './FileOptionsQueryPayload'; +import VersionPayload from './VersionPayload'; + +interface PresignedFileRepPayload extends FileOptionsQueryPayload, VersionPayload +{ + file: string, + expiry: number, +} + +export default PresignedFileRepPayload; diff --git a/src/File/Domain/Payloads/VersionPayload.ts b/src/File/Domain/Payloads/VersionPayload.ts new file mode 100644 index 00000000..72e32ed3 --- /dev/null +++ b/src/File/Domain/Payloads/VersionPayload.ts @@ -0,0 +1,7 @@ + +interface VersionPayload +{ + version: number; +} + +export default VersionPayload; diff --git a/src/File/Domain/Services/FileService.ts b/src/File/Domain/Services/FileService.ts new file mode 100644 index 00000000..1ad6529f --- /dev/null +++ b/src/File/Domain/Services/FileService.ts @@ -0,0 +1,269 @@ +// @ts-ignore +import { CWebp } from 'cwebp'; +import { readFile, stat } from 'fs/promises'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import FilesystemFactory from '../../../Shared/Factories/FilesystemFactory'; +import IFileVersionRepository from '../../Infrastructure/Repositories/IFileVersionRepository'; +import PresignedFileRepPayload from '../../Domain/Payloads/PresignedFileRepPayload'; +import { ICriteria } from '@digichanges/shared-experience'; +import { IFilesystem, IPaginator } from '@digichanges/shared-experience'; +import ListObjectsPayload from '../../Domain/Payloads/ListObjectsPayload'; +import FileBase64RepPayload from '../Payloads/FileBase64RepPayload'; +import FileMultipartRepPayload from '../Payloads/FileMultipartRepPayload'; +import CreateBucketPayload from '../Payloads/CreateBucketPayload'; +import FileVersionDTO from '../Models/FileVersionDTO'; +import IFileVersionDTO from '../Models/IFileVersionDTO'; +import IFileVersionPayload from '../Entities/IFileVersionPayload'; +import FileMultipartOptimizeDTO from '../../Presentation/DTO/FileMultipartOptimizeDTO'; +import FileBase64OptimizeDTO from '../../Presentation/DTO/FileBase64OptimizeDTO'; +import FileUpdateMultipartPayload from '../Payloads/FileUpdateMultipartPayload'; +import FileUpdateMultipartOptimizeDTO from '../../Presentation/DTO/FileUpdateMultipartOptimizeDTO'; +import FileUpdateBase64Payload from '../Payloads/FileUpdateBase64Payload'; +import FileUpdateBase64OptimizeDTO from '../../Presentation/DTO/FileUpdateBase64OptimizeDTO'; +import IFileRepository from '../../Infrastructure/Repositories/IFileRepository'; +import IFileDomain from '../Entities/IFileDomain'; +import File from '../Entities/File'; +import IFileVersionOptimizeDTO from '../Payloads/IFileVersionOptimizeDTO'; +import FileVersionOptimizeDTO from '../../Presentation/DTO/FileVersionOptimizeDTO'; +import IFileDTO from '../Models/IFileDTO'; +import FileDTO from '../Models/FileDTO'; +import DownloadPayload from '../Payloads/DownloadPayload'; +import { REPOSITORIES } from '../../../Shared/DI/Injects'; +import DependencyInjector from '../../../Shared/DI/DependencyInjector'; + +class FileService +{ + private versionRepository: IFileVersionRepository; + private fileRepository: IFileRepository; + // TODO fileSystem should be a package and not a result from the factory + private fileSystem: IFilesystem; + + constructor() + { + this.versionRepository = DependencyInjector.inject(REPOSITORIES.IFileVersionRepository); + this.fileRepository = DependencyInjector.inject(REPOSITORIES.IFileRepository); + this.fileSystem = FilesystemFactory.create(); + } + + async getOne(id: string): Promise + { + return await this.fileRepository.getOne(id); + } + + async persist(): Promise + { + const file = new File(); + return this.fileRepository.save(file); + } + + async getPresignedGetObject(payload: PresignedFileRepPayload): Promise + { + const { file, expiry, version } = payload; + + const fileVersion = await this.versionRepository.getLastOneByFields(file, version, { initThrow: true }); + + return await this.getFileUrl(fileVersion, expiry); + } + + async persistVersion(fileVersion: IFileVersionDomain): Promise + { + return await this.versionRepository.save(fileVersion); + } + + async update(file: IFileDomain): Promise + { + file.currentVersion++; + + return this.fileRepository.save(file); + } + + async uploadFileBase64(fileVersion: IFileVersionDomain, payload: FileBase64RepPayload): Promise + { + await this.fileSystem.uploadFileByBuffer(fileVersion, payload.base64); + + return fileVersion; + } + + async uploadFileMultipart(fileVersion: IFileVersionDomain, payload: FileMultipartRepPayload): Promise + { + await this.fileSystem.uploadFile(fileVersion, payload.file.path); + + return fileVersion; + } + + async uploadFileVersionOptimized(fileVersion: IFileVersionDomain, payload: IFileVersionOptimizeDTO): Promise + { + await this.fileSystem.uploadFile(fileVersion, payload.file.path); + + return fileVersion; + } + + async list(payload: ICriteria): Promise + { + return this.versionRepository.list(payload); + } + + async listObjects(payload: ListObjectsPayload): Promise + { + return await this.fileSystem.listObjects(payload); + } + + async getVersions(file: string): Promise + { + return await this.versionRepository.getAllByFileId(file); + } + + async getLastVersions(file: string): Promise + { + return await this.versionRepository.getLastOneByFields(file); + } + + async getOneVersion(file: string, version: number): Promise + { + return await this.versionRepository.getOneByFileIdAndVersion(file, version); + } + + async createBucket(payload: CreateBucketPayload): Promise + { + const name = payload.name; + const bucketNamePrivate = `${name}.private`; + const bucketNamePublic = `${name}.public`; + + const region = payload.region; + const bucketPrivatePolicy = payload.privateBucketPolicy; + const bucketPublicPolicy = payload.publicBucketPolicy; + + await this.fileSystem.createBucket(bucketNamePrivate, region); + await this.fileSystem.setBucketPolicy(bucketPrivatePolicy, bucketNamePrivate); + + await this.fileSystem.createBucket(bucketNamePublic, region); + await this.fileSystem.setBucketPolicy(bucketPublicPolicy, bucketNamePublic); + } + + async download(payload: DownloadPayload): Promise + { + const { id, version } = payload; + + const fileVersion = !version ? await this.getLastVersions(id) : await this.getOneVersion(id, version); + + const stream = await this.fileSystem.downloadStreamFile(fileVersion); + + return new FileVersionDTO(fileVersion, stream); + } + + async getFileUrl(fileVersion: IFileVersionDomain, expiry: number): Promise + { + const metadata = { + 'Content-Type': fileVersion.mimeType, + 'Content-Length': fileVersion.size + }; + + return await this.fileSystem.presignedGetObject(fileVersion, expiry, metadata); + } + + async removeFileAndVersions(id: string): Promise + { + const fileVersions = await this.versionRepository.getAllByFileId(id); + + for (const fileVersion of fileVersions) + { + await this.fileSystem.removeObjects(fileVersion); + await this.versionRepository.delete(fileVersion.getId()); + } + + const file = await this.fileRepository.delete(id); + + return new FileDTO(file, fileVersions); + } + + private async getFileVersionOptimized(fileVersion: IFileVersionDomain): Promise + { + const path = await this.fileSystem.downloadFile(fileVersion); + const encoder = CWebp(path); + const newPath = path.replace(fileVersion.extension, 'webp'); + await encoder.write(newPath); + + return { + originalName: fileVersion.originalName.replace(fileVersion.extension, 'webp'), + encoding: '', + mimeType: 'image/webp', + destination: '', + extension: 'webp', + filename: fileVersion.name.replace(fileVersion.extension, 'webp'), + path: newPath, + size: fileVersion.size, + isImage: true + }; + } + + private async getFileMultipartOptimized(payload: IFileVersionPayload): Promise + { + const path = `${payload.destination}/${payload.filename}`; + const encoder = CWebp(path); + const newPath = path.replace(payload.extension, 'webp'); + + await encoder.write(newPath); + + return { + originalName: payload.originalName.replace(payload.extension, 'webp'), + mimeType: 'image/webp', + destination: payload.destination, + extension: 'webp', + filename: payload.filename.replace(payload.extension, 'webp'), + path: newPath, + size: (await stat(newPath)).size, + encoding: payload.encoding, + isImage: payload.isImage + }; + } + + private async getFileBase64Optimized(payload: FileBase64RepPayload): Promise + { + const buffer = Buffer.from(payload.base64, 'base64'); + const encoder = CWebp(buffer); + const newPath = '/tmp/converted.webp'; + await encoder.write(newPath); + + const buff = await readFile(newPath); + return buff.toString('base64'); + } + + async optimizeMultipartToUpload(payload: FileMultipartRepPayload): Promise + { + const { file, query } = payload; + const fileVersion = await this.getFileMultipartOptimized(file); + + return new FileMultipartOptimizeDTO(fileVersion, query); + } + + async optimizeMultipartToUpdate(payload: FileUpdateMultipartPayload): Promise + { + const { file, query } = payload; + const fileVersion = await this.getFileMultipartOptimized(file); + + return new FileUpdateMultipartOptimizeDTO(fileVersion, query); + } + + async optimizeBase64ToUpload(payload: FileBase64RepPayload): Promise + { + const base64data = await this.getFileBase64Optimized(payload); + + return new FileBase64OptimizeDTO(payload, base64data); + } + + async optimizeBase64ToUpdate(payload: FileUpdateBase64Payload): Promise + { + const base64data = await this.getFileBase64Optimized(payload); + + return new FileUpdateBase64OptimizeDTO(payload, base64data); + } + + async optimizeFileVersion(fileVersion: IFileVersionDomain): Promise + { + const multipart = await this.getFileVersionOptimized(fileVersion); + + return new FileVersionOptimizeDTO(multipart, fileVersion); + } +} + +export default FileService; diff --git a/src/File/Domain/UseCases/CreateBucketUseCase.ts b/src/File/Domain/UseCases/CreateBucketUseCase.ts new file mode 100644 index 00000000..6c0e9981 --- /dev/null +++ b/src/File/Domain/UseCases/CreateBucketUseCase.ts @@ -0,0 +1,14 @@ +import CreateBucketPayload from '../Payloads/CreateBucketPayload'; +import FileService from '../Services/FileService'; + +class CreateBucketUseCase +{ + private fileService = new FileService(); + + async handle(payload: CreateBucketPayload): Promise + { + await this.fileService.createBucket(payload); + } +} + +export default CreateBucketUseCase; diff --git a/src/File/Domain/UseCases/DownloadUseCase.ts b/src/File/Domain/UseCases/DownloadUseCase.ts new file mode 100644 index 00000000..0f7cf4a6 --- /dev/null +++ b/src/File/Domain/UseCases/DownloadUseCase.ts @@ -0,0 +1,19 @@ +import IFileVersionDTO from '../Models/IFileVersionDTO'; +import FileService from '../Services/FileService'; +import DownloadPayload from '../Payloads/DownloadPayload'; +import DownloadSchemaValidation from '../../Presentation/Validations/DownloadSchemaValidation'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +class DownloadUseCase +{ + private fileService = new FileService(); + + async handle(payload: DownloadPayload): Promise + { + await ValidatorSchema.handle(DownloadSchemaValidation, payload); + + return await this.fileService.download(payload); + } +} + +export default DownloadUseCase; diff --git a/src/File/Domain/UseCases/GetFileMetadataUseCase.ts b/src/File/Domain/UseCases/GetFileMetadataUseCase.ts new file mode 100644 index 00000000..631701cd --- /dev/null +++ b/src/File/Domain/UseCases/GetFileMetadataUseCase.ts @@ -0,0 +1,24 @@ +import { IdPayload } from '@digichanges/shared-experience'; +import FileService from '../Services/FileService'; +import FileDTO from '../Models/FileDTO'; +import IFileDTO from '../Models/IFileDTO'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; +import IdSchemaValidation from '../../../Main/Domain/Validations/IdSchemaValidation'; + +class GetFileMetadataUserCase +{ + private fileService = new FileService(); + + async handle(payload: IdPayload): Promise + { + await ValidatorSchema.handle(IdSchemaValidation, payload); + + const { id } = payload; + const file = await this.fileService.getOne(id); + const fileVersions = await this.fileService.getVersions(id); + + return new FileDTO(file, fileVersions); + } +} + +export default GetFileMetadataUserCase; diff --git a/src/File/Domain/UseCases/GetPresignedGetObjectUseCase.ts b/src/File/Domain/UseCases/GetPresignedGetObjectUseCase.ts new file mode 100644 index 00000000..319c4808 --- /dev/null +++ b/src/File/Domain/UseCases/GetPresignedGetObjectUseCase.ts @@ -0,0 +1,18 @@ +import PresignedFileRepPayload from '../Payloads/PresignedFileRepPayload'; +import FileService from '../Services/FileService'; +import PresignedFileSchemaValidation from '../../Presentation/Validations/PresignedFileSchemaValidation'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +class GetPresignedGetObjectUseCase +{ + private fileService = new FileService(); + + async handle(payload: PresignedFileRepPayload): Promise + { + await ValidatorSchema.handle(PresignedFileSchemaValidation, payload); + + return this.fileService.getPresignedGetObject(payload); + } +} + +export default GetPresignedGetObjectUseCase; diff --git a/src/File/Domain/UseCases/ListFilesUseCase.ts b/src/File/Domain/UseCases/ListFilesUseCase.ts new file mode 100644 index 00000000..986fe24d --- /dev/null +++ b/src/File/Domain/UseCases/ListFilesUseCase.ts @@ -0,0 +1,15 @@ +import { ICriteria } from '@digichanges/shared-experience'; +import { IPaginator } from '@digichanges/shared-experience'; +import FileService from '../Services/FileService'; + +class ListFilesUseCase +{ + private fileService = new FileService(); + + async handle(payload: ICriteria): Promise + { + return await this.fileService.list(payload); + } +} + +export default ListFilesUseCase; diff --git a/src/File/Domain/UseCases/ListObjectsUseCase.ts b/src/File/Domain/UseCases/ListObjectsUseCase.ts new file mode 100644 index 00000000..2ad897a6 --- /dev/null +++ b/src/File/Domain/UseCases/ListObjectsUseCase.ts @@ -0,0 +1,18 @@ +import ListObjectsPayload from '../Payloads/ListObjectsPayload'; +import FileService from '../Services/FileService'; +import ListObjectsSchemaValidation from '../../Presentation/Validations/ListObjectsSchemaValidation'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +class ListObjectsUseCase +{ + private fileService = new FileService(); + + async handle(payload: ListObjectsPayload): Promise + { + await ValidatorSchema.handle(ListObjectsSchemaValidation, payload); + + return await this.fileService.listObjects(payload); + } +} + +export default ListObjectsUseCase; diff --git a/src/File/Domain/UseCases/OptimizeUseCase.ts b/src/File/Domain/UseCases/OptimizeUseCase.ts new file mode 100644 index 00000000..2296a648 --- /dev/null +++ b/src/File/Domain/UseCases/OptimizeUseCase.ts @@ -0,0 +1,53 @@ +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import FileVersion from '../Entities/FileVersion'; +import FileService from '../Services/FileService'; +import IFileDTO from '../Models/IFileDTO'; +import FileDTO from '../Models/FileDTO'; +import OptimizePayload from '../Payloads/OptimizePayload'; +import OptimizeSchemaValidation from '../../Presentation/Validations/OptimizeSchemaValidation'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +class OptimizeUseCase +{ + private fileService = new FileService(); + + async handle(payload: OptimizePayload): Promise + { + await ValidatorSchema.handle(OptimizeSchemaValidation, payload); + + const { id } = payload; + let file = await this.fileService.getOne(id); + const fileVersions = await this.fileService.getVersions(id); + const lastVersion = fileVersions.find(v => v.version === file.currentVersion); + + const optimizing = lastVersion && !lastVersion.isOptimized && lastVersion.isImage; + + if (optimizing) + { + const optimizePayload = await this.fileService.optimizeFileVersion(lastVersion); + + const build = { + originalName: optimizePayload.originalName, + isOriginalName: payload.isOriginalName, + mimeType: optimizePayload.mimeType, + extension: optimizePayload.extension, + isPublic: payload.isPublic, + size: optimizePayload.size, + path: optimizePayload.path, + isOptimized: true, + file + }; + + let newFileVersion: IFileVersionDomain = new FileVersion(build); + newFileVersion = await this.fileService.persistVersion(newFileVersion); + await this.fileService.uploadFileVersionOptimized(newFileVersion, optimizePayload); + file = await this.fileService.update(file); + + fileVersions.push(newFileVersion); + } + + return new FileDTO(file, fileVersions); + } +} + +export default OptimizeUseCase; diff --git a/src/File/Domain/UseCases/RemoveFileUseCase.ts b/src/File/Domain/UseCases/RemoveFileUseCase.ts new file mode 100644 index 00000000..a4fab9e9 --- /dev/null +++ b/src/File/Domain/UseCases/RemoveFileUseCase.ts @@ -0,0 +1,20 @@ +import { IdPayload } from '@digichanges/shared-experience'; +import FileService from '../Services/FileService'; +import IFileDTO from '../Models/IFileDTO'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; +import IdSchemaValidation from '../../../Main/Domain/Validations/IdSchemaValidation'; + +class RemoveFileUseCase +{ + private fileService = new FileService(); + + async handle(payload: IdPayload): Promise + { + await ValidatorSchema.handle(IdSchemaValidation, payload); + + const { id } = payload; + return this.fileService.removeFileAndVersions(id); + } +} + +export default RemoveFileUseCase; diff --git a/src/File/Domain/UseCases/UpdateFileBase64UseCase.ts b/src/File/Domain/UseCases/UpdateFileBase64UseCase.ts new file mode 100644 index 00000000..a7555c1e --- /dev/null +++ b/src/File/Domain/UseCases/UpdateFileBase64UseCase.ts @@ -0,0 +1,49 @@ +import FileUpdateBase64Payload from '../Payloads/FileUpdateBase64Payload'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import FileService from '../Services/FileService'; +import FileVersion from '../Entities/FileVersion'; +import FileDTO from '../Models/FileDTO'; +import FileBase64UpdateSchemaValidation from '../../Presentation/Validations/FileBase64UpdateSchemaValidation'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +class UpdateFileBase64UseCase +{ + private fileService = new FileService(); + + async handle(payload: FileUpdateBase64Payload): Promise + { + await ValidatorSchema.handle(FileBase64UpdateSchemaValidation, payload); + + const { id } = payload; + + if (payload.query.isOptimize && payload.isImage) + { + payload = await this.fileService.optimizeBase64ToUpdate(payload); + } + + let file = await this.fileService.getOne(id); + + const build = { + isOriginalName: payload.query.isOriginalName, + originalName: payload.originalName, + isOptimized: payload.query.isOptimize && payload.isImage, + mimeType: payload.mimeType, + extension: payload.extension, + isPublic: payload.query.isPublic, + size: payload.size, + path: payload.path, + file + }; + + let fileVersion: IFileVersionDomain = new FileVersion(build); + fileVersion = await this.fileService.persistVersion(fileVersion); + file = await this.fileService.update(file); + await this.fileService.uploadFileBase64(fileVersion, payload); + + const fileVersions = await this.fileService.getVersions(file.getId()); + + return new FileDTO(file, fileVersions); + } +} + +export default UpdateFileBase64UseCase; diff --git a/src/File/Domain/UseCases/UpdateFileMultipartUseCase.ts b/src/File/Domain/UseCases/UpdateFileMultipartUseCase.ts new file mode 100644 index 00000000..e801f29f --- /dev/null +++ b/src/File/Domain/UseCases/UpdateFileMultipartUseCase.ts @@ -0,0 +1,49 @@ +import FileUpdateMultipartPayload from '../Payloads/FileUpdateMultipartPayload'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import FileService from '../Services/FileService'; +import FileVersion from '../Entities/FileVersion'; +import FileDTO from '../Models/FileDTO'; +import FileMultipartUpdateSchemaValidation from '../../Presentation/Validations/FileMultipartUpdateSchemaValidation'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +class UpdateFileMultipartUseCase +{ + private fileService = new FileService(); + + async handle(payload: FileUpdateMultipartPayload): Promise + { + await ValidatorSchema.handle(FileMultipartUpdateSchemaValidation, payload); + + const { id } = payload; + + if (payload.query.isOptimize && payload.file.isImage) + { + payload = await this.fileService.optimizeMultipartToUpdate(payload); + } + + let file = await this.fileService.getOne(id); + + const build = { + isOriginalName: payload.query.isOriginalName, + originalName: payload.file.originalName, + isOptimized: payload.query.isOptimize && payload.file.isImage, + mimeType: payload.file.mimeType, + extension: payload.file.extension, + isPublic: payload.query.isPublic, + size: payload.file.size, + path: payload.file.path, + file + }; + + let fileVersion: IFileVersionDomain = new FileVersion(build); + fileVersion = await this.fileService.persistVersion(fileVersion); + file = await this.fileService.update(file); + await this.fileService.uploadFileMultipart(fileVersion, payload); + + const fileVersions = await this.fileService.getVersions(file.getId()); + + return new FileDTO(file, fileVersions); + } +} + +export default UpdateFileMultipartUseCase; diff --git a/src/File/Domain/UseCases/UploadBase64UseCase.ts b/src/File/Domain/UseCases/UploadBase64UseCase.ts new file mode 100644 index 00000000..ab36f0cd --- /dev/null +++ b/src/File/Domain/UseCases/UploadBase64UseCase.ts @@ -0,0 +1,41 @@ +import FileBase64RepPayload from '../Payloads/FileBase64RepPayload'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import FileVersion from '../Entities/FileVersion'; +import FileService from '../Services/FileService'; +import FileDTO from '../Models/FileDTO'; + +class UploadBase64UseCase +{ + private fileService = new FileService(); + + async handle(payload: FileBase64RepPayload): Promise + { + if (payload.query?.isOptimize && payload.isImage) + { + payload = await this.fileService.optimizeBase64ToUpload(payload); + } + + let file = await this.fileService.persist(); + + const build = { + isOriginalName: payload.query?.isOriginalName ?? false, + originalName: payload.originalName, + isOptimized: payload.query?.isOptimize && payload.isImage, + mimeType: payload.mimeType, + extension: payload.extension, + isPublic: payload.query?.isPublic ?? false, + size: payload.size, + path: payload.path, + file + }; + + let fileVersion: IFileVersionDomain = new FileVersion(build); + fileVersion = await this.fileService.persistVersion(fileVersion); + file = await this.fileService.update(file); + await this.fileService.uploadFileBase64(fileVersion, payload); + + return new FileDTO(file, [fileVersion]); + } +} + +export default UploadBase64UseCase; diff --git a/src/File/Domain/UseCases/UploadMultipartUseCase.ts b/src/File/Domain/UseCases/UploadMultipartUseCase.ts new file mode 100644 index 00000000..6ffe0c96 --- /dev/null +++ b/src/File/Domain/UseCases/UploadMultipartUseCase.ts @@ -0,0 +1,46 @@ +import FileMultipartRepPayload from '../Payloads/FileMultipartRepPayload'; +import IFileVersionDomain from '../Entities/IFileVersionDomain'; +import FileVersion from '../Entities/FileVersion'; +import FileService from '../Services/FileService'; +import IFileDTO from '../Models/IFileDTO'; +import FileDTO from '../Models/FileDTO'; +import FileMultipartSchemaValidation from '../../Presentation/Validations/FileMultipartSchemaValidation'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +class UploadMultipartUseCase +{ + private fileService = new FileService(); + + async handle(payload: FileMultipartRepPayload): Promise + { + await ValidatorSchema.handle(FileMultipartSchemaValidation, payload); + if (payload.query?.isOptimize && payload.file.isImage) + { + payload = await this.fileService.optimizeMultipartToUpload(payload); + } + payload.file.path = payload.file.path === '/' ? `${payload.file.destination}/${payload.file.originalName}` : payload.file.path; + + let file = await this.fileService.persist(); + + const build = { + isOriginalName: payload.query.isOriginalName, + originalName: payload.file.originalName, + isOptimized: payload.query.isOptimize && payload.file.isImage, + mimeType: payload.file.mimeType, + extension: payload.file.extension, + isPublic: payload.query.isPublic, + size: payload.file.size, + path: payload.file.path, + file + }; + + let fileVersion: IFileVersionDomain = new FileVersion(build); + fileVersion = await this.fileService.persistVersion(fileVersion); + + file = await this.fileService.update(file); + await this.fileService.uploadFileMultipart(fileVersion, payload); + return new FileDTO(file, [fileVersion]); + } +} + +export default UploadMultipartUseCase; diff --git a/src/File/Infrastructure/Repositories/FileMongooseRepository.ts b/src/File/Infrastructure/Repositories/FileMongooseRepository.ts new file mode 100644 index 00000000..09010154 --- /dev/null +++ b/src/File/Infrastructure/Repositories/FileMongooseRepository.ts @@ -0,0 +1,37 @@ +import { Query } from 'mongoose'; +import { IPaginator, ICriteria } from '@digichanges/shared-experience'; + +import FileFilter from '../../Presentation/Criterias/FileFilter'; +import MongoosePaginator from '../../../Main/Infrastructure/Orm/MongoosePaginator'; + +import BaseMongooseRepository from '../../../Main/Infrastructure/Repositories/BaseMongooseRepository'; +import IFileDomain from '../../Domain/Entities/IFileDomain'; +import IFileRepository from './IFileRepository'; +import File from '../../Domain/Entities/File'; +import { FileMongooseDocument } from '../Schemas/FileMongoose'; + +class FileMongooseRepository extends BaseMongooseRepository implements IFileRepository +{ + constructor() + { + super(File.name); + } + + async list(criteria: ICriteria): Promise + { + const queryBuilder: Query = this.repository.find(); + const filter = criteria.getFilter(); + + if (filter.has(FileFilter.NAME)) + { + const name: string = filter.get(FileFilter.NAME) as string; + const rSearch = new RegExp(name, 'g'); + + void queryBuilder.where(FileFilter.NAME).regex(rSearch); + } + + return new MongoosePaginator(queryBuilder, criteria); + } +} + +export default FileMongooseRepository; diff --git a/src/File/Infrastructure/Repositories/FileVersionMongooseRepository.ts b/src/File/Infrastructure/Repositories/FileVersionMongooseRepository.ts new file mode 100644 index 00000000..15f43256 --- /dev/null +++ b/src/File/Infrastructure/Repositories/FileVersionMongooseRepository.ts @@ -0,0 +1,78 @@ +import { NotFoundException, ICriteria } from '@digichanges/shared-experience'; + +import IFileVersionRepository from './IFileVersionRepository'; + +import FileFilter from '../../Presentation/Criterias/FileFilter'; +import IFileVersionDomain from '../../Domain/Entities/IFileVersionDomain'; + +import BaseMongooseRepository from '../../../Main/Infrastructure/Repositories/BaseMongooseRepository'; +import FileVersion from '../../Domain/Entities/FileVersion'; +import { FileVersionMongooseDocument } from '../Schemas/FileVersionMongoose'; +import IByOptions from '../../../Main/Domain/Repositories/IByOptions'; +import mongoose from 'mongoose'; + +class FileVersionMongooseRepository extends BaseMongooseRepository implements IFileVersionRepository +{ + constructor() + { + super(FileVersion.name); + } + + async list(criteria: ICriteria): Promise + { + const queryBuilder: mongoose.Query = this.repository.find(); + const filter = criteria.getFilter(); + + if (filter.has(FileFilter.NAME)) + { + const name: string = filter.get(FileFilter.NAME) as string; + const rSearch = new RegExp(name, 'g'); + + void queryBuilder.where(FileFilter.NAME).regex(rSearch); + } + return this.pagination(queryBuilder, criteria); + } + + async getLastOneByFields(file: string, version: number = null, options: IByOptions = {}): Promise + { + const { initThrow = false } = options; + + let conditions: any = { file }; + + if (version) + { + conditions = { ...conditions, version }; + } + + const [fileVersion] = await this.repository.find(conditions) + .sort({ version: -1 }) + .limit(1) + .populate(this.populate); + + if (!fileVersion && initThrow) + { + throw new NotFoundException(this.entityName); + } + + return fileVersion; + } + + async getOneByFileIdAndVersion(file: string, version: number = null): Promise + { + let conditions: any = { file }; + + if (version) + { + conditions = { ...conditions, version }; + } + + return await this.getOneBy(conditions); + } + + async getAllByFileId(file: string): Promise + { + return await this.getBy({ file }); + } +} + +export default FileVersionMongooseRepository; diff --git a/src/File/Infrastructure/Repositories/IFileRepository.ts b/src/File/Infrastructure/Repositories/IFileRepository.ts new file mode 100644 index 00000000..3d7bd82d --- /dev/null +++ b/src/File/Infrastructure/Repositories/IFileRepository.ts @@ -0,0 +1,12 @@ +import { ICriteria } from '@digichanges/shared-experience'; +import { IPaginator } from '@digichanges/shared-experience'; +import IFileDomain from '../../Domain/Entities/IFileDomain'; +import IBaseRepository from '../../../Main/Domain/Repositories/IBaseRepository'; + +interface IFileRepository extends IBaseRepository +{ + list(criteria: ICriteria): Promise; + getOne(id: string): Promise; +} + +export default IFileRepository; diff --git a/src/File/Infrastructure/Repositories/IFileVersionRepository.ts b/src/File/Infrastructure/Repositories/IFileVersionRepository.ts new file mode 100644 index 00000000..a546c570 --- /dev/null +++ b/src/File/Infrastructure/Repositories/IFileVersionRepository.ts @@ -0,0 +1,15 @@ +import { ICriteria } from '@digichanges/shared-experience'; +import { IPaginator } from '@digichanges/shared-experience'; +import IFileVersionDomain from '../../Domain/Entities/IFileVersionDomain'; +import IBaseRepository from '../../../Main/Domain/Repositories/IBaseRepository'; +import IByOptions from '../../../Main/Domain/Repositories/IByOptions'; + +interface IFileVersionRepository extends IBaseRepository +{ + list(criteria: ICriteria): Promise; + getLastOneByFields(file: string, version?: number, options?: IByOptions): Promise; + getOneByFileIdAndVersion(file: string, version?: number): Promise; + getAllByFileId(file: string): Promise; +} + +export default IFileVersionRepository; diff --git a/src/File/Infrastructure/Schemas/FileMongoose.ts b/src/File/Infrastructure/Schemas/FileMongoose.ts new file mode 100644 index 00000000..a8181fe1 --- /dev/null +++ b/src/File/Infrastructure/Schemas/FileMongoose.ts @@ -0,0 +1,15 @@ +import * as mongoose from 'mongoose'; +import { randomUUID } from 'crypto'; +import File from '../../Domain/Entities/File'; +import IFileDomain from '../../Domain/Entities/IFileDomain'; + +export type FileMongooseDocument = Document & IFileDomain; + +const FileMongoose: any = new mongoose.Schema({ + _id: { type: String, default: randomUUID }, + currentVersion: { type: Number } +}, { timestamps: true }); + +FileMongoose.loadClass(File); + +export default FileMongoose; diff --git a/src/File/Infrastructure/Schemas/FileVersionMongoose.ts b/src/File/Infrastructure/Schemas/FileVersionMongoose.ts new file mode 100644 index 00000000..a585e093 --- /dev/null +++ b/src/File/Infrastructure/Schemas/FileVersionMongoose.ts @@ -0,0 +1,33 @@ +import * as mongoose from 'mongoose'; +import { randomUUID } from 'crypto'; +import FileVersion from '../../Domain/Entities/FileVersion'; +import IFileVersionDomain from '../../Domain/Entities/IFileVersionDomain'; + +export type FileVersionMongooseDocument = Document & IFileVersionDomain; + +const FileVersionSchema: any = new mongoose.Schema({ + _id: { type: String, default: randomUUID }, + name: { type: String, required: true }, + originalName: { type: String, required: true }, + mimeType: { type: String, required: true }, + path: { type: String }, + extension: { type: String }, + size: { type: Number, required: true }, + version: { type: Number, required: true }, + isPublic: { type: Boolean, default: false }, + isOptimized: { type: Boolean, default: false }, + file: { type: String, ref: 'File' } +}, { timestamps: true }); + +FileVersionSchema.index({ + name: 1, + path: 1, + isPublic: 1 +}, +{ + unique: true, + name:'name_path_private_index' +}); +FileVersionSchema.loadClass(FileVersion); + +export default FileVersionSchema; diff --git a/src/File/Presentation/Commands/CreateBucketCommand.ts b/src/File/Presentation/Commands/CreateBucketCommand.ts new file mode 100644 index 00000000..bc9dc0da --- /dev/null +++ b/src/File/Presentation/Commands/CreateBucketCommand.ts @@ -0,0 +1,24 @@ +import Logger from '../../../Shared/Helpers/Logger'; +import commander from 'commander'; +import CreateBucketCommandRequest from '../Requests/CreateBucketCommandRepRequest'; +import CreateBucketPayload from '../../Domain/Payloads/CreateBucketPayload'; +import CreateBucketUseCase from '../../Domain/UseCases/CreateBucketUseCase'; + +const CreateBucketCommand = new commander.Command('createBucket'); + +CreateBucketCommand + .version('0.0.1') + .description('Add bucket to the system') + .option('-b, --name ', 'Bucket name') + .option('-r, --region ', 'Bucket region') + .action(async(env: any) => + { + const useCase = new CreateBucketUseCase(); + const request: CreateBucketPayload = new CreateBucketCommandRequest(env); + + await useCase.handle(request); + + Logger.info('Bucket was created successfully'); + }); + +export default CreateBucketCommand; diff --git a/src/File/Presentation/Controllers/FileFastifyController.ts b/src/File/Presentation/Controllers/FileFastifyController.ts new file mode 100644 index 00000000..fac909c6 --- /dev/null +++ b/src/File/Presentation/Controllers/FileFastifyController.ts @@ -0,0 +1,291 @@ +import { RequestCriteria, ICriteria, IPaginator, StatusCode, IdPayload } from '@digichanges/shared-experience'; +import FileVersionTransformer from '../Transformers/FileVersionTransformer'; +import ObjectTransformer from '../Transformers/ObjectTransformer'; +import FileTransformer from '../Transformers/FileTransformer'; +import FileFilter from '../Criterias/FileFilter'; +import FileSort from '../Criterias/FileSort'; +import Pagination from '../../../Shared/Utils/Pagination'; +import ListFilesUseCase from '../../Domain/UseCases/ListFilesUseCase'; +import ListObjectsUseCase from '../../Domain/UseCases/ListObjectsUseCase'; +import GetFileMetadataUseCase from '../../Domain/UseCases/GetFileMetadataUseCase'; +import FileBase64RepPayload from '../../Domain/Payloads/FileBase64RepPayload'; +import FileBase64SchemaValidation from '../Validations/FileBase64SchemaValidation'; +import UploadBase64UseCase from '../../Domain/UseCases/UploadBase64UseCase'; +import UploadMultipartUseCase from '../../Domain/UseCases/UploadMultipartUseCase'; +import GetPresignedGetObjectUseCase from '../../Domain/UseCases/GetPresignedGetObjectUseCase'; +import PresignedFileRepPayload from '../../Domain/Payloads/PresignedFileRepPayload'; +import DownloadUseCase from '../../Domain/UseCases/DownloadUseCase'; +import OptimizeUseCase from '../../Domain/UseCases/OptimizeUseCase'; +import UpdateFileBase64UseCase from '../../Domain/UseCases/UpdateFileBase64UseCase'; +import IFileDTO from '../../Domain/Models/IFileDTO'; +import FileUpdateBase64Payload from '../../Domain/Payloads/FileUpdateBase64Payload'; +import UpdateFileMultipartUseCase from '../../Domain/UseCases/UpdateFileMultipartUseCase'; +import RemoveFileUseCase from '../../Domain/UseCases/RemoveFileUseCase'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { ParsedQs } from 'qs'; +import FastifyResponder from '../../../Main/Presentation/Utils/FastifyResponder'; +import { IRequestFastify } from '../../../Shared/Utils/types'; +import ValidatorSchema from '../../../Main/Domain/Shared/ValidatorSchema'; + +const responder: FastifyResponder = new FastifyResponder(); + + +class FileController +{ + // TODO implmente IFastify(extends from QueryRequest, bodyRequest, params request) request, or simple request(QueryRequest, bodyRequest, params request) + static async listFiles(request: FastifyRequest, reply: FastifyReply): Promise + { + const { query, url } = request; + + const requestCriteria: ICriteria = new RequestCriteria({ + filter: new FileFilter(query as ParsedQs), + sort: new FileSort(query as ParsedQs), + pagination: new Pagination(query as ParsedQs, url) + }); + + const useCase = new ListFilesUseCase(); + const paginator: IPaginator = await useCase.handle(requestCriteria); + + await responder.paginate(paginator, reply, StatusCode.HTTP_OK, new FileVersionTransformer()); + } + + + static async listObjects(request: any, reply: FastifyReply): Promise + { + const query = request.query; + const payload = { + ...query, + recursive: query.recursive ? query.recursive : undefined, + prefix: query.prefix ? String(query.prefix) : undefined + }; + + const useCase = new ListObjectsUseCase(); + const objects = await useCase.handle(payload); + + void await responder.send(objects, reply, StatusCode.HTTP_OK, new ObjectTransformer()); + } + + static async getFileMetadata(request: FastifyRequest, reply: FastifyReply): Promise + { + const payload = { + id: (request.params as IdPayload).id + }; + + const useCase = new GetFileMetadataUseCase(); + const file = await useCase.handle(payload); + + void await responder.send(file, reply, StatusCode.HTTP_OK, new FileTransformer()); + } + + static async uploadBase64(request: any, reply: FastifyReply): Promise + { + const { filename, base64 } = request.body; + const partialBase64 = base64.split(';base64,'); + const _base64: string = partialBase64.pop(); + const mimeType = partialBase64.shift().split('data:').pop(); + const extension = filename.includes('.') ? filename.split('.').pop() : null; + const { length } = Buffer.from(_base64.substring(_base64.indexOf(',') + 1)); + + const payload = { + ...request.body, + query: request.query, + originalName: filename, + base64: _base64, + mimeType, + extension, + size: length, + isImage: mimeType.includes('image') + }; + + const cleanData = await ValidatorSchema.handle( + FileBase64SchemaValidation, + payload + ); + + const useCase = new UploadBase64UseCase(); + const file = await useCase.handle(cleanData); + + void await responder.send(file, reply, StatusCode.HTTP_CREATED, new FileTransformer()); + } + + static async uploadMultipart(request: any, reply: FastifyReply): Promise + { + const { originalname, encoding, mimetype, destination, filename, size } = request.file; + const { isOriginalName, isPublic, isOverwrite, isOptimize } = request.query; + if (!request.file) + { + return void await responder.send('No file received', reply, StatusCode.HTTP_BAD_REQUEST); + } + const payload = { + file: { + originalName: originalname, + mimeType: mimetype, + destination, + extension: originalname.includes('.') ? originalname.split('.').pop() : '', + filename, + path: '/', + size, + encoding, + isImage: mimetype.includes('image') + }, + query: { + isOriginalName: isOriginalName === 'true', + isPublic: isPublic === 'true', + isOverwrite: isOverwrite === 'true', + isOptimize: isOptimize === 'true' + } + }; + const useCase = new UploadMultipartUseCase(); + const uploadedFile = await useCase.handle(payload); + + void await responder.send(uploadedFile, reply, StatusCode.HTTP_CREATED, new FileTransformer()); + } + static async uploadMultipleImages(request: any, reply: FastifyReply): Promise + { + const files = request.files; + if (!files) + { + return void await responder.send('No files received', reply, StatusCode.HTTP_BAD_REQUEST); + } + const responseFiles = []; + for (const file of files) + { + const { originalname, encoding, mimetype, destination, filename, size } = file; + const { isOriginalName, isPublic, isOverwrite, isOptimize } = request.query; + const payload = { + file: { + originalName: originalname, + mimeType: mimetype, + destination, + extension: originalname.includes('.') ? originalname.split('.').pop() : '', + filename, + path: '/', + size, + encoding, + isImage: mimetype.includes('image') + }, + query: { + isOriginalName: isOriginalName === 'true', + isPublic: isPublic === 'true', + isOverwrite: isOverwrite === 'true', + isOptimize: isOptimize === 'true' + } + }; + + const useCase = new UploadMultipartUseCase(); + responseFiles.push((await useCase.handle(payload))); + } + void await responder.send(responseFiles, reply, StatusCode.HTTP_CREATED, new FileTransformer()); + } + + static async getPresignedGetObject(request: any, reply: FastifyReply): Promise + { + const payload: PresignedFileRepPayload = { + ...request.body, + query: request.query + }; + + const useCase = new GetPresignedGetObjectUseCase(); + const presignedGetObject = await useCase.handle(payload); + + void await responder.send({ presignedGetObject }, reply, StatusCode.HTTP_OK, null); + } + + static async download(request: any, reply: FastifyReply): Promise + { + const payload = { + id: request.params.id, + version: request.query?.version ? +request.query.version : null + }; + + const useCase = new DownloadUseCase(); + const fileDto = await useCase.handle(payload); + await responder.sendStream(fileDto, reply, StatusCode.HTTP_OK); + } + + static async optimize(request: any, reply: FastifyReply): Promise + { + const payload = { + id: request.params.id, + ...request.query + }; + + const useCase = new OptimizeUseCase(); + const file = await useCase.handle(payload); + + void await responder.send(file, reply, StatusCode.HTTP_CREATED, new FileTransformer()); + } + + static async updateBase64(request: any, reply: FastifyReply): Promise + { + const { filename, base64 } = request.body; + const partialBase64 = base64.split(';base64,'); + const _base64: string = partialBase64.pop(); + const mimeType = partialBase64.shift().split('data:').pop(); + const extension = filename.includes('.') ? filename.split('.').pop() : null; + const { length } = Buffer.from(_base64.substring(_base64.indexOf(',') + 1)); + + const payload: FileUpdateBase64Payload = { + ...request.body, + id: request.params.id, + query: request.query, + originalName: request.body.filename as string, + base64: _base64, + mimeType, + extension, + size: length, + isImage: mimeType.includes('image') + }; + + const useCase = new UpdateFileBase64UseCase(); + const file: IFileDTO = await useCase.handle(payload); + + void await responder.send(file, reply, StatusCode.HTTP_CREATED, new FileTransformer()); + } + + static async updateMultipart(request: any, reply: FastifyReply): Promise + { + const { originalname, encoding, mimetype, destination, filename, size } = request.file; + const { isOriginalName, isPublic, isOverwrite, isOptimize } = request.query; + + const payload = { + id: request.params.id, + file: { + originalName: originalname, + mimeType: mimetype, + destination, + extension: originalname.includes('.') ? originalname.split('.').pop() : '', + filename, + path: '/', + size, + encoding, + isImage: mimetype.includes('image') + }, + query: { + isOriginalName: isOriginalName === 'true', + isPublic: isPublic === 'true', + isOverwrite: isOverwrite === 'true', + isOptimize: isOptimize === 'true' + } + }; + + const useCase = new UpdateFileMultipartUseCase(); + const response = await useCase.handle(payload); + + void await responder.send(response, reply, StatusCode.HTTP_CREATED, new FileTransformer()); + } + + static async remove(request: any, reply: FastifyReply): Promise + { + const payload = { + id: request.params.id + }; + + const useCase = new RemoveFileUseCase(); + const file = await useCase.handle(payload); + + void await responder.send(file, reply, StatusCode.HTTP_CREATED, new FileTransformer()); + } +} + +export default FileController; diff --git a/src/File/Presentation/Criterias/FileFilter.ts b/src/File/Presentation/Criterias/FileFilter.ts new file mode 100644 index 00000000..0f9f3fa0 --- /dev/null +++ b/src/File/Presentation/Criterias/FileFilter.ts @@ -0,0 +1,20 @@ +import { Filter } from '@digichanges/shared-experience'; + +class FileFilter extends Filter +{ + static readonly NAME: string = 'name'; + + getFields(): any + { + return [ + FileFilter.NAME + ]; + } + + getDefaultFilters(): any + { + return []; + } +} + +export default FileFilter; diff --git a/src/File/Presentation/Criterias/FileSort.ts b/src/File/Presentation/Criterias/FileSort.ts new file mode 100644 index 00000000..c45ea7af --- /dev/null +++ b/src/File/Presentation/Criterias/FileSort.ts @@ -0,0 +1,22 @@ +import { Sort } from '@digichanges/shared-experience'; + +class FileSort extends Sort +{ + static readonly NAME: string = 'name'; + + getFields(): string[] + { + return [ + FileSort.NAME + ]; + } + + getDefaultSorts(): Record[] + { + return [ + { [FileSort.NAME]: 'asc' } + ]; + } +} + +export default FileSort; diff --git a/src/File/Presentation/DTO/FileBase64OptimizeDTO.ts b/src/File/Presentation/DTO/FileBase64OptimizeDTO.ts new file mode 100644 index 00000000..81bdfc8d --- /dev/null +++ b/src/File/Presentation/DTO/FileBase64OptimizeDTO.ts @@ -0,0 +1,65 @@ +import FileOptionsQueryPayload from '../../Domain/Payloads/FileOptionsQueryPayload'; +import FileBase64RepPayload from '../../Domain/Payloads/FileBase64RepPayload'; + +class FileBase64OptimizeDTO implements FileBase64RepPayload +{ + private readonly _req: FileBase64RepPayload; + private readonly _base64: string; + + constructor(fileRequest: FileBase64RepPayload, _base64: string) + { + this._req = fileRequest; + this._base64 = _base64; + } + + query: FileOptionsQueryPayload; + + get extension(): string + { + return 'webp'; + } + + get base64(): string + { + return this._base64; + } + get isOptimize(): boolean + { + return this._req.query.isOptimize; + } + get isOriginalName(): boolean + { + return this._req.query.isOriginalName; + } + get isOverwrite(): boolean + { + return this._req.query.isOverwrite; + } + get isPublic(): boolean + { + return this._req.isPublic; + } + get mimeType(): string + { + return 'image/webp'; + } + get originalName(): string + { + return this._req.originalName.replace(this._req.extension, 'webp'); + } + get path(): string + { + return '/'; + } + get size(): number + { + return this._req.size; + } + + get isImage(): boolean + { + return this._req.isImage; + } +} + +export default FileBase64OptimizeDTO; diff --git a/src/File/Presentation/DTO/FileMultipartOptimizeDTO.ts b/src/File/Presentation/DTO/FileMultipartOptimizeDTO.ts new file mode 100644 index 00000000..3f86cfe6 --- /dev/null +++ b/src/File/Presentation/DTO/FileMultipartOptimizeDTO.ts @@ -0,0 +1,17 @@ +import FileMultipartRepPayload from '../../Domain/Payloads/FileMultipartRepPayload'; +import IFileVersionPayload from '../../Domain/Entities/IFileVersionPayload'; +import FileOptionsQueryPayload from '../../Domain/Payloads/FileOptionsQueryPayload'; + +class FileMultipartOptimizeDTO implements FileMultipartRepPayload +{ + readonly file: IFileVersionPayload; + readonly query: FileOptionsQueryPayload; + + constructor(file: IFileVersionPayload, query: FileOptionsQueryPayload) + { + this.file = file; + this.query = query; + } +} + +export default FileMultipartOptimizeDTO; diff --git a/src/File/Presentation/DTO/FileUpdateBase64OptimizeDTO.ts b/src/File/Presentation/DTO/FileUpdateBase64OptimizeDTO.ts new file mode 100644 index 00000000..9b6c8ff2 --- /dev/null +++ b/src/File/Presentation/DTO/FileUpdateBase64OptimizeDTO.ts @@ -0,0 +1,20 @@ +import FileBase64OptimizeDTO from './FileBase64OptimizeDTO'; +import FileUpdateBase64Payload from '../../Domain/Payloads/FileUpdateBase64Payload'; + +class FileUpdateBase64OptimizeDTO extends FileBase64OptimizeDTO implements FileUpdateBase64Payload +{ + private readonly _id: string; + + constructor(fileRequest: FileUpdateBase64Payload, _base64: string) + { + super(fileRequest, _base64); + this._id = fileRequest.id; + } + + get id(): string + { + return this._id; + } +} + +export default FileUpdateBase64OptimizeDTO; diff --git a/src/File/Presentation/DTO/FileUpdateMultipartOptimizeDTO.ts b/src/File/Presentation/DTO/FileUpdateMultipartOptimizeDTO.ts new file mode 100644 index 00000000..e97c290b --- /dev/null +++ b/src/File/Presentation/DTO/FileUpdateMultipartOptimizeDTO.ts @@ -0,0 +1,22 @@ +import IFileVersionPayload from '../../Domain/Entities/IFileVersionPayload'; +import FileUpdateMultipartPayload from '../../Domain/Payloads/FileUpdateMultipartPayload'; +import FileMultipartOptimizeDTO from './FileMultipartOptimizeDTO'; +import FileOptionsQueryPayload from '../../Domain/Payloads/FileOptionsQueryPayload'; + +class FileUpdateMultipartOptimizeDTO extends FileMultipartOptimizeDTO implements FileUpdateMultipartPayload +{ + private readonly _id: string; + + constructor(fileVersion: IFileVersionPayload, query: FileOptionsQueryPayload) + { + super(fileVersion, query); + this._id = fileVersion._id; + } + + get id(): string + { + return this._id; + } +} + +export default FileUpdateMultipartOptimizeDTO; diff --git a/src/File/Presentation/DTO/FileVersionOptimizeDTO.ts b/src/File/Presentation/DTO/FileVersionOptimizeDTO.ts new file mode 100644 index 00000000..a2577d8a --- /dev/null +++ b/src/File/Presentation/DTO/FileVersionOptimizeDTO.ts @@ -0,0 +1,57 @@ +import IFileVersionOptimizeDTO from '../../Domain/Payloads/IFileVersionOptimizeDTO'; +import IFileVersionPayload from '../../Domain/Entities/IFileVersionPayload'; +import IFileVersionDomain from '../../Domain/Entities/IFileVersionDomain'; + +class FileVersionOptimizeDTO implements IFileVersionOptimizeDTO +{ + private readonly _file: IFileVersionPayload; + private readonly _lastVersion: IFileVersionDomain; + + constructor(_file: IFileVersionPayload, _lastVersion: IFileVersionDomain) + { + this._file = _file; + this._lastVersion = _lastVersion; + } + + get extension(): string + { + return 'webp'; + } + + get file(): IFileVersionPayload + { + return this._file; + } + + get isImage(): boolean + { + return true; + } + + get isPublic(): boolean + { + return this._lastVersion.isPublic; + } + + get mimeType(): string + { + return this._file.mimeType; + } + + get originalName(): string + { + return this._file.originalName; + } + + get path(): string + { + return '/'; + } + + get size(): number + { + return this._lastVersion.size; + } +} + +export default FileVersionOptimizeDTO; diff --git a/src/File/Presentation/Middlewares/FileFastifyReqMiddleware.ts b/src/File/Presentation/Middlewares/FileFastifyReqMiddleware.ts new file mode 100644 index 00000000..69622dd0 --- /dev/null +++ b/src/File/Presentation/Middlewares/FileFastifyReqMiddleware.ts @@ -0,0 +1,48 @@ +import { FastifyRequest } from 'fastify'; +import fs from 'fs'; +import util from 'node:util'; +import { pipeline } from 'node:stream'; +import { stat } from 'fs/promises'; +const pump = util.promisify(pipeline); + +async function writeFile(fileStream) +{ + const { filename, file } = fileStream; + await pump(file, fs.createWriteStream(`/tmp/${filename}`)); + fileStream.size = (await stat(`/tmp/${filename}`)).size; + fileStream.destination = '/tmp'; + fileStream.originalname = filename; + return fileStream; +} +async function writeFileHandler(request: any) +{ + const fileStream = await request.file(); + request.file = await writeFile(fileStream); +} +async function writeFilesHandler(request: any) +{ + const parts = request.files(); + const files = []; + for await (const part of parts) + { + files.push(await writeFile(part)); + } + request.files = files; +} + + +async function writeFileMiddleware(payload) +{ + return async function(request: FastifyRequest) + { + if (payload === 'file') + { + await writeFileHandler(request); + } + if (payload === 'files') + { + await writeFilesHandler(request); + } + }; +} +export default writeFileMiddleware; diff --git a/src/File/Presentation/RequestTypes/FileRequestTypes.ts b/src/File/Presentation/RequestTypes/FileRequestTypes.ts new file mode 100644 index 00000000..f6eb8e53 --- /dev/null +++ b/src/File/Presentation/RequestTypes/FileRequestTypes.ts @@ -0,0 +1,2 @@ + +export type UploadBase64BodyRq = { filename: string, base64: string } diff --git a/src/File/Presentation/Requests/CreateBucketCommandRepRequest.ts b/src/File/Presentation/Requests/CreateBucketCommandRepRequest.ts new file mode 100644 index 00000000..64525260 --- /dev/null +++ b/src/File/Presentation/Requests/CreateBucketCommandRepRequest.ts @@ -0,0 +1,67 @@ +import CreateBucketPayload from '../../Domain/Payloads/CreateBucketPayload'; + +class CreateBucketCommandRequest implements CreateBucketPayload +{ + private readonly _name: string; + private readonly _publicBucketPolicy: any; + private readonly _privateBucketPolicy: any; + private readonly _region: string; + + constructor(env: any) + { + this._name = env.name; + this._region = env.region; + this._privateBucketPolicy = { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { AWS: '*' }, + Action: [ + 's3:GetBucketLocation' + ], + Resource: 'arn:aws:s3:::*' + } + ] + }; + this._publicBucketPolicy = { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { AWS: '*' }, + Action: [ + 's3:GetBucketLocation', + 's3:ListBucket', + 's3:GetObject' + ], + Resource: [ + 'arn:aws:s3:::*' + ] + } + ] + }; + } + + get name(): string + { + return this._name; + } + + get publicBucketPolicy(): string + { + return JSON.stringify(this._publicBucketPolicy); + } + + get privateBucketPolicy(): string + { + return JSON.stringify(this._privateBucketPolicy); + } + + get region(): string + { + return this._region; + } +} + +export default CreateBucketCommandRequest; diff --git a/src/File/Presentation/Routes/FileFastifyRouter.ts b/src/File/Presentation/Routes/FileFastifyRouter.ts new file mode 100644 index 00000000..c65c4b31 --- /dev/null +++ b/src/File/Presentation/Routes/FileFastifyRouter.ts @@ -0,0 +1,24 @@ +import { FastifyInstance } from 'fastify'; +import FileController from '../Controllers/FileFastifyController'; +import writeFileMiddleware from '../Middlewares/FileFastifyReqMiddleware'; +import multipart from '@fastify/multipart'; + +const FileFastifyRouter = async(fastify: FastifyInstance) => +{ + // @ts-ignore + await fastify.register(multipart); + + fastify.get('/', FileController.listFiles); + fastify.get('/objects', FileController.listObjects); + fastify.get('/metadata/:id', FileController.getFileMetadata); + fastify.post('/base64', FileController.uploadBase64); + fastify.post('/', { preHandler: await writeFileMiddleware('file') }, FileController.uploadMultipart); + fastify.post('/multiple', { preHandler: await writeFileMiddleware('files') }, FileController.uploadMultipleImages); + fastify.post('/presigned-get-object', FileController.getPresignedGetObject); + fastify.get('/:id', FileController.download); + fastify.put('/optimize/:id', FileController.optimize); + fastify.put('/base64/:id', FileController.updateBase64); + fastify.put('/:id', FileController.updateMultipart); + fastify.delete('/:id', FileController.remove); +}; +export default FileFastifyRouter; diff --git a/src/File/Presentation/Transformers/FileTransformer.ts b/src/File/Presentation/Transformers/FileTransformer.ts new file mode 100644 index 00000000..f70cf7a6 --- /dev/null +++ b/src/File/Presentation/Transformers/FileTransformer.ts @@ -0,0 +1,31 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { Transformer } from '@digichanges/shared-experience'; +import IFileDTO from '../../Domain/Models/IFileDTO'; +import IFileTransformer from './IFileTransformer'; +import FileVersionTransformer from './FileVersionTransformer'; + +class FileTransformer extends Transformer +{ + private fileVersionTransformer: FileVersionTransformer; + + constructor() + { + super(); + this.fileVersionTransformer = new FileVersionTransformer(); + } + public async transform(fileDto: IFileDTO): Promise + { + dayjs.extend(utc); + + return { + id: fileDto.file.getId(), + currentVersion: fileDto.file.currentVersion, + // versions: await this.fileVersionTransformer.handle(fileDto.versions) ?? [], + createdAt: dayjs(fileDto.file.createdAt).utc().unix(), + updatedAt: dayjs(fileDto.file.updatedAt).utc().unix() + }; + } +} + +export default FileTransformer; diff --git a/src/File/Presentation/Transformers/FileVersionTransformer.ts b/src/File/Presentation/Transformers/FileVersionTransformer.ts new file mode 100644 index 00000000..503f7cbd --- /dev/null +++ b/src/File/Presentation/Transformers/FileVersionTransformer.ts @@ -0,0 +1,29 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { Transformer } from '@digichanges/shared-experience'; +import IFileVersionDomain from '../../Domain/Entities/IFileVersionDomain'; +import IFileVersionTransformer from './IFileVersionTransformer'; + +class FileVersionTransformer extends Transformer +{ + public async transform(fileVersion: IFileVersionDomain): Promise + { + dayjs.extend(utc); + return { + id: fileVersion.getId(), + name: fileVersion.name, + originalName: fileVersion.originalName, + extension: fileVersion.extension, + path: fileVersion.path, + mimeType: fileVersion.mimeType, + size: fileVersion.size, + version: fileVersion.version, + isPublic: fileVersion.isPublic, + isOptimized: fileVersion.isOptimized, + createdAt: dayjs(fileVersion.createdAt).utc().unix(), + updatedAt: dayjs(fileVersion.updatedAt).utc().unix() + }; + } +} + +export default FileVersionTransformer; diff --git a/src/File/Presentation/Transformers/IFileTransformer.ts b/src/File/Presentation/Transformers/IFileTransformer.ts new file mode 100644 index 00000000..50d2784b --- /dev/null +++ b/src/File/Presentation/Transformers/IFileTransformer.ts @@ -0,0 +1,11 @@ +import { BaseTransformer, BasePropertiesTransformer } from '@digichanges/shared-experience'; +import IFileVersionDomain from '../../Domain/Entities/IFileVersionDomain'; + +interface IFileData { + currentVersion: number; + versions: IFileVersionDomain[]; +} + +type IFileTransformer = BaseTransformer & BasePropertiesTransformer; + +export default IFileTransformer; diff --git a/src/File/Presentation/Transformers/IFileVersionTransformer.ts b/src/File/Presentation/Transformers/IFileVersionTransformer.ts new file mode 100644 index 00000000..deaa8956 --- /dev/null +++ b/src/File/Presentation/Transformers/IFileVersionTransformer.ts @@ -0,0 +1,6 @@ +import { BaseTransformer, BasePropertiesTransformer } from '@digichanges/shared-experience'; +import IFileVersionDomain from '../../Domain/Entities/IFileVersionDomain'; + +type IFileVersionTransformer = BaseTransformer & BasePropertiesTransformer; + +export default IFileVersionTransformer; diff --git a/src/File/Presentation/Transformers/ObjectTransformer.ts b/src/File/Presentation/Transformers/ObjectTransformer.ts new file mode 100644 index 00000000..63ea29f4 --- /dev/null +++ b/src/File/Presentation/Transformers/ObjectTransformer.ts @@ -0,0 +1,19 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { Transformer } from '@digichanges/shared-experience'; + +class ObjectTransformer extends Transformer +{ + public async transform(object: any) + { + dayjs.extend(utc); + return { + name: object.name, + lastModified: dayjs(object.lastModified).utc().unix(), + etag: object.etag, + size: object.size + }; + } +} + +export default ObjectTransformer; diff --git a/src/File/Presentation/Validations/DownloadSchemaValidation.ts b/src/File/Presentation/Validations/DownloadSchemaValidation.ts new file mode 100644 index 00000000..ffa13013 --- /dev/null +++ b/src/File/Presentation/Validations/DownloadSchemaValidation.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; +import IdSchemaValidation from '../../../Main/Domain/Validations/IdSchemaValidation'; + +const PartialDownloadSchemaValidation = z.object({ + version: z.number().min(1).nullish() +}); + +const DownloadSchemaValidation = PartialDownloadSchemaValidation.merge(IdSchemaValidation); + +export default DownloadSchemaValidation; diff --git a/src/File/Presentation/Validations/FileBase64SchemaValidation.ts b/src/File/Presentation/Validations/FileBase64SchemaValidation.ts new file mode 100644 index 00000000..2b21da19 --- /dev/null +++ b/src/File/Presentation/Validations/FileBase64SchemaValidation.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import PartialFileSchemaValidation from './PartialFileSchemaValidation'; + +const FileBase64SchemaValidation = PartialFileSchemaValidation.merge(z.object({ + base64: z.string().min(1) +})); + +export default FileBase64SchemaValidation; diff --git a/src/File/Presentation/Validations/FileBase64UpdateSchemaValidation.ts b/src/File/Presentation/Validations/FileBase64UpdateSchemaValidation.ts new file mode 100644 index 00000000..a6e929e8 --- /dev/null +++ b/src/File/Presentation/Validations/FileBase64UpdateSchemaValidation.ts @@ -0,0 +1,6 @@ +import FileBase64SchemaValidation from './FileBase64SchemaValidation'; +import IdSchemaValidation from '../../../Main/Domain/Validations/IdSchemaValidation'; + +const FileBase64UpdateSchemaValidation = FileBase64SchemaValidation.merge(IdSchemaValidation); + +export default FileBase64UpdateSchemaValidation; diff --git a/src/File/Presentation/Validations/FileMultipartSchemaValidation.ts b/src/File/Presentation/Validations/FileMultipartSchemaValidation.ts new file mode 100644 index 00000000..a9476e10 --- /dev/null +++ b/src/File/Presentation/Validations/FileMultipartSchemaValidation.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; +import FileRepSchemaValidation from './FileRepSchemaValidation'; +import FileOptionsQuerySchemaValidation from './FileOptionsQuerySchemaValidation'; + +const FileMultipartSchemaValidation = z.object({ + file: FileRepSchemaValidation, + query: FileOptionsQuerySchemaValidation +}); + +export default FileMultipartSchemaValidation; diff --git a/src/File/Presentation/Validations/FileMultipartUpdateSchemaValidation.ts b/src/File/Presentation/Validations/FileMultipartUpdateSchemaValidation.ts new file mode 100644 index 00000000..e7dc6018 --- /dev/null +++ b/src/File/Presentation/Validations/FileMultipartUpdateSchemaValidation.ts @@ -0,0 +1,6 @@ +import FileMultipartSchemaValidation from './FileMultipartSchemaValidation'; +import IdSchemaValidation from '../../../Main/Domain/Validations/IdSchemaValidation'; + +const FileMultipartUpdateSchemaValidation = FileMultipartSchemaValidation.merge(IdSchemaValidation); + +export default FileMultipartUpdateSchemaValidation; diff --git a/src/File/Presentation/Validations/FileOptionsQuerySchemaValidation.ts b/src/File/Presentation/Validations/FileOptionsQuerySchemaValidation.ts new file mode 100644 index 00000000..dec7f859 --- /dev/null +++ b/src/File/Presentation/Validations/FileOptionsQuerySchemaValidation.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +const FileOptionsQuerySchemaValidation = z.object({ + isOriginalName: z.boolean().default(true).nullish(), + isPublic: z.boolean().default(true).nullish(), + isOverwrite: z.boolean().default(true).nullish(), + isOptimize: z.boolean().default(true).nullish() +}); + +export default FileOptionsQuerySchemaValidation; diff --git a/src/File/Presentation/Validations/FileRepSchemaValidation.ts b/src/File/Presentation/Validations/FileRepSchemaValidation.ts new file mode 100644 index 00000000..d4e2632e --- /dev/null +++ b/src/File/Presentation/Validations/FileRepSchemaValidation.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +const FileRepSchemaValidation = z.object({ + originalName: z.string().min(1), + mimeType: z.string().min(2), + base64: z.string().min(5).nullish(), + destination: z.string().min(1).nullish(), + extension: z.string(), + filename: z.string().min(4), + path: z.string().min(1).default('/'), + size: z.number().min(1), + encoding: z.string().min(1).nullish(), + isImage: z.boolean().default(false) +}); + +export default FileRepSchemaValidation; diff --git a/src/File/Presentation/Validations/ListObjectsSchemaValidation.ts b/src/File/Presentation/Validations/ListObjectsSchemaValidation.ts new file mode 100644 index 00000000..e4a30585 --- /dev/null +++ b/src/File/Presentation/Validations/ListObjectsSchemaValidation.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; +import FileOptionsQuerySchemaValidation from './FileOptionsQuerySchemaValidation'; + +const ListObjectsSchemaValidation = z.object({ + recursive: z.string().nullish(), + prefix: z.string().nullish() +}).merge(FileOptionsQuerySchemaValidation); + +export default ListObjectsSchemaValidation; diff --git a/src/File/Presentation/Validations/OptimizeSchemaValidation.ts b/src/File/Presentation/Validations/OptimizeSchemaValidation.ts new file mode 100644 index 00000000..0c171b6a --- /dev/null +++ b/src/File/Presentation/Validations/OptimizeSchemaValidation.ts @@ -0,0 +1,7 @@ +import FileOptionsQuerySchemaValidation from './FileOptionsQuerySchemaValidation'; +import IdSchemaValidation from '../../../Main/Domain/Validations/IdSchemaValidation'; + + +const OptimizeSchemaValidation = FileOptionsQuerySchemaValidation.merge(IdSchemaValidation); + +export default OptimizeSchemaValidation; diff --git a/src/File/Presentation/Validations/PartialFileSchemaValidation.ts b/src/File/Presentation/Validations/PartialFileSchemaValidation.ts new file mode 100644 index 00000000..7f5f9dd8 --- /dev/null +++ b/src/File/Presentation/Validations/PartialFileSchemaValidation.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +const PartialFileSchemaValidation = z.object({ + filename: z.string().min(1), + originalName: z.string().min(1), + mimeType: z.string().min(1), + path: z.string().min(1).default('/'), + extension: z.string().min(1), + size: z.number().min(1), + isImage: z.boolean().default(false) +}); + +export default PartialFileSchemaValidation; diff --git a/src/File/Presentation/Validations/PresignedFileSchemaValidation.ts b/src/File/Presentation/Validations/PresignedFileSchemaValidation.ts new file mode 100644 index 00000000..c98615ba --- /dev/null +++ b/src/File/Presentation/Validations/PresignedFileSchemaValidation.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import FileOptionsQuerySchemaValidation from './FileOptionsQuerySchemaValidation'; + +const PresignedFileSchemaValidation = z.object({ + file: z.string().uuid(), + expiry: z.number().min(241920), + version: z.number().min(1), + query: FileOptionsQuerySchemaValidation +}); + +export default PresignedFileSchemaValidation; diff --git a/src/File/Tests/file.handler.spec.ts b/src/File/Tests/file.handler.spec.ts new file mode 100644 index 00000000..d774879e --- /dev/null +++ b/src/File/Tests/file.handler.spec.ts @@ -0,0 +1,188 @@ +import { SuperAgentTest } from 'supertest'; +import initTestServer from '../../initTestServer'; +import { UploadFileBase64 } from './fixture'; +import { IFileResponse } from './types'; +import ICreateConnection from '../../Main/Infrastructure/Database/ICreateConnection'; + + +describe('Start File Test', () => +{ + let request: SuperAgentTest; + let dbConnection: ICreateConnection; + let file_id = ''; + + beforeAll(async() => + { + const configServer = await initTestServer(); + + request = configServer.request; + dbConnection = configServer.dbConnection; + }); + + afterAll((async() => + { + await dbConnection.drop(); + await dbConnection.close(); + })); + + describe('File Success', () => + { + test('Upload File /files/base64', async() => + { + const response: IFileResponse = await request + .post('/api/files/base64') + .set('Accept', 'application/json') + .send(UploadFileBase64); + + const { body: { data } } = response; + + expect(response.statusCode).toStrictEqual(201); + expect(data.currentVersion).toStrictEqual(1); + expect(data.versions.length).toStrictEqual(1); + + file_id = data.id; + }); + + test('Get File /files/metadata/:id', async() => + { + const response = await request + .get(`/api/files/metadata/${file_id}`) + .set('Accept', 'application/json') + .send(); + + const { body: { data } } = response; + + expect(response.statusCode).toStrictEqual(200); + expect(data.id).toStrictEqual(file_id); + }); + + test('Update File /file/base64/:id', async() => + { + const response: IFileResponse = await request + .put(`/api/files/base64/${file_id}`) + .set('Accept', 'application/json') + .send(UploadFileBase64); + + const { body: { data } } = response; + + expect(response.statusCode).toStrictEqual(201); + expect(data.currentVersion).toStrictEqual(2); + expect(data.versions.length).toStrictEqual(2); + }); + + test('Get presigned File /file/presigned-get-object', async() => + { + const response = await request + .post('/api/files/presigned-get-object') + .set('Accept', 'application/json') + .send({ + file: `${file_id}`, + version: 1, + expiry: 241921 + }); + + + const { body: { data } } = response; + + expect(response.statusCode).toStrictEqual(200); + expect(data.presignedGetObject).toBeDefined(); + }); + + test('Get Files /files', async() => + { + const response = await request + .get('/api/files?pagination[limit]=30&pagination[offset]=0') + .set('Accept', 'application/json') + .send(); + + expect(response.statusCode).toStrictEqual(200); + }); + + test('Get Objects /files/objects', async() => + { + const response = await request + .get('/api/files/objects') + .set('Accept', 'application/json') + .send(); + + const { body: { data } } = response; + + expect(response.statusCode).toStrictEqual(200); + }); + + test('Delete file /files/:id', async() => + { + const response = await request + .get(`/api/files/${file_id}`) + .set('Accept', 'application/json') + .send(); + + const { body: { data } } = response; + + expect(response.statusCode).toStrictEqual(200); + }); + }); + + describe('File Failed', () => + { + test('Upload File /files/base64', async() => + { + const response: IFileResponse = await request + .post('/api/files/base64') + .set('Accept', 'application/json') + .send(); + + expect(response.statusCode).toStrictEqual(500); + }); + + test('Get File /files/metadata/:id', async() => + { + const response = await request + .get('/api/files/metadata/123456') + .set('Accept', 'application/json') + .send(); + + const { body } = response; + + expect(response.statusCode).toStrictEqual(422); + expect(body.message).toStrictEqual('Request Failed.'); + expect(body.errors[0].message).toStrictEqual('Invalid uuid'); + }); + + test('Update File /file/base64/:id', async() => + { + const response = await request + .put('/api/files/base64/123456') + .set('Accept', 'application/json') + .send(UploadFileBase64); + + const { body } = response; + + expect(response.statusCode).toStrictEqual(422); + expect(body.message).toStrictEqual('Request Failed.'); + expect(body.errors[0].message).toStrictEqual('Invalid uuid'); + }); + + test('Get presigned File /file/presigned-get-object', async() => + { + const response = await request + .post('/api/files/presigned-get-object') + .set('Accept', 'application/json') + .send({ + file: '123456acd', + version: null, + expiry: 1 + }); + + + const { body } = response; + + expect(response.statusCode).toStrictEqual(422); + expect(body.message).toStrictEqual('Request Failed.'); + expect(body.errors[0].message).toStrictEqual('Invalid uuid'); + expect(body.errors[1].message).toStrictEqual('Number must be greater than or equal to 241920'); + expect(body.errors[2].message).toStrictEqual('Expected number, received null'); + }); + }); +}); + diff --git a/src/File/Tests/fixture.ts b/src/File/Tests/fixture.ts new file mode 100644 index 00000000..60e25212 --- /dev/null +++ b/src/File/Tests/fixture.ts @@ -0,0 +1,5 @@ + +export const UploadFileBase64 = { + filename: 'photo.png', + base64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=' +}; diff --git a/src/File/Tests/types.ts b/src/File/Tests/types.ts new file mode 100644 index 00000000..a3fdb99b --- /dev/null +++ b/src/File/Tests/types.ts @@ -0,0 +1,24 @@ +import { IBodyResponse } from '../../Main/Tests/IBodyResponse'; +import { IFetchResponse } from '../../Main/Tests/IFetchResponse'; +import IFileVersionTransformer from '../Presentation/Transformers/IFileVersionTransformer'; +import IFileTransformer from '../Presentation/Transformers/IFileTransformer'; + +interface IFileBody extends IBodyResponse +{ + data: IFileTransformer; +} + +interface IListFileBody extends IBodyResponse +{ + data: IFileVersionTransformer[]; +} + +export interface IFileResponse extends IFetchResponse +{ + body: IFileBody; +} + +export interface IListFilesResponse extends IFetchResponse +{ + body: IListFileBody; +} diff --git a/src/Main/Infrastructure/Crons/Cron.ts b/src/Main/Infrastructure/Crons/Cron.ts index 0135b9a0..2e5f1c91 100644 --- a/src/Main/Infrastructure/Crons/Cron.ts +++ b/src/Main/Infrastructure/Crons/Cron.ts @@ -10,7 +10,6 @@ abstract class Cron { void (async() => { - console.info(`Running ${this.cronName()}`); await this.task(); })(); }, { diff --git a/src/Main/Infrastructure/Database/CreateMongooseConnection.ts b/src/Main/Infrastructure/Database/CreateMongooseConnection.ts index 7bcda254..9f7e700f 100644 --- a/src/Main/Infrastructure/Database/CreateMongooseConnection.ts +++ b/src/Main/Infrastructure/Database/CreateMongooseConnection.ts @@ -10,6 +10,8 @@ import { PushNotificationSchema } from '../../../Notification/Infrastructure/Schemas/NotificationMongoose'; import ICreateConnection from './ICreateConnection'; +import FileVersionSchema, { FileVersionMongooseDocument } from '../../../File/Infrastructure/Schemas/FileVersionMongoose'; +import FileMongoose, { FileMongooseDocument } from '../../../File/Infrastructure/Schemas/FileMongoose'; type MongooseOptions = { autoIndex: boolean }; @@ -44,7 +46,8 @@ class CreateMongooseConnection implements ICreateConnection // Domain connection.model('Item', ItemSchema); - + connection.model('FileVersion', FileVersionSchema); + connection.model('File', FileMongoose); // Infrastructure const NotificationModel = connection.model('Notification', NotificationSchema); NotificationModel.discriminator('EmailNotification', EmailNotificationSchema); diff --git a/src/Main/Presentation/Http/FastifyBootstrapping.ts b/src/Main/Presentation/Http/FastifyBootstrapping.ts index ec8aab4f..07cdf530 100644 --- a/src/Main/Presentation/Http/FastifyBootstrapping.ts +++ b/src/Main/Presentation/Http/FastifyBootstrapping.ts @@ -9,6 +9,7 @@ import NotificationFastifyHandler from '../../../Notification/Presentation/Handl import IExtendAppConfig from './IExtendAppConfig'; import { AppFastify } from './AppFastify'; import { ErrorFastifyHandler } from '../Middleware/ErrorFastifyHandler'; +import FileFastifyRouter from '../../../File/Presentation/Routes/FileFastifyRouter'; const FastifyBootstrapping = async(config: IExtendAppConfig) => { @@ -44,7 +45,7 @@ const FastifyBootstrapping = async(config: IExtendAppConfig) => await app.addRouter(IndexFastifyRouter, { prefix: '/' }); await app.addRouter(ItemFastifyRouter, { prefix: '/api/items' }); await app.addRouter(NotificationFastifyHandler, { prefix: '/api/notifications' }); - + await app.addRouter(FileFastifyRouter, { prefix: '/api/files' }); return app; }; diff --git a/src/Main/Presentation/Utils/FastifyResponder.ts b/src/Main/Presentation/Utils/FastifyResponder.ts index cbbc30ac..b7840b43 100644 --- a/src/Main/Presentation/Utils/FastifyResponder.ts +++ b/src/Main/Presentation/Utils/FastifyResponder.ts @@ -40,6 +40,19 @@ class FastifyResponder await reply.code(status.code).send({ data, metadata, pagination }); } + public async sendStream(result: any, reply: FastifyReply, status: IHttpStatusCode) + { + const stream = result.stream; + const mimeType = result._metadata.mimeType; + const fileName = result._metadata.name; + + await reply + .code(status.code) + .header('Content-Type', mimeType) + .header('Content-Disposition', `attachment; filename=${fileName}`) + .send(stream); + } + public async error(error: ErrorHttpException, reply: FastifyReply, status: IHttpStatusCode) { await reply.code(status.code).send(this.#formatError.getFormat(error)); diff --git a/src/Shared/DI/Injects/index.ts b/src/Shared/DI/Injects/index.ts index 80ce553c..e1007cc2 100644 --- a/src/Shared/DI/Injects/index.ts +++ b/src/Shared/DI/Injects/index.ts @@ -14,6 +14,9 @@ export enum REPOSITORIES { IItemRepository = 'IItemRepository', INotificationRepository = 'INotificationRepository', + IFileRepository = 'IFileRepository', + IFileVersionRepository = 'IFileVersionRepository', + ICacheRepository = 'ICacheRepository', ICacheDataAccess = 'ICacheDataAccess' diff --git a/src/Shared/DI/container.ts b/src/Shared/DI/container.ts index 958b4f18..1e00aed7 100644 --- a/src/Shared/DI/container.ts +++ b/src/Shared/DI/container.ts @@ -28,6 +28,12 @@ import DatabaseFactory from '../../Main/Infrastructure/Factories/DatabaseFactory import { IMessageBroker } from '../Infrastructure/IMessageBroker'; import RabbitMQMessageBroker from '../Infrastructure/RabbitMQMessageBroker'; import CronService, { ICronService } from '../../Main/Infrastructure/Factories/CronService'; + +import IFileVersionRepository from '../../File/Infrastructure/Repositories/IFileVersionRepository'; +import IFileRepository from '../../File/Infrastructure/Repositories/IFileRepository'; +import FileVersionMongooseRepository from '../../File/Infrastructure/Repositories/FileVersionMongooseRepository'; +import FileMongooseRepository from '../../File/Infrastructure/Repositories/FileMongooseRepository'; + import EventHandler, { IEventHandler } from '../../Notification/Infrastructure/events/EventHandler'; const config = MainConfig.getInstance().getConfig(); @@ -59,6 +65,8 @@ if (defaultDbConfig === 'Mongoose') return repository; }) }, { lifecycle: Lifecycle.Transient }); container.register>(REPOSITORIES.INotificationRepository, { useClass: NotificationMongooseRepository }, { lifecycle: Lifecycle.Singleton }); + container.register(REPOSITORIES.IFileVersionRepository, { useClass: FileVersionMongooseRepository }, { lifecycle: Lifecycle.ContainerScoped }); + container.register(REPOSITORIES.IFileRepository, { useClass: FileMongooseRepository }, { lifecycle: Lifecycle.ContainerScoped }); } else if (defaultDbConfig === 'MikroORM') { diff --git a/src/command.ts b/src/command.ts index 9316b7fb..75a58ce3 100644 --- a/src/command.ts +++ b/src/command.ts @@ -13,6 +13,7 @@ import SyncRolesPermissionCommand from './Auth/Presentation/Commands/SyncRolesPe import Seed from './Main/Presentation/Commands/SeedCommand'; import initCommand from './initCommand'; import Logger from './Shared/Helpers/Logger'; +import CreateBucketCommand from './File/Presentation/Commands/CreateBucketCommand'; void (async() => { @@ -25,7 +26,8 @@ void (async() => program.addCommand(CreateVapID); program.addCommand(SyncRolesPermissionCommand); program.addCommand(Seed); - + program.addCommand(CreateBucketCommand); + program.addCommand(Seed); await program.parseAsync(process.argv); exit(); } diff --git a/src/index.ts b/src/index.ts index be746268..4e115709 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,9 +42,9 @@ void (async() => // Create DB connection const databaseFactory = DependencyInjector.inject(FACTORIES.IDatabaseFactory); const createConnection: ICreateConnection = databaseFactory.create(); + await createConnection.initConfig(); await createConnection.create(); - // Create Cache connection let cache: ICacheDataAccess; diff --git a/tools/dev.init.sh b/tools/dev.init.sh index aec3649e..db730d9a 100644 --- a/tools/dev.init.sh +++ b/tools/dev.init.sh @@ -3,4 +3,5 @@ pnpm command syncRolesPermission pnpm sync:db pnpm command addUser --role Admin --email admin@node.com --firstName super --lastName admin --gender M --phone 541112345678 --country AR --password 12345678 --birthdate 1990-07-01 -pnpm command addUser --role Operator --email user@node.com --firstName node --lastName node --gender M --phone 541112345678 --country AR --password 12345678 --birthdate 1990-07-20 \ No newline at end of file +pnpm command addUser --role Operator --email user@node.com --firstName node --lastName node --gender M --phone 541112345678 --country AR --password 12345678 --birthdate 1990-07-20 +pnpm command createBucket --name experience --region us-east-1 \ No newline at end of file