workspace-v1/shared/catalog/feature-flags/_reference/feature-flags.service.reference.ts
rckrdmrd cb4c0681d3 feat(workspace): Add new projects and update architecture
New projects created:
- michangarrito (marketplace mobile)
- template-saas (SaaS template)
- clinica-dental (dental ERP)
- clinica-veterinaria (veterinary ERP)

Architecture updates:
- Move catalog from core/ to shared/
- Add MCP servers structure and templates
- Add git management scripts
- Update SUBREPOSITORIOS.md with 15 new repos
- Update .gitignore for new projects

Repository infrastructure:
- 4 main repositories
- 11 subrepositorios
- Gitea remotes configured

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:43:28 -06:00

248 lines
6.6 KiB
TypeScript

/**
* FEATURE FLAGS SERVICE - REFERENCE IMPLEMENTATION
*
* @description Servicio de feature flags para activación/desactivación
* de funcionalidades en runtime.
*
* @usage
* ```typescript
* if (await this.featureFlags.isEnabled('new-dashboard', userId)) {
* return this.newDashboard();
* }
* return this.legacyDashboard();
* ```
*
* @origin gamilit/apps/backend/src/modules/admin/services/feature-flags.service.ts
*/
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
// Adaptar imports según proyecto
// import { FeatureFlag, FeatureFlagOverride } from '../entities';
/**
* Estrategias de rollout
*/
export enum RolloutStrategyEnum {
/** Todos los usuarios */
ALL = 'all',
/** Ningún usuario */
NONE = 'none',
/** Porcentaje de usuarios */
PERCENTAGE = 'percentage',
/** Lista de usuarios específicos */
USER_LIST = 'user_list',
/** Por rol */
ROLE = 'role',
/** Por tenant */
TENANT = 'tenant',
}
@Injectable()
export class FeatureFlagsService {
private readonly logger = new Logger(FeatureFlagsService.name);
private cache = new Map<string, FeatureFlag>();
private cacheExpiry = 0;
private readonly CACHE_TTL = 60000; // 1 minuto
constructor(
@InjectRepository(FeatureFlag, 'config')
private readonly flagRepo: Repository<FeatureFlag>,
@InjectRepository(FeatureFlagOverride, 'config')
private readonly overrideRepo: Repository<FeatureFlagOverride>,
) {}
/**
* Verificar si una feature está habilitada
*
* @param flagKey - Clave única del feature flag
* @param context - Contexto del usuario para evaluación
*/
async isEnabled(flagKey: string, context?: FeatureFlagContext): Promise<boolean> {
// 1. Verificar override específico del usuario
if (context?.userId) {
const override = await this.getUserOverride(flagKey, context.userId);
if (override !== null) {
return override;
}
}
// 2. Obtener flag (con cache)
const flag = await this.getFlag(flagKey);
if (!flag) {
this.logger.warn(`Feature flag not found: ${flagKey}`);
return false;
}
// 3. Evaluar según estrategia
return this.evaluateStrategy(flag, context);
}
/**
* Obtener todos los flags con su estado para un usuario
*/
async getAllFlags(context?: FeatureFlagContext): Promise<Record<string, boolean>> {
await this.refreshCacheIfNeeded();
const result: Record<string, boolean> = {};
for (const [key, flag] of this.cache) {
result[key] = await this.isEnabled(key, context);
}
return result;
}
/**
* Crear o actualizar un feature flag
*/
async upsertFlag(dto: UpsertFeatureFlagDto): Promise<FeatureFlag> {
let flag = await this.flagRepo.findOne({ where: { key: dto.key } });
if (flag) {
Object.assign(flag, dto);
} else {
flag = this.flagRepo.create(dto);
}
const saved = await this.flagRepo.save(flag);
this.invalidateCache();
return saved;
}
/**
* Establecer override para un usuario específico
*/
async setUserOverride(
flagKey: string,
userId: string,
enabled: boolean,
): Promise<void> {
await this.overrideRepo.upsert(
{ flag_key: flagKey, user_id: userId, enabled },
['flag_key', 'user_id'],
);
}
/**
* Eliminar override de usuario
*/
async removeUserOverride(flagKey: string, userId: string): Promise<void> {
await this.overrideRepo.delete({ flag_key: flagKey, user_id: userId });
}
// ============ HELPERS PRIVADOS ============
private async getFlag(key: string): Promise<FeatureFlag | null> {
await this.refreshCacheIfNeeded();
return this.cache.get(key) || null;
}
private async getUserOverride(flagKey: string, userId: string): Promise<boolean | null> {
const override = await this.overrideRepo.findOne({
where: { flag_key: flagKey, user_id: userId },
});
return override?.enabled ?? null;
}
private evaluateStrategy(flag: FeatureFlag, context?: FeatureFlagContext): boolean {
if (!flag.is_active) return false;
switch (flag.strategy) {
case RolloutStrategyEnum.ALL:
return true;
case RolloutStrategyEnum.NONE:
return false;
case RolloutStrategyEnum.PERCENTAGE:
if (!context?.userId) return false;
// Hash consistente basado en userId
const hash = this.hashUserId(context.userId, flag.key);
return hash < (flag.percentage || 0);
case RolloutStrategyEnum.USER_LIST:
if (!context?.userId) return false;
return (flag.user_list || []).includes(context.userId);
case RolloutStrategyEnum.ROLE:
if (!context?.role) return false;
return (flag.allowed_roles || []).includes(context.role);
case RolloutStrategyEnum.TENANT:
if (!context?.tenantId) return false;
return (flag.allowed_tenants || []).includes(context.tenantId);
default:
return false;
}
}
private hashUserId(userId: string, flagKey: string): number {
// Hash simple para distribución consistente
const str = `${userId}-${flagKey}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash % 100);
}
private async refreshCacheIfNeeded(): Promise<void> {
if (Date.now() > this.cacheExpiry) {
const flags = await this.flagRepo.find();
this.cache.clear();
for (const flag of flags) {
this.cache.set(flag.key, flag);
}
this.cacheExpiry = Date.now() + this.CACHE_TTL;
}
}
private invalidateCache(): void {
this.cacheExpiry = 0;
}
}
// ============ TIPOS ============
interface FeatureFlagContext {
userId?: string;
role?: string;
tenantId?: string;
}
interface FeatureFlag {
id: string;
key: string;
name: string;
description?: string;
is_active: boolean;
strategy: RolloutStrategyEnum;
percentage?: number;
user_list?: string[];
allowed_roles?: string[];
allowed_tenants?: string[];
}
interface FeatureFlagOverride {
flag_key: string;
user_id: string;
enabled: boolean;
}
interface UpsertFeatureFlagDto {
key: string;
name: string;
description?: string;
is_active?: boolean;
strategy?: RolloutStrategyEnum;
percentage?: number;
user_list?: string[];
allowed_roles?: string[];
allowed_tenants?: string[];
}