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
@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
@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
@Injectable()
export class SystemSettingsService {
constructor(
@InjectRepository(SystemSetting)
private readonly repo: Repository<SystemSetting>,
private readonly cacheManager: Cache,
) {}
async getAll(category?: string): Promise<SystemSetting[]> {
const cacheKey = `system_settings:${category || 'all'}`;
let settings = await this.cacheManager.get<SystemSetting[]>(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<SystemSetting> {
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<SystemSetting> {
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<SystemSetting> {
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
@Injectable()
export class FeatureFlagsService {
constructor(
@InjectRepository(FeatureFlag)
private readonly flagRepo: Repository<FeatureFlag>,
@InjectRepository(FeatureFlagOverride)
private readonly overrideRepo: Repository<FeatureFlagOverride>,
private readonly cacheManager: Cache,
) {}
async evaluate(
key: string,
context: { tenantId?: string; userId?: string }
): Promise<FeatureEvaluationResult> {
const cacheKey = `feature:${key}:${context.tenantId}:${context.userId}`;
let result = await this.cacheManager.get<FeatureEvaluationResult>(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<FeatureFlagOverride> {
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<void> {
const keys = await this.cacheManager.store.keys(`feature:${key}:*`);
await Promise.all(keys.map(k => this.cacheManager.del(k)));
}
}
UserPreferencesService
@Injectable()
export class UserPreferencesService {
constructor(
@InjectRepository(UserPreference)
private readonly repo: Repository<UserPreference>,
private readonly tenantSettingsService: TenantSettingsService,
) {}
async getAll(userId: string): Promise<Record<string, any>> {
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<string, any>
): 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<void> {
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<Record<string, any>> {
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
@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
@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<string, FeatureEvaluationResult> = {};
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
// 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<boolean> {
const requiredFlag = this.reflector.get<string>(
'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 |