236 lines
7.3 KiB
TypeScript
236 lines
7.3 KiB
TypeScript
import { logger } from '@goodgo/logger';
|
|
|
|
import { prisma } from '../../config/database.config';
|
|
import { ConflictError } from '../../errors/http-error';
|
|
import { BaseRepository, IRepository } from '../common/repository';
|
|
|
|
// EN: Feature entity type from Prisma
|
|
// VI: Feature entity type từ Prisma
|
|
type Feature = {
|
|
id: string;
|
|
name: string;
|
|
title: string | null;
|
|
description: string | null;
|
|
config: any;
|
|
enabled: boolean;
|
|
version: string | null;
|
|
tags: string[];
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
};
|
|
|
|
// EN: Input types for create/update operations
|
|
// VI: Input types cho create/update operations
|
|
type CreateFeatureInput = {
|
|
name: string;
|
|
title?: string;
|
|
description?: string;
|
|
config?: any;
|
|
tags?: string[];
|
|
};
|
|
|
|
type UpdateFeatureInput = {
|
|
title?: string;
|
|
description?: string;
|
|
config?: any;
|
|
enabled?: boolean;
|
|
tags?: string[];
|
|
};
|
|
|
|
/**
|
|
* EN: Feature repository implementing repository pattern
|
|
* VI: Feature repository implement repository pattern
|
|
*/
|
|
export class FeatureRepository extends BaseRepository<Feature, CreateFeatureInput, UpdateFeatureInput>
|
|
implements IRepository<Feature, CreateFeatureInput, UpdateFeatureInput> {
|
|
|
|
constructor() {
|
|
super(prisma, 'feature');
|
|
}
|
|
|
|
/**
|
|
* EN: Find feature by name (unique field)
|
|
* VI: Tìm feature theo tên (field duy nhất)
|
|
*/
|
|
async findByName(name: string): Promise<Feature | null> {
|
|
return this.findByUnique('name', name);
|
|
}
|
|
|
|
/**
|
|
* EN: Find features by tags
|
|
* VI: Tìm features theo tags
|
|
*/
|
|
async findByTags(tags: string[]): Promise<Feature[]> {
|
|
try {
|
|
logger.debug('Finding features by tags / Tìm features theo tags', { tags });
|
|
|
|
const features = await (this.prisma as any).feature.findMany({
|
|
where: {
|
|
tags: {
|
|
hasSome: tags,
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
logger.debug(`Found ${features.length} features by tags / Đã tìm thấy ${features.length} features theo tags`, { tags });
|
|
return features;
|
|
} catch (error) {
|
|
logger.error('Failed to find features by tags / Không thể tìm features theo tags', { error, tags });
|
|
throw this.handleDatabaseError(error, { tags });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EN: Find enabled features only
|
|
* VI: Tìm chỉ features đã được bật
|
|
*/
|
|
async findEnabled(): Promise<Feature[]> {
|
|
try {
|
|
logger.debug('Finding enabled features / Tìm features đã được bật');
|
|
|
|
const features = await (this.prisma as any).feature.findMany({
|
|
where: { enabled: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
logger.debug(`Found ${features.length} enabled features / Đã tìm thấy ${features.length} features đã được bật`);
|
|
return features;
|
|
} catch (error) {
|
|
logger.error('Failed to find enabled features / Không thể tìm features đã được bật', { error });
|
|
throw this.handleDatabaseError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EN: Create feature with duplicate name check
|
|
* VI: Tạo feature với kiểm tra tên trùng lặp
|
|
*/
|
|
async create(data: CreateFeatureInput): Promise<Feature> {
|
|
try {
|
|
// EN: Check for duplicate name
|
|
// VI: Kiểm tra tên trùng lặp
|
|
const existingFeature = await this.findByName(data.name);
|
|
if (existingFeature) {
|
|
logger.warn('Feature with this name already exists / Feature với tên này đã tồn tại', { name: data.name });
|
|
throw new ConflictError('Feature with this name already exists / Feature với tên này đã tồn tại', { name: data.name });
|
|
}
|
|
|
|
return await super.create(data);
|
|
} catch (error) {
|
|
if (error instanceof ConflictError) {
|
|
throw error;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EN: Toggle feature enabled/disabled status
|
|
* VI: Bật/tắt trạng thái feature
|
|
*/
|
|
async toggleEnabled(id: string): Promise<Feature> {
|
|
try {
|
|
logger.debug('Toggling feature enabled status / Chuyển đổi trạng thái feature', { id });
|
|
|
|
const feature = await this.findById(id);
|
|
if (!feature) {
|
|
throw new ConflictError('Feature not found / Feature không tìm thấy', { id });
|
|
}
|
|
|
|
const updatedFeature = await this.update(id, {
|
|
enabled: !feature.enabled,
|
|
});
|
|
|
|
logger.debug(`Feature ${updatedFeature.enabled ? 'enabled' : 'disabled'} / Feature đã được ${updatedFeature.enabled ? 'bật' : 'tắt'}`, { id });
|
|
return updatedFeature;
|
|
} catch (error) {
|
|
if (error instanceof ConflictError) {
|
|
throw error;
|
|
}
|
|
logger.error('Failed to toggle feature status / Không thể chuyển đổi trạng thái feature', { error, id });
|
|
throw this.handleDatabaseError(error, { id });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EN: Search features by name or description
|
|
* VI: Tìm kiếm features theo tên hoặc mô tả
|
|
*/
|
|
async search(query: string, limit: number = 10): Promise<Feature[]> {
|
|
try {
|
|
logger.debug('Searching features / Tìm kiếm features', { query, limit });
|
|
|
|
const features = await (this.prisma as any).feature.findMany({
|
|
where: {
|
|
OR: [
|
|
{ name: { contains: query, mode: 'insensitive' } },
|
|
{ title: { contains: query, mode: 'insensitive' } },
|
|
{ description: { contains: query, mode: 'insensitive' } },
|
|
],
|
|
},
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
logger.debug(`Found ${features.length} features matching search / Đã tìm thấy ${features.length} features khớp với tìm kiếm`, { query });
|
|
return features;
|
|
} catch (error) {
|
|
logger.error('Failed to search features / Không thể tìm kiếm features', { error, query });
|
|
throw this.handleDatabaseError(error, { query });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EN: Get feature statistics
|
|
* VI: Lấy thống kê feature
|
|
*/
|
|
async getStatistics(): Promise<{
|
|
total: number;
|
|
enabled: number;
|
|
disabled: number;
|
|
byTag: Record<string, number>;
|
|
}> {
|
|
try {
|
|
logger.debug('Getting feature statistics / Lấy thống kê feature');
|
|
|
|
const [total, enabled, disabled, features] = await Promise.all([
|
|
this.count(),
|
|
this.count({ enabled: true }),
|
|
this.count({ enabled: false }),
|
|
this.findAll(),
|
|
]);
|
|
|
|
// EN: Count by tags
|
|
// VI: Đếm theo tags
|
|
const byTag: Record<string, number> = {};
|
|
features.forEach(feature => {
|
|
feature.tags.forEach(tag => {
|
|
byTag[tag] = (byTag[tag] || 0) + 1;
|
|
});
|
|
});
|
|
|
|
const statistics = { total, enabled, disabled, byTag };
|
|
logger.debug('Feature statistics retrieved / Thống kê feature đã được lấy', statistics);
|
|
return statistics;
|
|
} catch (error) {
|
|
logger.error('Failed to get feature statistics / Không thể lấy thống kê feature', { error });
|
|
throw this.handleDatabaseError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EN: Handle database-specific errors
|
|
* VI: Xử lý lỗi database-specific
|
|
*/
|
|
private handleDatabaseError(error: any, context?: any) {
|
|
if (error.code === 'P2002') {
|
|
return new ConflictError('Feature with this name already exists / Feature với tên này đã tồn tại', context);
|
|
}
|
|
return error;
|
|
}
|
|
}
|
|
|
|
// EN: Singleton instance
|
|
// VI: Singleton instance
|
|
export const featureRepository = new FeatureRepository(); |