workspace-v1/shared/libs/feature-flags/_reference/feature-flags.service.reference.ts
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -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[];
}