chore: add service image and audio
This commit is contained in:
+23
-22
@@ -30,7 +30,8 @@
|
|||||||
"prepare": "husky || true"
|
"prepare": "husky || true"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.850.0",
|
"@aws-sdk/client-s3": "^3.879.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.879.0",
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
"@keyv/redis": "^4.6.0",
|
"@keyv/redis": "^4.6.0",
|
||||||
"@nest-lab/fastify-multer": "^1.3.0",
|
"@nest-lab/fastify-multer": "^1.3.0",
|
||||||
@@ -39,12 +40,12 @@
|
|||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/bullmq": "^11.0.3",
|
"@nestjs/bullmq": "^11.0.3",
|
||||||
"@nestjs/cache-manager": "^3.0.1",
|
"@nestjs/cache-manager": "^3.0.1",
|
||||||
"@nestjs/common": "^11.1.5",
|
"@nestjs/common": "^11.1.6",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.1.5",
|
"@nestjs/core": "^11.1.6",
|
||||||
"@nestjs/jwt": "^11.0.0",
|
"@nestjs/jwt": "^11.0.0",
|
||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-fastify": "^11.1.5",
|
"@nestjs/platform-fastify": "^11.1.6",
|
||||||
"@nestjs/swagger": "^11.2.0",
|
"@nestjs/swagger": "^11.2.0",
|
||||||
"@nestjs/throttler": "^6.4.0",
|
"@nestjs/throttler": "^6.4.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
@@ -52,14 +53,14 @@
|
|||||||
"aws-sdk": "^2.1692.0",
|
"aws-sdk": "^2.1692.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^5.56.8",
|
"bullmq": "^5.58.3",
|
||||||
"cache-manager": "^6.4.3",
|
"cache-manager": "^6.4.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"decimal.js": "^10.6.0",
|
"decimal.js": "^10.6.0",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.5.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"nestjs-soap": "^3.0.4",
|
"nestjs-soap": "^3.0.4",
|
||||||
"nestjs-telegraf": "^2.9.1",
|
"nestjs-telegraf": "^2.9.1",
|
||||||
@@ -69,29 +70,29 @@
|
|||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"soap": "^1.2.1",
|
"soap": "^1.3.0",
|
||||||
"telegraf": "^4.16.3",
|
"telegraf": "^4.16.3",
|
||||||
"typeorm": "^0.3.25"
|
"typeorm": "^0.3.26"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.8.1",
|
"@commitlint/cli": "^19.8.1",
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
"@nestjs/cli": "^11.0.8",
|
"@nestjs/cli": "^11.0.10",
|
||||||
"@nestjs/schematics": "^11.0.5",
|
"@nestjs/schematics": "^11.0.7",
|
||||||
"@nestjs/testing": "^11.1.5",
|
"@nestjs/testing": "^11.1.6",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.9",
|
"@types/node": "^20.19.11",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.19",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||||
"@typescript-eslint/parser": "^8.38.0",
|
"@typescript-eslint/parser": "^8.41.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-import-resolver-typescript": "^3.10.1",
|
"eslint-import-resolver-typescript": "^3.10.1",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-prettier": "^5.5.3",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
@@ -99,12 +100,12 @@
|
|||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.1.4",
|
"supertest": "^7.1.4",
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.1",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.38.0"
|
"typescript-eslint": "^8.41.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|||||||
Generated
+1500
-1420
File diff suppressed because it is too large
Load Diff
@@ -223,6 +223,11 @@ export const enum ServiceMessage {
|
|||||||
GUIDE_ORDER_IS_NUMBER = "ترتیب راهنمای سرویس باید یک عدد صحیح باشد",
|
GUIDE_ORDER_IS_NUMBER = "ترتیب راهنمای سرویس باید یک عدد صحیح باشد",
|
||||||
GUIDE_IS_ACTIVE_BOOLEAN = "وضعیت راهنمای سرویس باید یک بولین باشد",
|
GUIDE_IS_ACTIVE_BOOLEAN = "وضعیت راهنمای سرویس باید یک بولین باشد",
|
||||||
GUIDE_TITLE_LENGTH = "عنوان راهنمای سرویس باید بین ۳ تا ۲۰۰ کاراکتر باشد",
|
GUIDE_TITLE_LENGTH = "عنوان راهنمای سرویس باید بین ۳ تا ۲۰۰ کاراکتر باشد",
|
||||||
|
IMAGES_SHOULD_BE_URL = "عکس های سرویس باید یک آدرس یو آر ال باشد",
|
||||||
|
AUDIOS_REQUIRED = "صوت های سرویس مورد نیاز است",
|
||||||
|
AUDIOS_SHOULD_BE_URL = "صوت های سرویس باید یک آدرس یو آر ال باشد",
|
||||||
|
VIDEOS_REQUIRED = "ویدیو های سرویس مورد نیاز است",
|
||||||
|
VIDEOS_SHOULD_BE_URL = "ویدیو های سرویس باید یک آدرس یو آر ال باشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum AnnouncementMessage {
|
export const enum AnnouncementMessage {
|
||||||
@@ -863,3 +868,11 @@ export const enum SupportPlanMessage {
|
|||||||
SUPPORT_PLAN_ALREADY_SUBSCRIBED = "کاربر قبلا اشتراک ثبت کرده است",
|
SUPPORT_PLAN_ALREADY_SUBSCRIBED = "کاربر قبلا اشتراک ثبت کرده است",
|
||||||
DOWN_GRADE_NOT_ALLOWED = "امکان تغییر به پلن پایینتر وجود ندارد",
|
DOWN_GRADE_NOT_ALLOWED = "امکان تغییر به پلن پایینتر وجود ندارد",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum UploaderMessage {
|
||||||
|
FILE_TYPE_NOT_ALLOWED = "نوع فایل مجاز نیست",
|
||||||
|
FILE_SIZE_TOO_SMALL = "فایل کوچکتر از حد مجاز است",
|
||||||
|
UPLOAD_FILE_INVALID = "فایل معتبر نیست",
|
||||||
|
UPLOAD_FILE_TOO_LARGE = "[MAX_FILE_SIZE] فایل بزرگتر از حد مجاز است",
|
||||||
|
UPLOAD_FILE_TYPE_NOT_SUPPORTED = "[MIME_TYPES] نوع فایل مجاز نیست",
|
||||||
|
}
|
||||||
|
|||||||
+20
-17
@@ -1,26 +1,29 @@
|
|||||||
|
import { FactoryProvider } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
export function S3Configs() {
|
import { S3_CONFIG } from "../modules/utils/constants";
|
||||||
return {
|
|
||||||
inject: [ConfigService],
|
export const S3ConfigsProvider: FactoryProvider<IS3Configs> = {
|
||||||
useFactory(configService: ConfigService) {
|
provide: S3_CONFIG,
|
||||||
return {
|
|
||||||
accessKeyId: configService.getOrThrow<string>("BUCKET_ACCESS_KEY"),
|
inject: [ConfigService],
|
||||||
secretAccessKey: configService.getOrThrow<string>("BUCKET_SECRET_KEY"),
|
useFactory(configService: ConfigService) {
|
||||||
endpoint: configService.getOrThrow<string>("BUCKET_URL"),
|
return {
|
||||||
region: configService.getOrThrow<string>("BUCKET_REGION"),
|
accessKeyId: configService.getOrThrow<string>("BUCKET_ACCESS_KEY"),
|
||||||
bucket: configService.getOrThrow<string>("BUCKET_NAME"),
|
secretAccessKey: configService.getOrThrow<string>("BUCKET_SECRET_KEY"),
|
||||||
url: configService.getOrThrow<string>("BUCKET_UPLOAD_URL"),
|
endpoint: configService.getOrThrow<string>("BUCKET_URL"),
|
||||||
};
|
region: configService.getOrThrow<string>("BUCKET_REGION"),
|
||||||
},
|
bucketName: configService.getOrThrow<string>("BUCKET_NAME"),
|
||||||
};
|
bucketUploadedUrl: configService.getOrThrow<string>("BUCKET_UPLOAD_URL"),
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export interface IS3Configs {
|
export interface IS3Configs {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
region: string;
|
region: string;
|
||||||
bucket: string;
|
bucketName: string;
|
||||||
url: string;
|
bucketUploadedUrl: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-37
@@ -1,42 +1,44 @@
|
|||||||
|
import { FactoryProvider } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
export function smsConfigs() {
|
import { SMS_CONFIG } from "../modules/utils/constants";
|
||||||
return {
|
|
||||||
inject: [ConfigService],
|
export const smsConfigsProvider: FactoryProvider<ISmsConfigs> = {
|
||||||
useFactory(configService: ConfigService) {
|
provide: SMS_CONFIG,
|
||||||
return {
|
inject: [ConfigService],
|
||||||
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
|
useFactory(configService: ConfigService) {
|
||||||
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
|
return {
|
||||||
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
|
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
|
||||||
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
|
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
|
||||||
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
|
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
|
||||||
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
|
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
|
||||||
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
|
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
|
||||||
SMS_PATTERN_WALLET_CHARGE: configService.getOrThrow<string>("SMS_PATTERN_WALLET_CHARGE"),
|
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
|
||||||
SMS_PATTERN_WALLET_DEDUCTION: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
|
||||||
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
|
SMS_PATTERN_WALLET_CHARGE: configService.getOrThrow<string>("SMS_PATTERN_WALLET_CHARGE"),
|
||||||
SMS_PATTERN_TICKET_ANSWERED: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
SMS_PATTERN_WALLET_DEDUCTION: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
||||||
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
|
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
|
||||||
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
|
SMS_PATTERN_TICKET_ANSWERED: configService.getOrThrow<string>("SMS_PATTERN_WALLET_DEDUCTION"),
|
||||||
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
|
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
|
||||||
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
|
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
|
||||||
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
|
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
|
||||||
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
|
||||||
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
|
||||||
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
|
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
||||||
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_CANCELLATION"),
|
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
||||||
//
|
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
|
||||||
SMS_PATTERN_BLOG_NEW_COMMENT: configService.getOrThrow<string>("SMS_PATTERN_BLOG_NEW_COMMENT"),
|
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_CANCELLATION"),
|
||||||
SMS_PATTERN_NEW_SERVICE_REVIEW: configService.getOrThrow<string>("SMS_PATTERN_NEW_SERVICE_REVIEW"),
|
//
|
||||||
SMS_PATTERN_NEW_CUSTOMER: configService.getOrThrow<string>("SMS_PATTERN_NEW_CUSTOMER"),
|
SMS_PATTERN_BLOG_NEW_COMMENT: configService.getOrThrow<string>("SMS_PATTERN_BLOG_NEW_COMMENT"),
|
||||||
SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow<string>("SMS_PATTERN_NEW_SUBSCRIPTION"),
|
SMS_PATTERN_NEW_SERVICE_REVIEW: configService.getOrThrow<string>("SMS_PATTERN_NEW_SERVICE_REVIEW"),
|
||||||
SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow<string>("SMS_PATTERN_NEW_TICKET_GLOBAL"),
|
SMS_PATTERN_NEW_CUSTOMER: configService.getOrThrow<string>("SMS_PATTERN_NEW_CUSTOMER"),
|
||||||
SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow<string>("SMS_PATTERN_NEW_CRITICISM"),
|
SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow<string>("SMS_PATTERN_NEW_SUBSCRIPTION"),
|
||||||
SMS_PATTERN_USER_PASSWORD: configService.getOrThrow<string>("SMS_PATTERN_USER_PASSWORD"),
|
SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow<string>("SMS_PATTERN_NEW_TICKET_GLOBAL"),
|
||||||
};
|
SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow<string>("SMS_PATTERN_NEW_CRITICISM"),
|
||||||
},
|
SMS_PATTERN_USER_PASSWORD: configService.getOrThrow<string>("SMS_PATTERN_USER_PASSWORD"),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export interface ISmsConfigs {
|
export interface ISmsConfigs {
|
||||||
API_URL: string;
|
API_URL: string;
|
||||||
|
|||||||
@@ -109,9 +109,24 @@ export class CreateServiceDto {
|
|||||||
|
|
||||||
@IsNotEmpty({ message: ServiceMessage.IMAGES_REQUIRED })
|
@IsNotEmpty({ message: ServiceMessage.IMAGES_REQUIRED })
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
|
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.IMAGES_SHOULD_BE_URL })
|
||||||
@ApiProperty({ description: "The images of the service", example: ["https://example.com/image1.png", "https://example.com/image2.png"] })
|
@ApiProperty({ description: "The images of the service", example: ["https://example.com/image1.png", "https://example.com/image2.png"] })
|
||||||
images: string[];
|
images: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: ServiceMessage.AUDIOS_REQUIRED })
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.AUDIOS_SHOULD_BE_URL })
|
||||||
|
@ApiProperty({ description: "The audios of the service", example: ["https://example.com/audio1.mp3", "https://example.com/audio2.mp3"] })
|
||||||
|
audios?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: ServiceMessage.VIDEOS_REQUIRED })
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.VIDEOS_SHOULD_BE_URL })
|
||||||
|
@ApiProperty({ description: "The videos of the service", example: ["https://example.com/video1.mp4", "https://example.com/video2.mp4"] })
|
||||||
|
videos?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNotEmpty({ message: ServiceMessage.COVER_URL_REQUIRED })
|
@IsNotEmpty({ message: ServiceMessage.COVER_URL_REQUIRED })
|
||||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.COVER_URL_SHOULD_BE_URL })
|
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: ServiceMessage.COVER_URL_SHOULD_BE_URL })
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export class DanakServicesController {
|
|||||||
@PermissionsDec(PermissionEnum.SERVICES)
|
@PermissionsDec(PermissionEnum.SERVICES)
|
||||||
@ApiOperation({ summary: "create new danak services => admin route" })
|
@ApiOperation({ summary: "create new danak services => admin route" })
|
||||||
@Post()
|
@Post()
|
||||||
createService(@Body() createDto: CreateServiceDto) {
|
createService(@Body() createDto: CreateServiceDto, @UserDec("id") userId: string) {
|
||||||
return this.danakServicesService.createService(createDto);
|
return this.danakServicesService.createService(createDto, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
@@ -185,8 +185,8 @@ export class DanakServicesController {
|
|||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
@PermissionsDec(PermissionEnum.SERVICES)
|
@PermissionsDec(PermissionEnum.SERVICES)
|
||||||
@Patch(":id")
|
@Patch(":id")
|
||||||
updateDanakService(@Param() paramDto: ParamDto, @Body() updateDto: UpdateServiceDto) {
|
updateDanakService(@Param() paramDto: ParamDto, @Body() updateDto: UpdateServiceDto, @UserDec("id") userId: string) {
|
||||||
return this.danakServicesService.updateDanakService(paramDto.id, updateDto);
|
return this.danakServicesService.updateDanakService(paramDto.id, updateDto, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "delete danak service" })
|
@ApiOperation({ summary: "delete danak service" })
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import { DanakServiceReviewRepository } from "./repositories/danak-services-revi
|
|||||||
import { DanakServicesRepository } from "./repositories/danak-services.repository";
|
import { DanakServicesRepository } from "./repositories/danak-services.repository";
|
||||||
import { NotificationModule } from "../notifications/notifications.module";
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
|
import { DanakServiceAudio } from "./entities/danak-service-audio.entity";
|
||||||
|
import { DanakServiceVideo } from "./entities/danak-service-video.entity";
|
||||||
|
import { DanakServicesAudioRepository } from "./repositories/danak-services-audio.repository";
|
||||||
|
import { DanakServicesVideoRepository } from "./repositories/danak-services-video.repository";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -27,6 +31,8 @@ import { UsersModule } from "../users/users.module";
|
|||||||
DanakServiceReview,
|
DanakServiceReview,
|
||||||
DanakServiceFeedback,
|
DanakServiceFeedback,
|
||||||
DanakServiceGuide,
|
DanakServiceGuide,
|
||||||
|
DanakServiceAudio,
|
||||||
|
DanakServiceVideo,
|
||||||
]),
|
]),
|
||||||
UsersModule,
|
UsersModule,
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
@@ -39,6 +45,8 @@ import { UsersModule } from "../users/users.module";
|
|||||||
DanakServiceReviewRepository,
|
DanakServiceReviewRepository,
|
||||||
DanakServiceFeedbackRepository,
|
DanakServiceFeedbackRepository,
|
||||||
DanakServiceGuideRepository,
|
DanakServiceGuideRepository,
|
||||||
|
DanakServicesAudioRepository,
|
||||||
|
DanakServicesVideoRepository,
|
||||||
],
|
],
|
||||||
controllers: [DanakServicesController],
|
controllers: [DanakServicesController],
|
||||||
exports: [DanakServicesService],
|
exports: [DanakServicesService],
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Column, Entity, ManyToOne } from "typeorm";
|
||||||
|
|
||||||
|
import { DanakService } from "./danak-service.entity";
|
||||||
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class DanakServiceAudio extends BaseEntity {
|
||||||
|
@ManyToOne(() => DanakService, (danakService) => danakService.audios, { nullable: false, onDelete: "CASCADE" })
|
||||||
|
danakService: DanakService;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255, nullable: false })
|
||||||
|
audioUrl: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.audios, { nullable: false, onDelete: "SET NULL" })
|
||||||
|
createBy: User;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Column, Entity, ManyToOne } from "typeorm";
|
||||||
|
|
||||||
|
import { DanakService } from "./danak-service.entity";
|
||||||
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class DanakServiceVideo extends BaseEntity {
|
||||||
|
@ManyToOne(() => DanakService, (danakService) => danakService.videos, { nullable: false, onDelete: "CASCADE" })
|
||||||
|
danakService: DanakService;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255, nullable: false })
|
||||||
|
videoUrl: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.videos, { nullable: false, onDelete: "SET NULL" })
|
||||||
|
createBy: User;
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Column, DeleteDateColumn, Entity, Index, ManyToOne, OneToMany } from "typeorm";
|
import { Column, DeleteDateColumn, Entity, Index, ManyToOne, OneToMany } from "typeorm";
|
||||||
|
|
||||||
|
import { DanakServiceAudio } from "./danak-service-audio.entity";
|
||||||
import { DanakServiceCategory } from "./danak-service-category.entity";
|
import { DanakServiceCategory } from "./danak-service-category.entity";
|
||||||
import { DanakServiceFeedback } from "./danak-service-feedback.entity";
|
import { DanakServiceFeedback } from "./danak-service-feedback.entity";
|
||||||
import { DanakServiceGuide } from "./danak-service-guide.entity";
|
import { DanakServiceGuide } from "./danak-service-guide.entity";
|
||||||
import { DanakServiceImage } from "./danak-service-image.entity";
|
import { DanakServiceImage } from "./danak-service-image.entity";
|
||||||
import { DanakServiceReview } from "./danak-service-review.entity";
|
import { DanakServiceReview } from "./danak-service-review.entity";
|
||||||
|
import { DanakServiceVideo } from "./danak-service-video.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { Ads } from "../../ads/entities/ads.entity";
|
import { Ads } from "../../ads/entities/ads.entity";
|
||||||
import { Announcement } from "../../announcements/entities/announcement.entity";
|
import { Announcement } from "../../announcements/entities/announcement.entity";
|
||||||
@@ -79,6 +81,12 @@ export class DanakService extends BaseEntity {
|
|||||||
@OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { cascade: true })
|
@OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { cascade: true })
|
||||||
images: DanakServiceImage[];
|
images: DanakServiceImage[];
|
||||||
|
|
||||||
|
@OneToMany(() => DanakServiceAudio, (danakServiceAudio) => danakServiceAudio.danakService, { cascade: true })
|
||||||
|
audios: DanakServiceAudio[];
|
||||||
|
|
||||||
|
@OneToMany(() => DanakServiceVideo, (danakServiceVideo) => danakServiceVideo.danakService, { cascade: true })
|
||||||
|
videos: DanakServiceVideo[];
|
||||||
|
|
||||||
@OneToMany(() => Announcement, (announcement) => announcement.targetService)
|
@OneToMany(() => Announcement, (announcement) => announcement.targetService)
|
||||||
announcements: Announcement[];
|
announcements: Announcement[];
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,18 @@ import { UpdateDanakServiceCategoryDto } from "../DTO/update-category.dto";
|
|||||||
import { UpdateReviewStatusDto } from "../DTO/update-review-status.dto";
|
import { UpdateReviewStatusDto } from "../DTO/update-review-status.dto";
|
||||||
import { UpdateServiceGuideDto } from "../DTO/update-service-guide.dto";
|
import { UpdateServiceGuideDto } from "../DTO/update-service-guide.dto";
|
||||||
import { UpdateServiceDto } from "../DTO/update-service.dto";
|
import { UpdateServiceDto } from "../DTO/update-service.dto";
|
||||||
|
import { DanakServiceAudio } from "../entities/danak-service-audio.entity";
|
||||||
import { DanakServiceCategory } from "../entities/danak-service-category.entity";
|
import { DanakServiceCategory } from "../entities/danak-service-category.entity";
|
||||||
import { DanakServiceImage } from "../entities/danak-service-image.entity";
|
import { DanakServiceImage } from "../entities/danak-service-image.entity";
|
||||||
|
import { DanakServiceVideo } from "../entities/danak-service-video.entity";
|
||||||
import { ReviewStatus } from "../enums/review-status.enum";
|
import { ReviewStatus } from "../enums/review-status.enum";
|
||||||
import { DanakServiceFeedbackRepository } from "../repositories/danak-service-feedback.repository";
|
import { DanakServiceFeedbackRepository } from "../repositories/danak-service-feedback.repository";
|
||||||
import { DanakServiceGuideRepository } from "../repositories/danak-service-guide.repository";
|
import { DanakServiceGuideRepository } from "../repositories/danak-service-guide.repository";
|
||||||
|
import { DanakServicesAudioRepository } from "../repositories/danak-services-audio.repository";
|
||||||
import { DanakServicesCategoryRepository } from "../repositories/danak-services-category.repository";
|
import { DanakServicesCategoryRepository } from "../repositories/danak-services-category.repository";
|
||||||
import { DanakServicesImageRepository } from "../repositories/danak-services-image.repository";
|
import { DanakServicesImageRepository } from "../repositories/danak-services-image.repository";
|
||||||
import { DanakServiceReviewRepository } from "../repositories/danak-services-review.repository";
|
import { DanakServiceReviewRepository } from "../repositories/danak-services-review.repository";
|
||||||
|
import { DanakServicesVideoRepository } from "../repositories/danak-services-video.repository";
|
||||||
import { DanakServicesRepository } from "../repositories/danak-services.repository";
|
import { DanakServicesRepository } from "../repositories/danak-services.repository";
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DanakServicesService {
|
export class DanakServicesService {
|
||||||
@@ -42,6 +46,8 @@ export class DanakServicesService {
|
|||||||
private readonly danakServiceFeedbackRepository: DanakServiceFeedbackRepository,
|
private readonly danakServiceFeedbackRepository: DanakServiceFeedbackRepository,
|
||||||
private readonly danakServiceGuideRepository: DanakServiceGuideRepository,
|
private readonly danakServiceGuideRepository: DanakServiceGuideRepository,
|
||||||
private readonly danakServicesImageRepository: DanakServicesImageRepository,
|
private readonly danakServicesImageRepository: DanakServicesImageRepository,
|
||||||
|
private readonly danakServicesAudioRepository: DanakServicesAudioRepository,
|
||||||
|
private readonly danakServicesVideoRepository: DanakServicesVideoRepository,
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
private readonly adminsService: AdminsService,
|
private readonly adminsService: AdminsService,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
@@ -279,7 +285,7 @@ export class DanakServicesService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
/******************************************** */
|
/******************************************** */
|
||||||
async createService(createDto: CreateServiceDto) {
|
async createService(createDto: CreateServiceDto, userId: string) {
|
||||||
const category = await this.danakServicesCategoryRepository.findOneById(createDto.categoryId);
|
const category = await this.danakServicesCategoryRepository.findOneById(createDto.categoryId);
|
||||||
if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST);
|
if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST);
|
||||||
|
|
||||||
@@ -292,7 +298,23 @@ export class DanakServicesService {
|
|||||||
return { imageUrl: image };
|
return { imageUrl: image };
|
||||||
});
|
});
|
||||||
|
|
||||||
const service = this.danakServicesRepository.create({ ...createDto, category, images, slug });
|
let audios: Partial<DanakServiceAudio>[] = [];
|
||||||
|
|
||||||
|
if (createDto.audios) {
|
||||||
|
audios = createDto.audios.map((audio) => {
|
||||||
|
return { audioUrl: audio, createBy: { id: userId } as User };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let videos: Partial<DanakServiceVideo>[] = [];
|
||||||
|
|
||||||
|
if (createDto.videos) {
|
||||||
|
videos = createDto.videos.map((video) => {
|
||||||
|
return { videoUrl: video, createBy: { id: userId } as User };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = this.danakServicesRepository.create({ ...createDto, category, images, audios, videos, slug });
|
||||||
await this.danakServicesRepository.save(service);
|
await this.danakServicesRepository.save(service);
|
||||||
return {
|
return {
|
||||||
message: CommonMessage.CREATED,
|
message: CommonMessage.CREATED,
|
||||||
@@ -300,10 +322,12 @@ export class DanakServicesService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
/******************************************** */
|
/******************************************** */
|
||||||
async updateDanakService(serviceId: string, updateDto: UpdateServiceDto) {
|
async updateDanakService(serviceId: string, updateDto: UpdateServiceDto, userId: string) {
|
||||||
const service = await this.findServiceById(serviceId);
|
const service = await this.findServiceById(serviceId);
|
||||||
|
|
||||||
let images: DanakServiceImage[] = [];
|
let images: DanakServiceImage[] = [];
|
||||||
|
let audios: DanakServiceAudio[] = [];
|
||||||
|
let videos: DanakServiceVideo[] = [];
|
||||||
|
|
||||||
if (updateDto.categoryId) {
|
if (updateDto.categoryId) {
|
||||||
const category = await this.danakServicesCategoryRepository.findOneById(updateDto.categoryId);
|
const category = await this.danakServicesCategoryRepository.findOneById(updateDto.categoryId);
|
||||||
@@ -331,6 +355,37 @@ export class DanakServicesService {
|
|||||||
images = newImages;
|
images = newImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateDto.audios) {
|
||||||
|
await this.danakServicesAudioRepository.delete({ danakService: { id: serviceId } });
|
||||||
|
|
||||||
|
const newAudios = updateDto.audios.map((audio) => {
|
||||||
|
return this.danakServicesAudioRepository.create({
|
||||||
|
audioUrl: audio,
|
||||||
|
danakService: service,
|
||||||
|
createBy: { id: userId } as User,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await this.danakServicesAudioRepository.save(newAudios);
|
||||||
|
|
||||||
|
audios = newAudios;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateDto.videos) {
|
||||||
|
await this.danakServicesVideoRepository.delete({ danakService: { id: serviceId } });
|
||||||
|
|
||||||
|
const newVideos = updateDto.videos.map((video) => {
|
||||||
|
return this.danakServicesVideoRepository.create({
|
||||||
|
videoUrl: video,
|
||||||
|
danakService: service,
|
||||||
|
createBy: { id: userId } as User,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.danakServicesVideoRepository.save(newVideos);
|
||||||
|
|
||||||
|
videos = newVideos;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateDto.slug) {
|
if (updateDto.slug) {
|
||||||
const existSlug = await this.danakServicesRepository.findOne({ where: { slug: updateDto.slug, id: Not(serviceId) } });
|
const existSlug = await this.danakServicesRepository.findOne({ where: { slug: updateDto.slug, id: Not(serviceId) } });
|
||||||
if (existSlug) throw new BadRequestException(ServiceMessage.SERVICE_ALREADY_EXISTS_WITH_THIS_SLUG);
|
if (existSlug) throw new BadRequestException(ServiceMessage.SERVICE_ALREADY_EXISTS_WITH_THIS_SLUG);
|
||||||
@@ -341,6 +396,8 @@ export class DanakServicesService {
|
|||||||
...updateDto,
|
...updateDto,
|
||||||
slug: this.generateSlug(updateDto.slug ?? service.slug ?? service.name),
|
slug: this.generateSlug(updateDto.slug ?? service.slug ?? service.name),
|
||||||
images: images.length ? images : service.images,
|
images: images.length ? images : service.images,
|
||||||
|
audios: audios.length ? audios : service.audios,
|
||||||
|
videos: videos.length ? videos : service.videos,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
message: CommonMessage.UPDATE_SUCCESS,
|
message: CommonMessage.UPDATE_SUCCESS,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { DanakServiceAudio } from "../entities/danak-service-audio.entity";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DanakServicesAudioRepository extends Repository<DanakServiceAudio> {
|
||||||
|
constructor(@InjectRepository(DanakServiceAudio) danakServicesAudioRepository: Repository<DanakServiceAudio>) {
|
||||||
|
super(danakServicesAudioRepository.target, danakServicesAudioRepository.manager, danakServicesAudioRepository.queryRunner);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { DanakServiceVideo } from "../entities/danak-service-video.entity";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DanakServicesVideoRepository extends Repository<DanakServiceVideo> {
|
||||||
|
constructor(@InjectRepository(DanakServiceVideo) danakServicesVideoRepository: Repository<DanakServiceVideo>) {
|
||||||
|
super(danakServicesVideoRepository.target, danakServicesVideoRepository.manager, danakServicesVideoRepository.queryRunner);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,8 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
|||||||
.leftJoin("service.category", "category")
|
.leftJoin("service.category", "category")
|
||||||
.addSelect(["category.id", "category.title"])
|
.addSelect(["category.id", "category.title"])
|
||||||
.leftJoinAndSelect("service.images", "images")
|
.leftJoinAndSelect("service.images", "images")
|
||||||
|
.leftJoinAndSelect("service.audios", "audios")
|
||||||
|
.leftJoinAndSelect("service.videos", "videos")
|
||||||
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
|
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
|
||||||
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
||||||
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
||||||
@@ -122,7 +124,7 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
|||||||
// "averageRating",
|
// "averageRating",
|
||||||
// )
|
// )
|
||||||
.andWhere("service.id = :serviceId", { serviceId })
|
.andWhere("service.id = :serviceId", { serviceId })
|
||||||
.groupBy("service.id,category.id, images.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
.groupBy("service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
serviceQueryBuilder
|
serviceQueryBuilder
|
||||||
@@ -141,6 +143,8 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
|||||||
.leftJoin("service.category", "category")
|
.leftJoin("service.category", "category")
|
||||||
.addSelect(["category.id", "category.title"])
|
.addSelect(["category.id", "category.title"])
|
||||||
.leftJoinAndSelect("service.images", "images")
|
.leftJoinAndSelect("service.images", "images")
|
||||||
|
.leftJoinAndSelect("service.audios", "audios")
|
||||||
|
.leftJoinAndSelect("service.videos", "videos")
|
||||||
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
|
.leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans")
|
||||||
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
||||||
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
||||||
@@ -168,7 +172,7 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
|||||||
// "averageRating",
|
// "averageRating",
|
||||||
// )
|
// )
|
||||||
.andWhere("service.slug = :slug", { slug })
|
.andWhere("service.slug = :slug", { slug })
|
||||||
.groupBy("service.id,category.id, images.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
.groupBy("service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
serviceQueryBuilder
|
serviceQueryBuilder
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import { File } from "@nest-lab/fastify-multer";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { IFile } from "../../utils/interfaces/IFile";
|
|
||||||
|
|
||||||
export class UploadSingleFileDto {
|
export class UploadSingleFileDto {
|
||||||
@ApiProperty({ type: "string", format: "binary", nullable: false })
|
@ApiProperty({ type: "string", format: "binary", nullable: false })
|
||||||
file: IFile;
|
file: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UploadMultipleFileDto {
|
export class UploadMultipleFileDto {
|
||||||
@@ -16,5 +15,5 @@ export class UploadMultipleFileDto {
|
|||||||
format: "binary",
|
format: "binary",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
files: IFile[];
|
files: File[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { File } from "@nest-lab/fastify-multer";
|
||||||
|
|
||||||
|
export interface FileUploadResult {
|
||||||
|
file: File;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileStreamResult {
|
||||||
|
stream: NodeJS.ReadableStream;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileMetadata {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
duration?: number;
|
||||||
|
bitrate?: number;
|
||||||
|
codec?: string;
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileType = "video" | "audio" | "image" | "document" | "text" | "unknown";
|
||||||
|
|
||||||
|
export interface FileValidationOptions {
|
||||||
|
maxFileSize: number;
|
||||||
|
allowedMimeTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadOptions {
|
||||||
|
roomId?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File processing status
|
||||||
|
export type FileProcessingStatus = "pending" | "processing" | "completed" | "failed";
|
||||||
|
|
||||||
|
export interface FileProcessingResult {
|
||||||
|
status: FileProcessingStatus;
|
||||||
|
metadata?: FileMetadata;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export interface S3UploadResult {
|
||||||
|
key: string;
|
||||||
|
url: string;
|
||||||
|
bucket: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3UploadOptions {
|
||||||
|
contentType: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
acl?: "private" | "public-read" | "public-read-write";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3DownloadOptions {
|
||||||
|
expiresIn?: number;
|
||||||
|
responseContentType?: string;
|
||||||
|
responseContentDisposition?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3FileInfo {
|
||||||
|
key: string;
|
||||||
|
bucket: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: Date;
|
||||||
|
contentType: string;
|
||||||
|
etag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3DeleteResult {
|
||||||
|
deleted: boolean;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3ListResult {
|
||||||
|
files: S3FileInfo[];
|
||||||
|
continuationToken?: string;
|
||||||
|
isTruncated: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export interface FileSearchOptions {
|
||||||
|
userId?: string;
|
||||||
|
roomId?: string;
|
||||||
|
fileType?: "video" | "audio" | "subtitle";
|
||||||
|
isActive?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
sortBy?: "uploadedAt" | "size" | "originalName";
|
||||||
|
sortOrder?: "ASC" | "DESC";
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { Readable } from "stream";
|
||||||
|
|
||||||
|
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { IS3Configs } from "../../../configs/s3.config";
|
||||||
|
import { S3_CONFIG } from "../../utils/constants";
|
||||||
|
import { FileType } from "../interfaces/file.interface";
|
||||||
|
import { S3UploadResult } from "../interfaces/s3.interface";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class S3Service {
|
||||||
|
private readonly logger = new Logger(S3Service.name);
|
||||||
|
private readonly s3Client: S3Client;
|
||||||
|
|
||||||
|
constructor(@Inject(S3_CONFIG) private readonly s3Configs: IS3Configs) {
|
||||||
|
this.s3Client = new S3Client({
|
||||||
|
region: this.s3Configs.region,
|
||||||
|
endpoint: this.s3Configs.endpoint,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: this.s3Configs.accessKeyId,
|
||||||
|
secretAccessKey: this.s3Configs.secretAccessKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload file to S3
|
||||||
|
*/
|
||||||
|
async uploadFile(buffer: Buffer, key: string, contentType: string, metadata?: Record<string, string>): Promise<S3UploadResult> {
|
||||||
|
try {
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: this.s3Configs.bucketName,
|
||||||
|
Key: key,
|
||||||
|
Body: buffer,
|
||||||
|
ACL: "public-read",
|
||||||
|
ContentType: contentType,
|
||||||
|
Metadata: metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.s3Client.send(command);
|
||||||
|
|
||||||
|
const url = `${this.s3Configs.bucketUploadedUrl}/${key}`;
|
||||||
|
|
||||||
|
this.logger.log(`File uploaded to S3: ${key}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
url,
|
||||||
|
bucket: this.s3Configs.bucketName,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file stream from S3
|
||||||
|
*/
|
||||||
|
async getFileStream(key: string): Promise<Readable> {
|
||||||
|
try {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: this.s3Configs.bucketName,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.s3Client.send(command);
|
||||||
|
return response.Body as Readable;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate presigned URL for file download
|
||||||
|
*/
|
||||||
|
async getPresignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
|
||||||
|
try {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: this.s3Configs.bucketName,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await getSignedUrl(this.s3Client, command, { expiresIn });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete file from S3
|
||||||
|
*/
|
||||||
|
async deleteFile(key: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const command = new DeleteObjectCommand({
|
||||||
|
Bucket: this.s3Configs.bucketName,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.s3Client.send(command);
|
||||||
|
this.logger.log(`File deleted from S3: ${key}`);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate S3 key for file
|
||||||
|
*/
|
||||||
|
generateFileKey(originalName: string, fileType: FileType): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomSuffix = Math.round(Math.random() * 1e9);
|
||||||
|
const extension = originalName.split(".").pop();
|
||||||
|
|
||||||
|
const basePathMap: Record<FileType, string> = {
|
||||||
|
image: "images",
|
||||||
|
video: "videos",
|
||||||
|
audio: "audio",
|
||||||
|
document: "documents",
|
||||||
|
text: "texts",
|
||||||
|
unknown: "unknowns",
|
||||||
|
};
|
||||||
|
|
||||||
|
const basePath = basePathMap[fileType];
|
||||||
|
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { File } from "@nest-lab/fastify-multer";
|
||||||
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
import { S3Service } from "./s3.service";
|
||||||
|
import { UploaderMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { FileType } from "../interfaces/file.interface";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UploaderService {
|
||||||
|
private readonly logger = new Logger(UploaderService.name);
|
||||||
|
private readonly maxFileSize: number;
|
||||||
|
private readonly allowedMimeTypes: string[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly s3Service: S3Service,
|
||||||
|
) {
|
||||||
|
this.maxFileSize = this.configService.get("MAX_FILE_SIZE", 1024 * 1024 * 100); // 100MB
|
||||||
|
this.allowedMimeTypes = [
|
||||||
|
"video/mp4",
|
||||||
|
"video/webm",
|
||||||
|
"video/ogg",
|
||||||
|
"video/avi",
|
||||||
|
"video/mov",
|
||||||
|
"video/mkv",
|
||||||
|
"audio/mp3",
|
||||||
|
"audio/wav",
|
||||||
|
"audio/ogg",
|
||||||
|
"audio/webm",
|
||||||
|
"audio/m4a",
|
||||||
|
"audio/aac",
|
||||||
|
"audio/flac",
|
||||||
|
"audio/m4a",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/tiff",
|
||||||
|
"image/bmp",
|
||||||
|
"application/pdf",
|
||||||
|
"text/plain",
|
||||||
|
"text/csv",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadMultiple(files: File[]) {
|
||||||
|
return await Promise.all(files.map((file) => this.uploadFile(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle file upload
|
||||||
|
*/
|
||||||
|
async uploadFile(file: File) {
|
||||||
|
if (!file || !file.buffer || !file.size) {
|
||||||
|
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
|
||||||
|
}
|
||||||
|
this.logger.log(`Uploading file: ${file.originalname}`);
|
||||||
|
|
||||||
|
this.validateFile(file);
|
||||||
|
|
||||||
|
const fileType = await this.determineFileType(file.mimetype);
|
||||||
|
|
||||||
|
const s3Key = this.s3Service.generateFileKey(file.originalname, fileType);
|
||||||
|
|
||||||
|
// Upload to S3
|
||||||
|
const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, {
|
||||||
|
originalName: file.originalname,
|
||||||
|
uploadedBy: "admin",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`File uploaded successfully: `);
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: s3Result,
|
||||||
|
url: s3Result.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate uploaded file
|
||||||
|
*/
|
||||||
|
private validateFile(file: File): void {
|
||||||
|
if (!file) throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
|
||||||
|
|
||||||
|
if (file.size && file.size > this.maxFileSize)
|
||||||
|
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TOO_LARGE.replace("[MAX_FILE_SIZE]", this.maxFileSize.toString()));
|
||||||
|
|
||||||
|
if (!this.allowedMimeTypes.includes(file.mimetype))
|
||||||
|
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TYPE_NOT_SUPPORTED.replace("[MIME_TYPES]", file.mimetype));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine file type based on mimetype
|
||||||
|
*/
|
||||||
|
private async determineFileType(mimetype: string): Promise<FileType> {
|
||||||
|
if (mimetype.startsWith("video/")) {
|
||||||
|
return "video";
|
||||||
|
} else if (mimetype.startsWith("audio/")) {
|
||||||
|
return "audio";
|
||||||
|
} else if (mimetype.startsWith("image/")) {
|
||||||
|
return "image";
|
||||||
|
} else if (mimetype.startsWith("text/")) {
|
||||||
|
return "text";
|
||||||
|
} else if (mimetype.startsWith("application/")) {
|
||||||
|
return "document";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable → Regular
+7
-9
@@ -1,25 +1,23 @@
|
|||||||
import { FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
|
import { File, FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
|
||||||
import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common";
|
import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common";
|
||||||
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiBody, ApiConsumes, ApiOperation } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto";
|
import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto";
|
||||||
import { UploaderService } from "./uploader.service";
|
import { UploaderService } from "./services/uploader.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
import { IFile } from "../utils/interfaces/IFile";
|
|
||||||
|
|
||||||
@ApiTags("Uploader")
|
|
||||||
@Controller("uploader")
|
@Controller("uploader")
|
||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
export class UploaderController {
|
export class UploaderController {
|
||||||
constructor(private readonly uploaderService: UploaderService) {}
|
constructor(private readonly uploaderService: UploaderService) {}
|
||||||
|
|
||||||
@ApiOperation({ summary: "Uploads a single file" })
|
@ApiOperation({ summary: "Upload a file (admin)" })
|
||||||
@ApiConsumes("multipart/form-data")
|
@ApiConsumes("multipart/form-data")
|
||||||
@UseInterceptors(FileInterceptor("file"))
|
@UseInterceptors(FileInterceptor("file"))
|
||||||
@ApiBody({ type: UploadSingleFileDto })
|
@ApiBody({ type: UploadSingleFileDto })
|
||||||
@Post("single-file")
|
@Post("single-file")
|
||||||
uploadFile(@UploadedFile() file: IFile) {
|
uploadFile(@UploadedFile() file: File) {
|
||||||
return this.uploaderService.upload(file);
|
return this.uploaderService.uploadFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: "Uploads multiple files" })
|
@ApiOperation({ summary: "Uploads multiple files" })
|
||||||
@@ -27,7 +25,7 @@ export class UploaderController {
|
|||||||
@UseInterceptors(FilesInterceptor("files"))
|
@UseInterceptors(FilesInterceptor("files"))
|
||||||
@ApiBody({ type: UploadMultipleFileDto })
|
@ApiBody({ type: UploadMultipleFileDto })
|
||||||
@Post("multi-file")
|
@Post("multi-file")
|
||||||
uploadFiles(@UploadedFiles() files: IFile[]) {
|
uploadFiles(@UploadedFiles() files: File[]) {
|
||||||
return this.uploaderService.uploadMultiple(files);
|
return this.uploaderService.uploadMultiple(files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
+4
-5
@@ -1,14 +1,13 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { S3Service } from "./services/s3.service";
|
||||||
|
import { UploaderService } from "./services/uploader.service";
|
||||||
import { UploaderController } from "./uploader.controller";
|
import { UploaderController } from "./uploader.controller";
|
||||||
import { UploaderService } from "./uploader.service";
|
import { S3ConfigsProvider } from "../../configs/s3.config";
|
||||||
import { S3Service } from "../utils/providers/s3.service";
|
|
||||||
import { UtilsModule } from "../utils/utils.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UtilsModule],
|
|
||||||
providers: [UploaderService, S3Service],
|
|
||||||
controllers: [UploaderController],
|
controllers: [UploaderController],
|
||||||
|
providers: [UploaderService, S3Service, S3ConfigsProvider],
|
||||||
exports: [UploaderService],
|
exports: [UploaderService],
|
||||||
})
|
})
|
||||||
export class UploaderModule {}
|
export class UploaderModule {}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
|
|
||||||
import { IFile } from "../utils/interfaces/IFile";
|
|
||||||
import { S3Service } from "../utils/providers/s3.service";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UploaderService {
|
|
||||||
constructor(private s3Service: S3Service) {}
|
|
||||||
|
|
||||||
async upload(file: IFile) {
|
|
||||||
return await this.s3Service.upload(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadMultiple(files: IFile[]) {
|
|
||||||
// const uploadPromises = files.map((file) => this.s3Service.upload(file));
|
|
||||||
|
|
||||||
// return await Promise.all(uploadPromises);
|
|
||||||
return this.s3Service.uploadMultiple(files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,10 @@ import { UserAnnouncement } from "../../announcements/entities/user-announcement
|
|||||||
import { BlogComment } from "../../blogs/entities/blog-comment.entity";
|
import { BlogComment } from "../../blogs/entities/blog-comment.entity";
|
||||||
import { Blog } from "../../blogs/entities/blog.entity";
|
import { Blog } from "../../blogs/entities/blog.entity";
|
||||||
import { Criticism } from "../../criticisms/entities/criticism.entity";
|
import { Criticism } from "../../criticisms/entities/criticism.entity";
|
||||||
|
import { DanakServiceAudio } from "../../danak-services/entities/danak-service-audio.entity";
|
||||||
import { DanakServiceFeedback } from "../../danak-services/entities/danak-service-feedback.entity";
|
import { DanakServiceFeedback } from "../../danak-services/entities/danak-service-feedback.entity";
|
||||||
import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.entity";
|
import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.entity";
|
||||||
|
import { DanakServiceVideo } from "../../danak-services/entities/danak-service-video.entity";
|
||||||
import { Discount } from "../../discounts/entities/discount.entity";
|
import { Discount } from "../../discounts/entities/discount.entity";
|
||||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||||
import { LearningProgress } from "../../learnings/entities/learning-progress.entity";
|
import { LearningProgress } from "../../learnings/entities/learning-progress.entity";
|
||||||
@@ -79,6 +81,12 @@ export class User extends BaseEntity {
|
|||||||
|
|
||||||
//---------------------------------------
|
//---------------------------------------
|
||||||
|
|
||||||
|
@OneToMany(() => DanakServiceAudio, (danakServiceAudio) => danakServiceAudio.createBy)
|
||||||
|
audios: DanakServiceAudio[];
|
||||||
|
|
||||||
|
@OneToMany(() => DanakServiceVideo, (danakServiceVideo) => danakServiceVideo.createBy)
|
||||||
|
videos: DanakServiceVideo[];
|
||||||
|
|
||||||
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.user)
|
@OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.user)
|
||||||
reviews: DanakServiceReview[];
|
reviews: DanakServiceReview[];
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface IFile {
|
|
||||||
fieldname: string;
|
|
||||||
originalname: string;
|
|
||||||
encoding: string;
|
|
||||||
mimetype: string;
|
|
||||||
buffer: Buffer;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import { extname } from "node:path";
|
|
||||||
|
|
||||||
import { PutObjectCommand, PutObjectCommandInput, S3Client } from "@aws-sdk/client-s3";
|
|
||||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
|
||||||
|
|
||||||
import { IS3Configs } from "../../../configs/s3.config";
|
|
||||||
import { S3_CONFIG } from "../constants";
|
|
||||||
import { IFile } from "../interfaces/IFile";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class S3Service {
|
|
||||||
private readonly logger = new Logger(S3Service.name);
|
|
||||||
private client: S3Client;
|
|
||||||
|
|
||||||
//
|
|
||||||
constructor(@Inject(S3_CONFIG) private s3Configs: IS3Configs) {
|
|
||||||
this.client = new S3Client({
|
|
||||||
endpoint: this.s3Configs.endpoint,
|
|
||||||
region: this.s3Configs.region,
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: this.s3Configs.accessKeyId,
|
|
||||||
secretAccessKey: this.s3Configs.secretAccessKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload(file: IFile) {
|
|
||||||
const extName = extname(file.originalname);
|
|
||||||
const params: PutObjectCommandInput = {
|
|
||||||
Body: file.buffer,
|
|
||||||
Bucket: this.s3Configs.bucket,
|
|
||||||
Key: `images/${randomUUID()}${Date.now()}${extName}`,
|
|
||||||
ACL: "public-read",
|
|
||||||
};
|
|
||||||
//
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.client.send(new PutObjectCommand(params));
|
|
||||||
const url = `${this.s3Configs.url}/${params.Key}`;
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
originalname: file.originalname,
|
|
||||||
size: file.size,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
throw new InternalServerErrorException("Failed to upload file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadMultiple(files: IFile[]) {
|
|
||||||
const uploadPromises = files.map((file) => this.upload(file));
|
|
||||||
const results = await Promise.all(uploadPromises);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,15 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
import { S3_CONFIG, SMS_CONFIG } from "./constants";
|
import { SMS_CONFIG } from "./constants";
|
||||||
import { CacheService } from "./providers/cache.service";
|
import { CacheService } from "./providers/cache.service";
|
||||||
import { EmailService } from "./providers/email.service";
|
import { EmailService } from "./providers/email.service";
|
||||||
import { OTPService } from "./providers/otp.service";
|
import { OTPService } from "./providers/otp.service";
|
||||||
import { PasswordService } from "./providers/password.service";
|
import { PasswordService } from "./providers/password.service";
|
||||||
import { S3Service } from "./providers/s3.service";
|
|
||||||
import { SmsService } from "./providers/sms.service";
|
import { SmsService } from "./providers/sms.service";
|
||||||
import { S3Configs } from "../../configs/s3.config";
|
import { smsConfigsProvider } from "../../configs/sms.config";
|
||||||
import { smsConfigs } from "../../configs/sms.config";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [EmailService, OTPService, PasswordService, CacheService, SmsService, smsConfigsProvider],
|
||||||
EmailService,
|
exports: [SMS_CONFIG, OTPService, PasswordService, CacheService, SmsService, EmailService],
|
||||||
OTPService,
|
|
||||||
PasswordService,
|
|
||||||
CacheService,
|
|
||||||
SmsService,
|
|
||||||
S3Service,
|
|
||||||
{
|
|
||||||
provide: SMS_CONFIG,
|
|
||||||
useFactory: smsConfigs().useFactory,
|
|
||||||
inject: smsConfigs().inject,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: S3_CONFIG,
|
|
||||||
useFactory: S3Configs().useFactory,
|
|
||||||
inject: S3Configs().inject,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [SMS_CONFIG, S3_CONFIG, S3Service, OTPService, PasswordService, CacheService, SmsService, EmailService],
|
|
||||||
})
|
})
|
||||||
export class UtilsModule {}
|
export class UtilsModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user