add config module
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-05-17 17:01:50 +03:30
parent 8ba80f86de
commit 2d34fb68e7
12 changed files with 5773 additions and 3 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260517132951_AddConfigModule extends Migration {
override async up(): Promise<void> {
this.addSql(`create table "configs" ("id" char(26) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "key" varchar(255) not null, "value" varchar(255) not null, "shop_id" char(26) null, constraint "configs_pkey" primary key ("id"));`);
this.addSql(`create index "configs_created_at_index" on "configs" ("created_at");`);
this.addSql(`create index "configs_deleted_at_index" on "configs" ("deleted_at");`);
this.addSql(`alter table "configs" add constraint "configs_shop_id_key_unique" unique ("shop_id", "key");`);
this.addSql(`alter table "configs" add constraint "configs_shop_id_foreign" foreign key ("shop_id") references "shops" ("id") on update cascade on delete set null;`);
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
}
override async down(): Promise<void> {
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
}
}
+4 -2
View File
@@ -1,7 +1,7 @@
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import dataBaseConfig from './config/mikro-orm.config';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule as NestConfigModule } from '@nestjs/config';
import { UserModule } from './modules/users/user.module';
import { UtilsModule } from './modules/utils/utils.module';
import { AuthModule } from './modules/auth/auth.module';
@@ -24,10 +24,11 @@ import { ContactModule } from './modules/contact/contact.module';
import { IconsModule } from './modules/icons/icons.module';
import { CacheModule } from '@nestjs/cache-manager';
import { cacheConfig } from './config/cache.config';
import { ShopConfigModule } from './modules/config/config.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, cache: true }),
NestConfigModule.forRoot({ isGlobal: true, cache: true }),
CacheModule.registerAsync(cacheConfig()),
MikroOrmModule.forRootAsync(dataBaseConfig),
UserModule,
@@ -55,6 +56,7 @@ import { cacheConfig } from './config/cache.config';
EventEmitterModule.forRoot(),
ContactModule,
IconsModule,
ShopConfigModule,
],
controllers: [],
providers: [],
+16
View File
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from './providers/config.service';
import { ConfigController } from './controllers/config.controller';
import { Config } from './entities/config.entity';
import { ConfigRepository } from './repositories/config.repository';
import { ShopsModule } from '../shops/shops.module';
@Module({
imports: [MikroOrmModule.forFeature([Config]), JwtModule, ShopsModule],
controllers: [ConfigController],
providers: [ConfigService, ConfigRepository],
exports: [ConfigService, ConfigRepository],
})
export class ShopConfigModule {}
@@ -0,0 +1,86 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
import {
ApiBearerAuth,
ApiBody,
ApiHeader,
ApiOperation,
ApiParam,
ApiTags,
} from '@nestjs/swagger';
import { ConfigService } from '../providers/config.service';
import { CreateConfigDto } from '../dto/create-config.dto';
import { UpdateConfigDto } from '../dto/update-config.dto';
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
import { ShopId } from 'src/common/decorators/shop-id.decorator';
import { Permission } from 'src/common/enums/permission.enum';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { API_HEADER_SLUG } from 'src/common/constants';
import { Config } from '../entities/config.entity';
@ApiTags('config')
@Controller()
export class ConfigController {
constructor(private readonly configService: ConfigService) {}
@Get('public/config/shop/:slug')
@ApiOperation({ summary: 'Get shop configs by slug (public)' })
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'slug', description: 'Shop slug' })
findAllPublic(@Param('slug') slug: string): Promise<Config[]> {
return this.configService.findAllBySlug(slug);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Post('admin/config')
@ApiOperation({ summary: 'Create a shop config (admin)' })
@ApiBody({ type: CreateConfigDto })
create(@Body() dto: CreateConfigDto, @ShopId() shopId: string): Promise<Config> {
return this.configService.create(shopId, dto);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Get('admin/config')
@ApiOperation({ summary: 'Get all shop configs (admin)' })
findAll(@ShopId() shopId: string): Promise<Config[]> {
return this.configService.findAllByShopId(shopId);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Get('admin/config/:id')
@ApiOperation({ summary: 'Get a shop config by ID (admin)' })
@ApiParam({ name: 'id', description: 'Config ID' })
findOne(@ShopId() shopId: string, @Param('id') id: string): Promise<Config> {
return this.configService.findOne(shopId, id);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Patch('admin/config/:id')
@ApiOperation({ summary: 'Update a shop config (admin)' })
@ApiParam({ name: 'id', description: 'Config ID' })
@ApiBody({ type: UpdateConfigDto })
update(
@ShopId() shopId: string,
@Param('id') id: string,
@Body() dto: UpdateConfigDto,
): Promise<Config> {
return this.configService.update(shopId, id, dto);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_SETTINGS)
@Delete('admin/config/:id')
@ApiOperation({ summary: 'Delete a shop config (admin)' })
@ApiParam({ name: 'id', description: 'Config ID' })
async remove(@ShopId() shopId: string, @Param('id') id: string): Promise<void> {
await this.configService.remove(shopId, id);
}
}
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateConfigDto {
@ApiProperty({ example: 'theme_color', description: 'Config key' })
@IsString()
@IsNotEmpty()
key!: string;
@ApiProperty({ example: '#ff5500', description: 'Config value' })
@IsString()
@IsNotEmpty()
value!: string;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateConfigDto } from './create-config.dto';
export class UpdateConfigDto extends PartialType(CreateConfigDto) {}
@@ -0,0 +1,16 @@
import { Entity, ManyToOne, Property, Unique } from '@mikro-orm/core';
import { BaseEntity } from 'src/common/entities/base.entity';
import { Shop } from '../../shops/entities/shop.entity';
@Entity({ tableName: 'configs' })
@Unique({ properties: ['shop', 'key'] })
export class Config extends BaseEntity {
@Property()
key!: string;
@Property()
value!: string;
@ManyToOne(() => Shop, { nullable: true })
shop?: Shop;
}
@@ -0,0 +1,101 @@
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { EntityManager, RequiredEntityData } from '@mikro-orm/postgresql';
import { CreateConfigDto } from '../dto/create-config.dto';
import { UpdateConfigDto } from '../dto/update-config.dto';
import { Config } from '../entities/config.entity';
import { ConfigRepository } from '../repositories/config.repository';
import { ShopRepository } from '../../shops/repositories/rest.repository';
import { ShopMessage } from 'src/common/enums/message.enum';
@Injectable()
export class ConfigService {
constructor(
private readonly configRepository: ConfigRepository,
private readonly shopRepository: ShopRepository,
private readonly em: EntityManager,
) {}
async create(shopId: string, dto: CreateConfigDto): Promise<Config> {
const shop = await this.shopRepository.findOne({ id: shopId });
if (!shop) {
throw new NotFoundException(ShopMessage.NOT_FOUND);
}
const existing = await this.configRepository.findOne({
shop: { id: shopId },
key: dto.key,
});
if (existing) {
throw new ConflictException('این کلید تنظیمات قبلاً برای این فروشگاه ثبت شده است.');
}
const data: RequiredEntityData<Config> = {
shop,
key: dto.key,
value: dto.value,
};
const config = this.configRepository.create(data);
await this.em.persistAndFlush(config);
return config;
}
async findAllByShopId(shopId: string): Promise<Config[]> {
return this.configRepository.findAllByShopId(shopId);
}
async findAllBySlug(slug: string): Promise<Config[]> {
const shop = await this.shopRepository.findOne({ slug });
if (!shop) {
throw new NotFoundException(ShopMessage.NOT_FOUND);
}
return this.configRepository.findAllByShopId(shop.id);
}
async findOne(shopId: string, id: string): Promise<Config> {
const config = await this.configRepository.findOne({
id,
shop: { id: shopId },
});
if (!config) {
throw new NotFoundException(`Config with ID ${id} not found`);
}
return config;
}
async update(shopId: string, id: string, dto: UpdateConfigDto): Promise<Config> {
const config = await this.configRepository.findOne({
id,
shop: { id: shopId },
});
if (!config) {
throw new NotFoundException(`Config with ID ${id} not found`);
}
if (dto.key !== undefined && dto.key !== config.key) {
const existing = await this.configRepository.findOne({
shop: { id: shopId },
key: dto.key,
id: { $ne: id },
});
if (existing) {
throw new ConflictException('این کلید تنظیمات قبلاً برای این فروشگاه ثبت شده است.');
}
}
this.em.assign(config, dto);
await this.em.persistAndFlush(config);
return config;
}
async remove(shopId: string, id: string): Promise<void> {
const config = await this.configRepository.findOne({
id,
shop: { id: shopId },
});
if (!config) {
throw new NotFoundException(`Config with ID ${id} not found`);
}
await this.em.removeAndFlush(config);
}
}
@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { Config } from '../entities/config.entity';
@Injectable()
export class ConfigRepository extends EntityRepository<Config> {
constructor(readonly em: EntityManager) {
super(em, Config);
}
findAllByShopId(shopId: string): Promise<Config[]> {
return this.find({ shop: { id: shopId } }, { orderBy: { key: 'asc' } });
}
}
@@ -2,6 +2,7 @@ import { Collection, Entity, Index, OneToMany, Property } from '@mikro-orm/core'
import { BaseEntity } from '../../../common/entities/base.entity';
import { Delivery } from '../../delivery/entities/delivery.entity';
import { Product } from 'src/modules/products/entities/product.entity';
import { Config } from 'src/modules/config/entities/config.entity';
@Entity({ tableName: 'shops' })
@Index({ properties: ['slug'] })
@@ -109,4 +110,7 @@ export class Shop extends BaseEntity {
@Property({ nullable: true })
subscriptionStartDate?: Date;
@OneToMany(() => Config, config => config.shop)
configs = new Collection<Config>(this);
}
+1 -1
View File
@@ -113,7 +113,7 @@ export class ShopService {
}
async findBySlug(slug: string): Promise<Shop> {
const shop = await this.em.findOne(Shop, { slug });
const shop = await this.em.findOne(Shop, { slug }, { populate: ['configs'] });
if (!shop) {
throw new NotFoundException(ShopMessage.NOT_FOUND);