# ET-SETTINGS-BACKEND: Servicios y API REST ## Identificacion | Campo | Valor | |-------|-------| | **ID** | ET-SETTINGS-BACKEND | | **Modulo** | MGN-006 Settings | | **Version** | 1.0 | | **Estado** | En Diseno | | **Framework** | NestJS | | **Autor** | Requirements-Analyst | | **Fecha** | 2025-12-05 | --- ## Estructura de Archivos ``` apps/backend/src/modules/settings/ ├── settings.module.ts ├── controllers/ │ ├── system-settings.controller.ts │ ├── tenant-settings.controller.ts │ ├── user-preferences.controller.ts │ └── feature-flags.controller.ts ├── services/ │ ├── system-settings.service.ts │ ├── tenant-settings.service.ts │ ├── user-preferences.service.ts │ └── feature-flags.service.ts ├── entities/ │ ├── system-setting.entity.ts │ ├── tenant-setting.entity.ts │ ├── user-preference.entity.ts │ ├── feature-flag.entity.ts │ └── feature-flag-override.entity.ts ├── dto/ │ ├── update-setting.dto.ts │ ├── update-preference.dto.ts │ └── feature-flag.dto.ts ├── decorators/ │ └── setting.decorator.ts └── guards/ └── feature-flag.guard.ts ``` --- ## Entidades ### SystemSetting Entity ```typescript @Entity('system_settings', { schema: 'core_settings' }) export class SystemSetting { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true, length: 100 }) key: string; @Column({ type: 'jsonb' }) value: any; @Column({ name: 'data_type', length: 20 }) dataType: SettingDataType; @Column({ length: 50 }) category: string; @Column({ type: 'text', nullable: true }) description: string; @Column({ name: 'is_public', default: false }) isPublic: boolean; @Column({ name: 'is_editable', default: true }) isEditable: boolean; @Column({ name: 'default_value', type: 'jsonb', nullable: true }) defaultValue: any; @Column({ name: 'validation_rules', type: 'jsonb', default: {} }) validationRules: ValidationRules; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; } ``` ### FeatureFlag Entity ```typescript @Entity('feature_flags', { schema: 'core_settings' }) export class FeatureFlag { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true, length: 100 }) key: string; @Column({ length: 255 }) name: string; @Column({ type: 'text', nullable: true }) description: string; @Column({ name: 'flag_type', length: 20, default: 'boolean' }) flagType: FeatureFlagType; @Column({ name: 'default_value', type: 'jsonb', default: false }) defaultValue: any; @Column({ name: 'rollout_config', type: 'jsonb', default: {} }) rolloutConfig: RolloutConfig; @Column({ name: 'is_active', default: true }) isActive: boolean; @Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) expiresAt: Date; @OneToMany(() => FeatureFlagOverride, o => o.featureFlag) overrides: FeatureFlagOverride[]; } ``` --- ## Servicios ### SystemSettingsService ```typescript @Injectable() export class SystemSettingsService { constructor( @InjectRepository(SystemSetting) private readonly repo: Repository, private readonly cacheManager: Cache, ) {} async getAll(category?: string): Promise { const cacheKey = `system_settings:${category || 'all'}`; let settings = await this.cacheManager.get(cacheKey); if (!settings) { const qb = this.repo.createQueryBuilder('s'); if (category) qb.where('s.category = :category', { category }); settings = await qb.orderBy('s.key').getMany(); await this.cacheManager.set(cacheKey, settings, 3600); } return settings; } async get(key: string): Promise { const setting = await this.repo.findOne({ where: { key } }); if (!setting) throw new NotFoundException(`Setting ${key} not found`); return setting; } async update(key: string, value: any, userId: string): Promise { const setting = await this.get(key); if (!setting.isEditable) { throw new ForbiddenException('Setting is not editable'); } this.validateValue(value, setting.dataType, setting.validationRules); setting.value = value; const saved = await this.repo.save(setting); await this.cacheManager.del(`system_settings:${setting.category}`); await this.cacheManager.del('system_settings:all'); return saved; } async reset(key: string): Promise { const setting = await this.get(key); setting.value = setting.defaultValue; return this.repo.save(setting); } private validateValue(value: any, dataType: string, rules: ValidationRules): void { // Implementar validacion segun tipo y reglas } } ``` ### FeatureFlagsService ```typescript @Injectable() export class FeatureFlagsService { constructor( @InjectRepository(FeatureFlag) private readonly flagRepo: Repository, @InjectRepository(FeatureFlagOverride) private readonly overrideRepo: Repository, private readonly cacheManager: Cache, ) {} async evaluate( key: string, context: { tenantId?: string; userId?: string } ): Promise { const cacheKey = `feature:${key}:${context.tenantId}:${context.userId}`; let result = await this.cacheManager.get(cacheKey); if (result) return result; const flag = await this.flagRepo.findOne({ where: { key, isActive: true }, relations: ['overrides'], }); if (!flag || (flag.expiresAt && flag.expiresAt < new Date())) { return { enabled: false, source: 'not_found' }; } // Check user override if (context.userId) { const userOverride = flag.overrides.find( o => o.level === 'user' && o.levelId === context.userId && o.isActive ); if (userOverride) { result = { enabled: userOverride.value, source: 'user_override' }; await this.cacheManager.set(cacheKey, result, 300); return result; } } // Check tenant override if (context.tenantId) { const tenantOverride = flag.overrides.find( o => o.level === 'tenant' && o.levelId === context.tenantId && o.isActive ); if (tenantOverride) { result = { enabled: tenantOverride.value, source: 'tenant_override' }; await this.cacheManager.set(cacheKey, result, 300); return result; } } // Evaluate based on flag type result = this.evaluateFlag(flag, context); await this.cacheManager.set(cacheKey, result, 300); return result; } private evaluateFlag( flag: FeatureFlag, context: { userId?: string } ): FeatureEvaluationResult { switch (flag.flagType) { case 'boolean': return { enabled: flag.defaultValue, source: 'default' }; case 'percentage': const config = flag.rolloutConfig as PercentageConfig; const hash = this.hashUserId(context.userId, flag.key); const enabled = hash < config.percentage; return { enabled, source: 'percentage' }; case 'variant': const variantConfig = flag.rolloutConfig as VariantConfig; const variant = this.selectVariant(context.userId, flag.key, variantConfig); return { enabled: true, variant, source: 'variant' }; default: return { enabled: false, source: 'unknown' }; } } private hashUserId(userId: string, seed: string): number { // Deterministic hash 0-100 const hash = createHash('md5').update(`${userId}:${seed}`).digest('hex'); return parseInt(hash.substring(0, 8), 16) % 100; } async createOverride( flagKey: string, dto: CreateOverrideDto ): Promise { const flag = await this.flagRepo.findOne({ where: { key: flagKey } }); if (!flag) throw new NotFoundException('Flag not found'); const override = this.overrideRepo.create({ featureFlagId: flag.id, level: dto.level, levelId: dto.levelId, value: dto.value, }); const saved = await this.overrideRepo.save(override); await this.invalidateCache(flagKey); return saved; } private async invalidateCache(key: string): Promise { const keys = await this.cacheManager.store.keys(`feature:${key}:*`); await Promise.all(keys.map(k => this.cacheManager.del(k))); } } ``` ### UserPreferencesService ```typescript @Injectable() export class UserPreferencesService { constructor( @InjectRepository(UserPreference) private readonly repo: Repository, private readonly tenantSettingsService: TenantSettingsService, ) {} async getAll(userId: string): Promise> { const prefs = await this.repo.find({ where: { userId } }); return prefs.reduce((acc, p) => { this.setNestedValue(acc, p.key, p.value); return acc; }, {}); } async update( userId: string, updates: Record ): Promise<{ updated: string[]; syncedAt: Date }> { const updated: string[] = []; const now = new Date(); for (const [key, value] of Object.entries(updates)) { await this.repo.upsert( { userId, key, value, syncedAt: now }, ['userId', 'key'] ); updated.push(key); } return { updated, syncedAt: now }; } async reset(userId: string, keys?: string[]): Promise { const qb = this.repo.createQueryBuilder() .delete() .where('user_id = :userId', { userId }); if (keys?.length) { qb.andWhere('key IN (:...keys)', { keys }); } await qb.execute(); } async getEffective( userId: string, tenantId: string ): Promise> { const tenantSettings = await this.tenantSettingsService.getEffective(tenantId); const userPrefs = await this.getAll(userId); // User prefs override tenant settings return this.deepMerge(tenantSettings, userPrefs); } } ``` --- ## Controladores ### SystemSettingsController ```typescript @ApiTags('System Settings') @Controller('settings/system') @UseGuards(JwtAuthGuard, RbacGuard) export class SystemSettingsController { constructor(private readonly service: SystemSettingsService) {} @Get() @Permissions('settings.system.read') async findAll(@Query('category') category?: string) { return this.service.getAll(category); } @Get(':key') @Permissions('settings.system.read') async findOne(@Param('key') key: string) { return this.service.get(key); } @Put(':key') @Permissions('settings.system.update') async update( @Param('key') key: string, @Body() dto: UpdateSettingDto, @CurrentUser() user: User ) { return this.service.update(key, dto.value, user.id); } @Post(':key/reset') @Permissions('settings.system.update') async reset(@Param('key') key: string) { return this.service.reset(key); } } ``` ### FeatureFlagsController ```typescript @ApiTags('Feature Flags') @Controller('features') @UseGuards(JwtAuthGuard) export class FeatureFlagsController { constructor(private readonly service: FeatureFlagsService) {} @Get(':key') async evaluate( @Param('key') key: string, @TenantId() tenantId: string, @CurrentUser() user: User ) { return this.service.evaluate(key, { tenantId, userId: user.id }); } @Post('evaluate') async evaluateMultiple( @Body('keys') keys: string[], @TenantId() tenantId: string, @CurrentUser() user: User ) { const results: Record = {}; for (const key of keys) { results[key] = await this.service.evaluate(key, { tenantId, userId: user.id }); } return results; } } @ApiTags('Feature Flags Admin') @Controller('admin/features') @UseGuards(JwtAuthGuard, RbacGuard) @Permissions('settings.features.manage') export class FeatureFlagsAdminController { constructor(private readonly service: FeatureFlagsService) {} @Get() async findAll() { return this.service.findAll(); } @Post(':key/overrides') async createOverride( @Param('key') key: string, @Body() dto: CreateOverrideDto ) { return this.service.createOverride(key, dto); } @Patch(':key') async updateRollout( @Param('key') key: string, @Body() dto: UpdateRolloutDto ) { return this.service.updateRollout(key, dto); } } ``` --- ## Decoradores y Guards ### FeatureFlag Guard ```typescript // guards/feature-flag.guard.ts @Injectable() export class FeatureFlagGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly featureFlagsService: FeatureFlagsService, ) {} async canActivate(context: ExecutionContext): Promise { const requiredFlag = this.reflector.get( 'feature_flag', context.getHandler() ); if (!requiredFlag) return true; const request = context.switchToHttp().getRequest(); const result = await this.featureFlagsService.evaluate(requiredFlag, { tenantId: request.tenantId, userId: request.user?.id, }); if (!result.enabled) { throw new ForbiddenException('Feature not available'); } return true; } } // decorators/feature-flag.decorator.ts export const RequireFeature = (flag: string) => SetMetadata('feature_flag', flag); // Uso: @Get('new-dashboard') @RequireFeature('feature.new_dashboard') async getNewDashboard() { ... } ``` --- ## API Endpoints Summary | Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /settings/system | settings.system.read | List system settings | | GET | /settings/system/:key | settings.system.read | Get setting | | PUT | /settings/system/:key | settings.system.update | Update setting | | POST | /settings/system/:key/reset | settings.system.update | Reset to default | | GET | /settings/tenant | settings.tenant.read | List tenant settings | | GET | /settings/tenant/effective | settings.tenant.read | Get effective settings | | PUT | /settings/tenant/:key | settings.tenant.update | Update tenant setting | | DELETE | /settings/tenant/:key | settings.tenant.update | Remove override | | GET | /settings/user | - | Get user preferences | | PATCH | /settings/user | - | Update preferences | | POST | /settings/user/reset | - | Reset preferences | | GET | /features/:key | - | Evaluate flag | | POST | /features/evaluate | - | Evaluate multiple | | GET | /admin/features | settings.features.manage | List flags | | POST | /admin/features/:key/overrides | settings.features.manage | Create override | | PATCH | /admin/features/:key | settings.features.manage | Update rollout | --- ## Historial | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial |