From 9a9f6406226a90522a5ea6b8416264e8bd1539c6 Mon Sep 17 00:00:00 2001 From: Mahyargdz Date: Sun, 11 May 2025 12:26:26 +0330 Subject: [PATCH] chore: add industry crud --- database/seeders/index.seeder.ts | 15 ++++ database/seeders/role.seeder.ts | 36 ++++++++ package.json | 12 ++- pnpm-lock.yaml | 65 ++++++++++++++ src/app.module.ts | 6 ++ src/common/enums/message.enum.ts | 15 ++++ src/modules/auth/providers/auth.service.ts | 2 +- src/modules/companies/companies.controller.ts | 4 + src/modules/companies/companies.module.ts | 14 +++ .../companies/entities/company.entity.ts | 52 +++++++++++ .../repositories/company.repository.ts | 5 ++ .../companies/services/companies.service.ts | 8 ++ .../industries/DTO/create-industry.dto.ts | 23 +++++ .../industries/DTO/industry-list-query.dto.ts | 19 ++++ .../industries/DTO/update-industry.dto.ts | 5 ++ .../industries/entities/industry.entity.ts | 22 +++++ .../industries/industries.controller.ts | 42 +++++++++ src/modules/industries/industries.module.ts | 14 +++ .../repositories/industry.repository.ts | 29 ++++++ .../industries/services/industries.service.ts | 88 +++++++++++++++++++ src/modules/uploader/DTO/upload-file.dto.ts | 20 +++++ src/modules/uploader/uploader.controller.ts | 32 +++++++ src/modules/uploader/uploader.module.ts | 14 +++ src/modules/uploader/uploader.service.ts | 20 +++++ src/modules/utils/providers/sms.service.ts | 1 - 25 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 database/seeders/index.seeder.ts create mode 100644 database/seeders/role.seeder.ts create mode 100644 src/modules/companies/companies.controller.ts create mode 100644 src/modules/companies/companies.module.ts create mode 100644 src/modules/companies/entities/company.entity.ts create mode 100644 src/modules/companies/repositories/company.repository.ts create mode 100644 src/modules/companies/services/companies.service.ts create mode 100644 src/modules/industries/DTO/create-industry.dto.ts create mode 100644 src/modules/industries/DTO/industry-list-query.dto.ts create mode 100644 src/modules/industries/DTO/update-industry.dto.ts create mode 100644 src/modules/industries/entities/industry.entity.ts create mode 100644 src/modules/industries/industries.controller.ts create mode 100644 src/modules/industries/industries.module.ts create mode 100644 src/modules/industries/repositories/industry.repository.ts create mode 100644 src/modules/industries/services/industries.service.ts create mode 100755 src/modules/uploader/DTO/upload-file.dto.ts create mode 100755 src/modules/uploader/uploader.controller.ts create mode 100755 src/modules/uploader/uploader.module.ts create mode 100755 src/modules/uploader/uploader.service.ts diff --git a/database/seeders/index.seeder.ts b/database/seeders/index.seeder.ts new file mode 100644 index 0000000..77d3736 --- /dev/null +++ b/database/seeders/index.seeder.ts @@ -0,0 +1,15 @@ +import { EntityManager } from "@mikro-orm/postgresql"; +import { Seeder } from "@mikro-orm/seeder"; +import { Logger } from "@nestjs/common"; + +import { RoleSeeder } from "./role.seeder"; + +export class IndexSeeder extends Seeder { + private readonly logger = new Logger(IndexSeeder.name); + + async run(em: EntityManager): Promise { + this.logger.log("Starting seeding process..."); + await new RoleSeeder().run(em); + this.logger.log("Seeding process completed successfully."); + } +} diff --git a/database/seeders/role.seeder.ts b/database/seeders/role.seeder.ts new file mode 100644 index 0000000..87b11e1 --- /dev/null +++ b/database/seeders/role.seeder.ts @@ -0,0 +1,36 @@ +import { EntityManager, RequiredEntityData } from "@mikro-orm/postgresql"; +import { Seeder } from "@mikro-orm/seeder"; +import { Logger } from "@nestjs/common"; + +import { Role } from "../../src/modules/users/entities/role.entity"; +import { RoleEnum } from "../../src/modules/users/enums/role.enum"; + +export class RoleSeeder extends Seeder { + private readonly logger = new Logger(RoleSeeder.name); + + async run(em: EntityManager): Promise { + const roles: RequiredEntityData[] = [ + { + name: RoleEnum.ADMIN, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: RoleEnum.USER, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + for (const roleData of roles) { + const existingRole = await em.findOne(Role, { name: roleData.name }); + if (!existingRole) { + const role = em.create(Role, roleData); + await em.persistAndFlush(role); + this.logger.log(`[RoleSeeder] Created role: ${roleData.name}`); + } else { + this.logger.log(`[RoleSeeder] Role already exists: ${roleData.name}`); + } + } + } +} diff --git a/package.json b/package.json index 7773961..5128775 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,25 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", - "prepare": "husky" + "prepare": "husky || true", + "orm:create": "mikro-orm migration:create --config=database/mikro-orm.config.ts", + "orm:up": "mikro-orm migration:up --config=database/mikro-orm.config.ts", + "orm:down": "mikro-orm migration:down --config=database/mikro-orm.config.ts", + "orm:generate": "mikro-orm migration:create --blank --config=database/mikro-orm.config.ts", + "orm:update": "mikro-orm schema:update --run --config=database/mikro-orm.config.ts", + "orm:drop": "mikro-orm schema:drop --run --config=database/mikro-orm.config.ts", + "orm:seed": "mikro-orm seeder:create --config=database/mikro-orm.config.ts", + "seed:run": "mikro-orm seeder:run --config=database/mikro-orm.config.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.806.0", "@fastify/static": "^8.1.1", "@keyv/redis": "^4.4.0", + "@mikro-orm/cli": "^6.4.15", "@mikro-orm/core": "^6.4.15", "@mikro-orm/nestjs": "^6.1.1", "@mikro-orm/postgresql": "^6.4.15", + "@mikro-orm/seeder": "^6.4.15", "@nest-lab/fastify-multer": "^1.3.0", "@nest-lab/throttler-storage-redis": "^1.1.0", "@nestjs-modules/mailer": "^2.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5c6862..c6801ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@keyv/redis': specifier: ^4.4.0 version: 4.4.0(keyv@5.3.3) + '@mikro-orm/cli': + specifier: ^6.4.15 + version: 6.4.15(pg@8.15.6) '@mikro-orm/core': specifier: ^6.4.15 version: 6.4.15 @@ -26,6 +29,9 @@ importers: '@mikro-orm/postgresql': specifier: ^6.4.15 version: 6.4.15(@mikro-orm/core@6.4.15) + '@mikro-orm/seeder': + specifier: ^6.4.15 + version: 6.4.15(@mikro-orm/core@6.4.15) '@nest-lab/fastify-multer': specifier: ^1.3.0 version: 1.3.0(@nestjs/common@11.1.0(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-fastify@11.1.0(@fastify/static@8.1.1)(@nestjs/common@11.1.0(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.0))(rxjs@7.8.2) @@ -972,6 +978,9 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jercle/yargonaut@1.1.5': + resolution: {integrity: sha512-zBp2myVvBHp1UaJsNTyS6q4UDKT7eRiqTS4oNTS6VQMd6mpxYOdbeK4pY279cDCdakGy6hG0J3ejoXZVsPwHqw==} + '@jest/console@29.7.0': resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1086,6 +1095,11 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@mikro-orm/cli@6.4.15': + resolution: {integrity: sha512-w0AqJvclBoSihECEtxPD2JtuOzwkm2nBYQvnZexiTfO5BPgjP9xGbCEDEp8figQW1yWIVM93z9aqQ2M3cXsADg==} + engines: {node: '>= 18.12.0'} + hasBin: true + '@mikro-orm/core@6.4.15': resolution: {integrity: sha512-1uQ7lThYymUHf6k6soByJ0qmnYFZcfpczAdmGsfIkGwoE0Ix83RIea+UAJk4pSFeKgXYdiDZOAFu5t224P/lOw==} engines: {node: '>= 18.12.0'} @@ -1120,6 +1134,12 @@ packages: peerDependencies: '@mikro-orm/core': ^6.0.0 + '@mikro-orm/seeder@6.4.15': + resolution: {integrity: sha512-jZ1Ai28mMkHSRYzxrGQEdYktRXqq5P54syNi1H3wkYmRjzYqwAD6hOKrBCOmOzYqskQRWLe042F07w/27Nqo7g==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + '@modelcontextprotocol/sdk@1.11.1': resolution: {integrity: sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==} engines: {node: '>=18'} @@ -3437,6 +3457,11 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figlet@1.8.1: + resolution: {integrity: sha512-kEC3Sme+YvA8Hkibv0NR1oClGcWia0VB2fC1SlMy027cwe795Xx40Xiv/nw/iFAwQLupymWh+uhAAErn/7hwPg==} + engines: {node: '>= 0.4.0'} + hasBin: true + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -5027,6 +5052,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parent-require@1.0.0: + resolution: {integrity: sha512-2MXDNZC4aXdkkap+rBBMv0lUsfJqvX5/2FiYYnfCnorZt3Pk06/IOR5KeaoghgS2w07MLWgjbsnyaq6PdHn2LQ==} + engines: {node: '>= 0.4.0'} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -7448,6 +7477,12 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jercle/yargonaut@1.1.5': + dependencies: + chalk: 4.1.2 + figlet: 1.8.1 + parent-require: 1.0.0 + '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -7668,6 +7703,26 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@mikro-orm/cli@6.4.15(pg@8.15.6)': + dependencies: + '@jercle/yargonaut': 1.1.5 + '@mikro-orm/core': 6.4.15 + '@mikro-orm/knex': 6.4.15(@mikro-orm/core@6.4.15)(pg@8.15.6) + fs-extra: 11.3.0 + tsconfig-paths: 4.2.0 + yargs: 17.7.2 + transitivePeerDependencies: + - better-sqlite3 + - libsql + - mariadb + - mysql + - mysql2 + - pg + - pg-native + - sqlite3 + - supports-color + - tedious + '@mikro-orm/core@6.4.15': dependencies: dataloader: 2.2.3 @@ -7718,6 +7773,12 @@ snapshots: - supports-color - tedious + '@mikro-orm/seeder@6.4.15(@mikro-orm/core@6.4.15)': + dependencies: + '@mikro-orm/core': 6.4.15 + fs-extra: 11.3.0 + globby: 11.1.0 + '@modelcontextprotocol/sdk@1.11.1': dependencies: content-type: 1.0.5 @@ -10421,6 +10482,8 @@ snapshots: fflate@0.8.2: {} + figlet@1.8.1: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -12506,6 +12569,8 @@ snapshots: dependencies: callsites: 3.1.0 + parent-require@1.0.0: {} + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 diff --git a/src/app.module.ts b/src/app.module.ts index 3988896..922e107 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,7 +15,10 @@ import { databaseConfig } from "./configs/mikro-orm.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AuthModule } from "./modules/auth/auth.module"; +import { CompaniesModule } from "./modules/companies/companies.module"; +import { IndustriesModule } from "./modules/industries/industries.module"; import { NotificationModule } from "./modules/notifications/notifications.module"; +import { UploaderModule } from "./modules/uploader/uploader.module"; import { UsersModule } from "./modules/users/users.module"; import { UtilsModule } from "./modules/utils/utils.module"; @Module({ @@ -40,6 +43,9 @@ import { UtilsModule } from "./modules/utils/utils.module"; UsersModule, NotificationModule, UtilsModule, + IndustriesModule, + CompaniesModule, + UploaderModule, ], }) export class AppModule implements NestModule { diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 9fa5a85..120ad8d 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -523,3 +523,18 @@ export const enum AdminMessage { export const enum SmsMessage { SEND_SMS_ERROR = "خطا در ارسال پیامک", } + +export const enum IndustryMessage { + TITLE_REQUIRED = "عنوان صنعت مورد نیاز است", + TITLE_MUST_BE_A_STRING = "عنوان صنعت باید یک رشته باشد", + TITLE_LENGTH = "عنوان صنعت باید بین ۳ تا ۱۰۰ کاراکتر باشد", + ICON_URL_REQUIRED = "آیکون صنعت مورد نیاز است", + ICON_URL_MUST_BE_A_URL = "آیکون صنعت باید یک URL معتبر باشد", + IS_ACTIVE_REQUIRED = "وضعیت فعالیت صنعت مورد نیاز است", + IS_ACTIVE_MUST_BE_A_BOOLEAN = "وضعیت فعالیت صنعت باید یک بولین باشد", + INDUSTRY_CREATED_SUCCESSFULLY = "صنعت با موفقیت ایجاد شد", + INDUSTRY_ALREADY_EXISTS = "این صنعت قبلا ثبت شده است", + INDUSTRY_NOT_FOUND = "این صنعت یافت نشد", + UPDATED_SUCCESSFULLY = "صنعت با موفقیت به روز رسانی شد", + INDUSTRY_DELETED_SUCCESSFULLY = "صنعت با موفقیت حذف شد", +} diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 3114cc4..6573c78 100755 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -42,9 +42,9 @@ export class AuthService { }; } const otpCode = await this.otpService.generateAndSetInCache(phone, "REGISTER"); + // await this.notificationQueue.addOtpNotification(phone, { userPhone: phone, otp: otpCode }); - await this.smsService.sendSmsVerifyCode(phone, otpCode); return { message: AuthMessage.OTP_SENT, diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts new file mode 100644 index 0000000..00b838f --- /dev/null +++ b/src/modules/companies/companies.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from "@nestjs/common"; + +@Controller("companies") +export class CompaniesController {} diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts new file mode 100644 index 0000000..f9709be --- /dev/null +++ b/src/modules/companies/companies.module.ts @@ -0,0 +1,14 @@ +import { MikroOrmModule } from "@mikro-orm/nestjs"; +import { Module } from "@nestjs/common"; + +import { CompaniesController } from "./companies.controller"; +import { Company } from "./entities/company.entity"; +import { CompaniesService } from "./services/companies.service"; + +@Module({ + imports: [MikroOrmModule.forFeature([Company])], + providers: [CompaniesService], + controllers: [CompaniesController], + exports: [CompaniesService], +}) +export class CompaniesModule {} diff --git a/src/modules/companies/entities/company.entity.ts b/src/modules/companies/entities/company.entity.ts new file mode 100644 index 0000000..a8ca483 --- /dev/null +++ b/src/modules/companies/entities/company.entity.ts @@ -0,0 +1,52 @@ +import { Entity, EntityRepositoryType, ManyToOne, Opt, Property } from "@mikro-orm/core"; + +import { BaseEntity } from "../../../common/entities/base.entity"; +import { Industry } from "../../industries/entities/industry.entity"; +import { CompanyRepository } from "../repositories/company.repository"; + +@Entity({ repository: () => CompanyRepository }) +export class Company extends BaseEntity { + @Property({ type: "varchar", length: 255, unique: true }) + name!: string; + + @Property({ type: "varchar", length: 255 }) + chiefExecutive!: string; + + @Property({ type: "varchar", length: 255, unique: true }) + phone!: string; + + @Property({ type: "varchar", length: 255, unique: true }) + email!: string; + + @Property({ type: "varchar", length: 255, unique: true }) + identificationNumber!: string; + + @Property({ type: "date" }) + dateOfEstablishment!: Date; + + @Property({ type: "varchar", length: 255 }) + address!: string; + + @Property({ type: "varchar", length: 255 }) + mapAddressLink!: string; + + @Property({ type: "text", nullable: false }) + description!: string; + + @Property({ type: "varchar", length: 255 }) + websiteUrl!: string; + + @Property({ type: "varchar", length: 255 }) + profileImageUrl!: string; + + @Property({ type: "varchar", length: 255 }) + coverImageUrl!: string; + + @Property({ type: "boolean", default: true }) + isActive!: boolean & Opt; + + @ManyToOne(() => Industry, { deleteRule: "restrict" }) + industry!: Industry; + + [EntityRepositoryType]?: CompanyRepository; +} diff --git a/src/modules/companies/repositories/company.repository.ts b/src/modules/companies/repositories/company.repository.ts new file mode 100644 index 0000000..13da204 --- /dev/null +++ b/src/modules/companies/repositories/company.repository.ts @@ -0,0 +1,5 @@ +import { EntityRepository } from "@mikro-orm/core"; + +import { Company } from "../entities/company.entity"; + +export class CompanyRepository extends EntityRepository {} diff --git a/src/modules/companies/services/companies.service.ts b/src/modules/companies/services/companies.service.ts new file mode 100644 index 0000000..b49d80e --- /dev/null +++ b/src/modules/companies/services/companies.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from "@nestjs/common"; + +import { CompanyRepository } from "../repositories/company.repository"; + +@Injectable() +export class CompaniesService { + constructor(private readonly companyRepository: CompanyRepository) {} +} diff --git a/src/modules/industries/DTO/create-industry.dto.ts b/src/modules/industries/DTO/create-industry.dto.ts new file mode 100644 index 0000000..7c98ae0 --- /dev/null +++ b/src/modules/industries/DTO/create-industry.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUrl, Length } from "class-validator"; + +import { IndustryMessage } from "../../../common/enums/message.enum"; + +export class CreateIndustryDto { + @IsNotEmpty({ message: IndustryMessage.TITLE_REQUIRED }) + @IsString({ message: IndustryMessage.TITLE_MUST_BE_A_STRING }) + @Length(3, 100, { message: IndustryMessage.TITLE_LENGTH }) + @ApiProperty({ description: "The title of the industry", example: "Industry" }) + title: string; + + @IsNotEmpty({ message: IndustryMessage.ICON_URL_REQUIRED }) + @IsUrl({ protocols: ["http", "https"] }, { message: IndustryMessage.ICON_URL_MUST_BE_A_URL }) + @ApiProperty({ description: "The icon url of the industry", example: "https://example.com/icon.png" }) + iconUrl: string; + + @IsOptional() + @IsNotEmpty({ message: IndustryMessage.IS_ACTIVE_REQUIRED }) + @IsBoolean({ message: IndustryMessage.IS_ACTIVE_MUST_BE_A_BOOLEAN }) + @ApiProperty({ description: "The active status of the industry", example: true }) + isActive?: boolean; +} diff --git a/src/modules/industries/DTO/industry-list-query.dto.ts b/src/modules/industries/DTO/industry-list-query.dto.ts new file mode 100644 index 0000000..7c2b110 --- /dev/null +++ b/src/modules/industries/DTO/industry-list-query.dto.ts @@ -0,0 +1,19 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsIn, IsOptional, IsString } from "class-validator"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { CommonMessage } from "../../../common/enums/message.enum"; + +export class IndustryListQueryDto extends PaginationDto { + @IsOptional() + @Type(() => Number) + @IsIn([1, 0], { message: CommonMessage.IS_ACTIVE_SHOULD_BE_1_0 }) + @ApiPropertyOptional({ description: "ads status", example: 1 }) + isActive?: number; + + @IsOptional() + @IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) + @ApiPropertyOptional({ description: "Search query", example: "search query" }) + q?: string; +} diff --git a/src/modules/industries/DTO/update-industry.dto.ts b/src/modules/industries/DTO/update-industry.dto.ts new file mode 100644 index 0000000..47fb8dc --- /dev/null +++ b/src/modules/industries/DTO/update-industry.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateIndustryDto } from "./create-industry.dto"; + +export class UpdateIndustryDto extends PartialType(CreateIndustryDto) {} diff --git a/src/modules/industries/entities/industry.entity.ts b/src/modules/industries/entities/industry.entity.ts new file mode 100644 index 0000000..2a8d9f0 --- /dev/null +++ b/src/modules/industries/entities/industry.entity.ts @@ -0,0 +1,22 @@ +import { Collection, Entity, EntityRepositoryType, OneToMany, Opt, Property } from "@mikro-orm/core"; + +import { BaseEntity } from "../../../common/entities/base.entity"; +import { Company } from "../../companies/entities/company.entity"; +import { IndustryRepository } from "../repositories/industry.repository"; + +@Entity({ repository: () => IndustryRepository }) +export class Industry extends BaseEntity { + @Property({ type: "varchar", length: 255, unique: true }) + title!: string; + + @Property({ type: "varchar", length: 255 }) + iconUrl!: string; + + @Property({ type: "boolean", default: true }) + isActive!: boolean & Opt; + + @OneToMany(() => Company, (company) => company.industry) + companies = new Collection(this); + + [EntityRepositoryType]?: IndustryRepository; +} diff --git a/src/modules/industries/industries.controller.ts b/src/modules/industries/industries.controller.ts new file mode 100644 index 0000000..25db554 --- /dev/null +++ b/src/modules/industries/industries.controller.ts @@ -0,0 +1,42 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; + +import { CreateIndustryDto } from "./DTO/create-industry.dto"; +import { IndustryListQueryDto } from "./DTO/industry-list-query.dto"; +import { UpdateIndustryDto } from "./DTO/update-industry.dto"; +import { IndustriesService } from "./services/industries.service"; +import { ParamDto } from "../../common/DTO/param.dto"; + +@Controller("industries") +export class IndustriesController { + constructor(private readonly industriesService: IndustriesService) {} + + @Post() + create(@Body() createIndustryDto: CreateIndustryDto) { + return this.industriesService.create(createIndustryDto); + } + + @Get(":id") + getIndustryById(@Param() paramDto: ParamDto) { + return this.industriesService.getIndustriesById(paramDto.id); + } + + @Patch(":id") + updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto) { + return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto); + } + + @Delete(":id") + deleteIndustry(@Param() paramDto: ParamDto) { + return this.industriesService.deleteIndustry(paramDto.id); + } + + @Get("list") + getIndustriesForAdmin(@Query() query: IndustryListQueryDto) { + return this.industriesService.getIndustriesListForAdmin(query); + } + + @Patch(":id/toggle-status") + toggleStatus(@Param() paramDto: ParamDto) { + return this.industriesService.toggleStatus(paramDto.id); + } +} diff --git a/src/modules/industries/industries.module.ts b/src/modules/industries/industries.module.ts new file mode 100644 index 0000000..d2c1ed6 --- /dev/null +++ b/src/modules/industries/industries.module.ts @@ -0,0 +1,14 @@ +import { MikroOrmModule } from "@mikro-orm/nestjs"; +import { Module } from "@nestjs/common"; + +import { Industry } from "./entities/industry.entity"; +import { IndustriesController } from "./industries.controller"; +import { IndustriesService } from "./services/industries.service"; + +@Module({ + imports: [MikroOrmModule.forFeature([Industry])], + controllers: [IndustriesController], + providers: [IndustriesService], + exports: [IndustriesService], +}) +export class IndustriesModule {} diff --git a/src/modules/industries/repositories/industry.repository.ts b/src/modules/industries/repositories/industry.repository.ts new file mode 100644 index 0000000..b2decb1 --- /dev/null +++ b/src/modules/industries/repositories/industry.repository.ts @@ -0,0 +1,29 @@ +import { EntityRepository } from "@mikro-orm/postgresql"; + +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { IndustryListQueryDto } from "../DTO/industry-list-query.dto"; +import { Industry } from "../entities/industry.entity"; + +export class IndustryRepository extends EntityRepository { + // + async getIndustriesListForAdmin(queryDto: IndustryListQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + + const queryBuilder = this.createQueryBuilder("i") + .leftJoinAndSelect("i.companies", "c") + .where({ deletedAt: null }) + .orderBy({ createdAt: "DESC" }) + .limit(limit) + .offset(skip); + + if (queryDto.q) { + queryBuilder.andWhere({ name: { $ilike: `%${queryDto.q}%` } }); + } + + if (queryDto.isActive !== undefined) { + queryBuilder.andWhere({ isActive: queryDto.isActive === 1 }); + } + + return queryBuilder.getResultAndCount(); + } +} diff --git a/src/modules/industries/services/industries.service.ts b/src/modules/industries/services/industries.service.ts new file mode 100644 index 0000000..1f487f5 --- /dev/null +++ b/src/modules/industries/services/industries.service.ts @@ -0,0 +1,88 @@ +import { EntityManager } from "@mikro-orm/postgresql"; +import { BadRequestException, Injectable } from "@nestjs/common"; +import dayjs from "dayjs"; + +import { IndustryMessage } from "../../../common/enums/message.enum"; +import { CreateIndustryDto } from "../DTO/create-industry.dto"; +import { IndustryListQueryDto } from "../DTO/industry-list-query.dto"; +import { UpdateIndustryDto } from "../DTO/update-industry.dto"; +import { IndustryRepository } from "../repositories/industry.repository"; + +@Injectable() +export class IndustriesService { + constructor( + private readonly industryRepository: IndustryRepository, + private readonly em: EntityManager, + ) {} + + //----------------------------------- + + async create(createIndustryDto: CreateIndustryDto) { + const existingIndustry = await this.industryRepository.findOne({ title: createIndustryDto.title }); + if (existingIndustry) throw new BadRequestException(IndustryMessage.INDUSTRY_ALREADY_EXISTS); + + const industry = this.industryRepository.create(createIndustryDto); + this.em.persistAndFlush(industry); + + return { + message: IndustryMessage.INDUSTRY_CREATED_SUCCESSFULLY, + industry, + }; + } + //----------------------------------- + + async getIndustriesById(id: string) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); + + return { industry }; + } + //----------------------------------- + + async updateIndustry(id: string, updateIndustryDto: UpdateIndustryDto) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); + + this.industryRepository.assign(industry, updateIndustryDto); + this.em.persistAndFlush(industry); + + return { + message: IndustryMessage.UPDATED_SUCCESSFULLY, + industry, + }; + } + + //----------------------------------- + + async deleteIndustry(id: string) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); + + industry.deletedAt = dayjs().toDate(); + + this.em.flush(); + + return { + message: IndustryMessage.INDUSTRY_DELETED_SUCCESSFULLY, + }; + } + //----------------------------------- + + async getIndustriesListForAdmin(queryDto: IndustryListQueryDto) { + const [industries, count] = await this.industryRepository.getIndustriesListForAdmin(queryDto); + return { industries, count, paginate: true }; + } + //----------------------------------- + + async toggleStatus(id: string) { + const industry = await this.industryRepository.findOne({ id, deletedAt: null }); + if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND); + + industry.isActive = !industry.isActive; + this.em.flush(); + + return { + message: IndustryMessage.UPDATED_SUCCESSFULLY, + }; + } +} diff --git a/src/modules/uploader/DTO/upload-file.dto.ts b/src/modules/uploader/DTO/upload-file.dto.ts new file mode 100755 index 0000000..cae108e --- /dev/null +++ b/src/modules/uploader/DTO/upload-file.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; + +import { IFile } from "../../utils/interfaces/IFile"; + +export class UploadSingleFileDto { + @ApiProperty({ type: "string", format: "binary", nullable: false }) + file: IFile; +} + +export class UploadMultipleFileDto { + @ApiProperty({ + required: true, + type: "array", + items: { + type: "string", + format: "binary", + }, + }) + files: IFile[]; +} diff --git a/src/modules/uploader/uploader.controller.ts b/src/modules/uploader/uploader.controller.ts new file mode 100755 index 0000000..40c0f36 --- /dev/null +++ b/src/modules/uploader/uploader.controller.ts @@ -0,0 +1,32 @@ +import { FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer"; +import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common"; +import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto"; +import { UploaderService } from "./uploader.service"; +import { IFile } from "../utils/interfaces/IFile"; + +@ApiTags("Uploader") +@Controller("uploader") +// @AuthGuards() +export class UploaderController { + constructor(private readonly uploaderService: UploaderService) {} + + @ApiOperation({ summary: "Uploads a single file" }) + @ApiConsumes("multipart/form-data") + @UseInterceptors(FileInterceptor("file")) + @ApiBody({ type: UploadSingleFileDto }) + @Post("single-file") + uploadFile(@UploadedFile() file: IFile) { + return this.uploaderService.upload(file); + } + + @ApiOperation({ summary: "Uploads multiple files" }) + @ApiConsumes("multipart/form-data") + @UseInterceptors(FilesInterceptor("files")) + @ApiBody({ type: UploadMultipleFileDto }) + @Post("multi-file") + uploadFiles(@UploadedFiles() files: IFile[]) { + return this.uploaderService.uploadMultiple(files); + } +} diff --git a/src/modules/uploader/uploader.module.ts b/src/modules/uploader/uploader.module.ts new file mode 100755 index 0000000..776edc7 --- /dev/null +++ b/src/modules/uploader/uploader.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; + +import { UploaderController } from "./uploader.controller"; +import { UploaderService } from "./uploader.service"; +import { S3Service } from "../utils/providers/s3.service"; +import { UtilsModule } from "../utils/utils.module"; + +@Module({ + imports: [UtilsModule], + providers: [UploaderService, S3Service], + controllers: [UploaderController], + exports: [UploaderService], +}) +export class UploaderModule {} diff --git a/src/modules/uploader/uploader.service.ts b/src/modules/uploader/uploader.service.ts new file mode 100755 index 0000000..015bbcf --- /dev/null +++ b/src/modules/uploader/uploader.service.ts @@ -0,0 +1,20 @@ +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); + } +} diff --git a/src/modules/utils/providers/sms.service.ts b/src/modules/utils/providers/sms.service.ts index 682da36..41c0aee 100755 --- a/src/modules/utils/providers/sms.service.ts +++ b/src/modules/utils/providers/sms.service.ts @@ -43,7 +43,6 @@ export class SmsService { }), ), ); - console.log(data, "================"); return data; } catch (error) {