import { IRepository, QueryOptions, PagedResult, QueryFilter } from '../contracts/repository'; import { RequestContext, TenantAccessDeniedError, NotFoundError, createRequestContext, } from '../contracts/context'; import { getRecord, putRecord, deleteRecord, getRecordsByEntity, putStoreItem, RecordEntity, } from '../storage/indexedDb'; import { ulid } from '../../shared/lib/ulid'; import { useDemoStore } from '../../stores/demo'; import { BaseEntity } from '../../entities/types'; export class BaseMockRepository implements IRepository { constructor(protected entityName: string) {} protected resolveContext(context?: RequestContext): RequestContext { return context || createRequestContext(); } public async applySimulation(): Promise { const { simulateLatency, latencyMs, simulateErrors, errorType } = useDemoStore.getState(); if (simulateLatency && latencyMs > 0) { await new Promise((resolve) => setTimeout(resolve, latencyMs)); } if (simulateErrors || (errorType && errorType !== 'none')) { switch (errorType) { case 'forbidden_403': throw new Error(`[403 Forbidden]: Отказано в доступе к ресурсу «${this.entityName}».`); case 'conflict_409': throw new Error(`[409 Conflict]: Конфликт параллельного изменения записи «${this.entityName}».`); case 'validation_422': throw new Error(`[422 Validation Error]: Ошибка валидации структуры записи «${this.entityName}».`); case 'network_error': default: throw new Error(`[Сетевая ошибка]: Не удалось связаться с сервером для «${this.entityName}».`); } } } async getById(id: string, context?: RequestContext): Promise { await this.applySimulation(); const ctx = this.resolveContext(context); const res = await getRecord(id, ctx.tenantId); if (!res || res.entityType !== this.entityName) { return null; } // Tenant check: strictly prevent data leakage across tenants const recordTenant = res.tenant_id || 'tenant_main'; if (recordTenant !== ctx.tenantId) { throw new TenantAccessDeniedError( `Отказано в доступе: запись «${this.entityName}» с ID ${id} принадлежит тенанту «${recordTenant}» (запрос от «${ctx.tenantId}»).` ); } return res; } async list(options?: QueryOptions, context?: RequestContext): Promise> { await this.applySimulation(); const ctx = this.resolveContext(context); const sortOption = options?.sort ? { field: String(options.sort.field), order: options.sort.order } : undefined; const result = await getRecordsByEntity(this.entityName, { search: options?.search, filter: options?.filter as Record, sort: sortOption, page: options?.page, pageSize: options?.pageSize, tenantId: ctx.tenantId, }); return { items: result.items as T[], total: result.total, page: result.page, pageSize: result.pageSize, totalPages: result.totalPages, }; } async create(data: Omit, context?: RequestContext): Promise { await this.applySimulation(); const ctx = this.resolveContext(context); const now = useDemoStore.getState().demoDate || new Date().toISOString(); const userId = ctx.actorId || useDemoStore.getState().currentRoleId || 'demo_user'; const id = ulid(); const entity = { ...data, id, entityType: this.entityName, tenant_id: ctx.tenantId, updatedAt: now, audit: { createdAt: now, createdById: userId, updatedAt: now, updatedById: userId, version: 1, tenantId: ctx.tenantId, correlationId: ctx.correlationId, }, } as unknown as T & RecordEntity; await putRecord(entity, ctx.tenantId); // Write audit log with tenant & correlation tracing await putStoreItem( 'audit_log', { id: ulid(), entityType: this.entityName, entityId: id, tenant_id: ctx.tenantId, correlation_id: ctx.correlationId, action: 'create', userId, userName: `Роль: ${userId}`, timestamp: now, summary: `Создана запись «${this.entityName}» (${id.slice(0, 8)}...)`, }, ctx.tenantId ); return entity as T; } async update(id: string, data: Partial, context?: RequestContext): Promise { await this.applySimulation(); const ctx = this.resolveContext(context); const existing = await this.getById(id, ctx); if (!existing) { throw new NotFoundError(`Запись «${this.entityName}» с ID ${id} не найдена.`); } const now = useDemoStore.getState().demoDate || new Date().toISOString(); const userId = ctx.actorId || useDemoStore.getState().currentRoleId || 'demo_user'; const updated = { ...existing, ...data, id, entityType: this.entityName, tenant_id: ctx.tenantId, updatedAt: now, audit: { ...existing.audit, updatedAt: now, updatedById: userId, version: (existing.audit?.version || 1) + 1, tenantId: ctx.tenantId, correlationId: ctx.correlationId, }, } as unknown as T & RecordEntity; await putRecord(updated, ctx.tenantId); // Write audit log with tenant & correlation tracing await putStoreItem( 'audit_log', { id: ulid(), entityType: this.entityName, entityId: id, tenant_id: ctx.tenantId, correlation_id: ctx.correlationId, action: 'update', userId, userName: `Роль: ${userId}`, timestamp: now, summary: `Обновлена запись «${this.entityName}» (${id.slice(0, 8)}...)`, }, ctx.tenantId ); return updated as T; } async delete(id: string, context?: RequestContext): Promise { await this.applySimulation(); const ctx = this.resolveContext(context); const existing = await this.getById(id, ctx); if (!existing) { return false; } await deleteRecord(id, this.entityName, ctx.tenantId); const now = useDemoStore.getState().demoDate || new Date().toISOString(); const userId = ctx.actorId || useDemoStore.getState().currentRoleId || 'demo_user'; // Write audit log with tenant & correlation tracing await putStoreItem( 'audit_log', { id: ulid(), entityType: this.entityName, entityId: id, tenant_id: ctx.tenantId, correlation_id: ctx.correlationId, action: 'delete', userId, userName: `Роль: ${userId}`, timestamp: now, summary: `Удалена запись «${this.entityName}» (${id.slice(0, 8)}...)`, }, ctx.tenantId ); return true; } async batchUpdate( items: { id: string; data: Partial }[], context?: RequestContext ): Promise { const updated: T[] = []; for (const item of items) { const res = await this.update(item.id, item.data, context); updated.push(res); } return updated; } async count(filter?: QueryFilter, context?: RequestContext): Promise { await this.applySimulation(); const result = await this.list({ filter }, context); return result.total; } }