diff --git a/internal/storage/trigger.go b/internal/storage/trigger.go new file mode 100644 index 0000000..3895667 --- /dev/null +++ b/internal/storage/trigger.go @@ -0,0 +1,950 @@ +/* + * Copyright 2026 Safronov Grigorii + * + * Licensed under the CDDL, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * https://opensource.org/licenses/CDDL-1.0 + */ + +// Файл: internal/storage/trigger.go +// Назначение: Реализация триггеров, похожих на MongoDB trigger syntax. +// Поддерживает события: INSERT, UPDATE, DELETE, REPLACE. +// Триггеры могут выполняться до или после события. + +package storage + +import ( + "fmt" + "regexp" + "strings" + "sync" + "time" + + "futriis/internal/log" +) + +// ============================================================================= +// КОНСТАНТЫ +// ============================================================================= + +const ( + MaxVersionsPerDoc = 100 + VersionRetentionDays = 7 + VisibilityMapSize = 1024 * 1024 +) + +// ============================================================================= +// ТИПЫ ТРИГГЕРОВ +// ============================================================================= + +// TriggerEvent определяет тип события для триггера +type TriggerEvent string + +const ( + TriggerBeforeInsert TriggerEvent = "BEFORE_INSERT" + TriggerAfterInsert TriggerEvent = "AFTER_INSERT" + TriggerBeforeUpdate TriggerEvent = "BEFORE_UPDATE" + TriggerAfterUpdate TriggerEvent = "AFTER_UPDATE" + TriggerBeforeDelete TriggerEvent = "BEFORE_DELETE" + TriggerAfterDelete TriggerEvent = "AFTER_DELETE" + TriggerBeforeReplace TriggerEvent = "BEFORE_REPLACE" + TriggerAfterReplace TriggerEvent = "AFTER_REPLACE" +) + +// TriggerAction определяет действие триггера +type TriggerAction string + +const ( + ActionAbort TriggerAction = "abort" // Прервать операцию + ActionSkip TriggerAction = "skip" // Пропустить операцию + ActionModify TriggerAction = "modify" // Модифицировать документ + ActionLog TriggerAction = "log" // Записать в лог + ActionNotify TriggerAction = "notify" // Отправить уведомление + ActionCustom TriggerAction = "custom" // Пользовательское действие +) + +// Trigger представляет триггер на коллекции +type Trigger struct { + Name string `msgpack:"name" json:"name"` + Collection string `msgpack:"collection" json:"collection"` + Event string `msgpack:"event" json:"event"` + Action string `msgpack:"action" json:"action"` + Condition *TriggerCondition `msgpack:"condition" json:"condition"` + Operations []TriggerOperation `msgpack:"operations" json:"operations"` + CreatedAt int64 `msgpack:"created_at" json:"created_at"` + UpdatedAt int64 `msgpack:"updated_at" json:"updated_at"` + Enabled bool `msgpack:"enabled" json:"enabled"` + Description string `msgpack:"description" json:"description"` + EventType TriggerEvent `msgpack:"-" json:"-"` + ActionType TriggerAction `msgpack:"-" json:"-"` + mu sync.RWMutex `msgpack:"-" json:"-"` +} + +// GetEventType возвращает типизированное событие +func (t *Trigger) GetEventType() TriggerEvent { + t.mu.RLock() + defer t.mu.RUnlock() + if t.EventType != "" { + return t.EventType + } + return TriggerEvent(t.Event) +} + +// GetActionType возвращает типизированное действие +func (t *Trigger) GetActionType() TriggerAction { + t.mu.RLock() + defer t.mu.RUnlock() + if t.ActionType != "" { + return t.ActionType + } + return TriggerAction(t.Action) +} + +// SetEventType устанавливает типизированное событие +func (t *Trigger) SetEventType(event TriggerEvent) { + t.mu.Lock() + defer t.mu.Unlock() + t.EventType = event + t.Event = string(event) +} + +// SetActionType устанавливает типизированное действие +func (t *Trigger) SetActionType(action TriggerAction) { + t.mu.Lock() + defer t.mu.Unlock() + t.ActionType = action + t.Action = string(action) +} + +// IsEnabled проверяет, включен ли триггер +func (t *Trigger) IsEnabled() bool { + t.mu.RLock() + defer t.mu.RUnlock() + return t.Enabled +} + +// GetEvent возвращает строковое представление события +func (t *Trigger) GetEvent() string { + t.mu.RLock() + defer t.mu.RUnlock() + return t.Event +} + +// GetAction возвращает строковое представление действия +func (t *Trigger) GetAction() string { + t.mu.RLock() + defer t.mu.RUnlock() + return t.Action +} + +// TriggerCondition определяет условие выполнения триггера +type TriggerCondition struct { + Field string `msgpack:"field" json:"field"` + Operator string `msgpack:"operator" json:"operator"` + Value interface{} `msgpack:"value" json:"value"` + Match string `msgpack:"match" json:"match"` +} + +// TriggerOperation определяет операцию, выполняемую триггером +type TriggerOperation struct { + Type string `msgpack:"type" json:"type"` + Field string `msgpack:"field" json:"field"` + Value interface{} `msgpack:"value" json:"value"` + Params map[string]interface{} `msgpack:"params" json:"params"` +} + +// TriggerExecution содержит контекст выполнения триггера +type TriggerExecution struct { + TriggerName string + Event TriggerEvent + Collection string + Database string + DocumentID string + OldDocument *Document + NewDocument *Document + Operation string + Timestamp time.Time + TimestampMs int64 `msgpack:"timestamp_ms"` + TimestampStr string `msgpack:"timestamp_str"` + User string + Role string + CustomData map[string]interface{} + ActionResult string `msgpack:"action_result"` + DurationMs int64 `msgpack:"duration_ms"` +} + +// ============================================================================= +// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ДЛЯ ЛОГА ВЫПОЛНЕНИЯ ТРИГГЕРОВ +// ============================================================================= + +// triggerExecutionLog - глобальный лог выполнения триггеров +var triggerExecutionLog []TriggerExecutionLogEntry +var triggerExecutionLogMu sync.RWMutex + +// TriggerExecutionLogEntry представляет запись выполнения триггера +type TriggerExecutionLogEntry struct { + Timestamp time.Time `json:"timestamp"` + TriggerName string `json:"trigger_name"` + Event string `json:"event"` + Collection string `json:"collection"` + DocumentID string `json:"document_id"` + Action string `json:"action"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +// GetTriggerExecutionLog возвращает лог выполнения триггеров +func GetTriggerExecutionLog() []TriggerExecutionLogEntry { + triggerExecutionLogMu.RLock() + defer triggerExecutionLogMu.RUnlock() + result := make([]TriggerExecutionLogEntry, len(triggerExecutionLog)) + copy(result, triggerExecutionLog) + return result +} + +// LogTriggerExecution записывает выполнение триггера +func LogTriggerExecution(triggerName, event, collection, docID, action string, success bool, err error) { + triggerExecutionLogMu.Lock() + defer triggerExecutionLogMu.Unlock() + + entry := TriggerExecutionLogEntry{ + Timestamp: time.Now(), + TriggerName: triggerName, + Event: event, + Collection: collection, + DocumentID: docID, + Action: action, + Success: success, + } + if err != nil { + entry.Error = err.Error() + } + + triggerExecutionLog = append(triggerExecutionLog, entry) + if len(triggerExecutionLog) > 1000 { + triggerExecutionLog = triggerExecutionLog[len(triggerExecutionLog)-1000:] + } +} + +// ============================================================================= +// TRIGGER MANAGER +// ============================================================================= + +// TriggerManager управляет триггерами в СУБД +type TriggerManager struct { + triggers sync.Map // map[string]*Trigger (ключ: collection|event|name) + logger *log.Logger + mu sync.RWMutex + auditLog []*TriggerExecution + maxLogSize int +} + +var ( + globalTriggerManager *TriggerManager + triggerManagerOnce sync.Once +) + +// GetTriggerManager возвращает глобальный менеджер триггеров +func GetTriggerManager() *TriggerManager { + triggerManagerOnce.Do(func() { + globalTriggerManager = &TriggerManager{ + maxLogSize: 10000, + auditLog: make([]*TriggerExecution, 0), + } + }) + return globalTriggerManager +} + +// InitTriggerManager инициализирует менеджер триггеров с логгером +func InitTriggerManager(logger *log.Logger) { + tm := GetTriggerManager() + tm.logger = logger + if logger != nil { + logger.Info("Trigger manager initialized") + } +} + +// CreateTrigger создаёт новый триггер (синтаксис MongoDB-like) +func (tm *TriggerManager) CreateTrigger(database, collection, name string, event TriggerEvent, config map[string]interface{}) error { + tm.mu.Lock() + defer tm.mu.Unlock() + + key := tm.getTriggerKey(collection, event, name) + if _, exists := tm.triggers.Load(key); exists { + return fmt.Errorf("trigger '%s' already exists on %s for event %s", name, collection, event) + } + + now := time.Now().UnixMilli() + + trigger := &Trigger{ + Name: name, + Collection: collection, + Event: string(event), + EventType: event, + Enabled: true, + CreatedAt: now, + UpdatedAt: now, + Operations: make([]TriggerOperation, 0), + } + + // Парсим конфигурацию триггера + if action, ok := config["action"].(string); ok { + switch strings.ToLower(action) { + case "abort": + trigger.Action = string(ActionAbort) + trigger.ActionType = ActionAbort + case "skip": + trigger.Action = string(ActionSkip) + trigger.ActionType = ActionSkip + case "modify": + trigger.Action = string(ActionModify) + trigger.ActionType = ActionModify + case "log": + trigger.Action = string(ActionLog) + trigger.ActionType = ActionLog + case "notify": + trigger.Action = string(ActionNotify) + trigger.ActionType = ActionNotify + default: + trigger.Action = string(ActionCustom) + trigger.ActionType = ActionCustom + } + } + + // Парсим условие + if cond, ok := config["condition"].(map[string]interface{}); ok { + trigger.Condition = &TriggerCondition{} + if field, ok := cond["field"].(string); ok { + trigger.Condition.Field = field + } + if operator, ok := cond["operator"].(string); ok { + trigger.Condition.Operator = operator + } + if value, ok := cond["value"]; ok { + trigger.Condition.Value = value + } + if match, ok := cond["match"].(string); ok { + trigger.Condition.Match = match + } + } + + // Парсим операции + if ops, ok := config["operations"].([]interface{}); ok { + for _, opRaw := range ops { + opMap, ok := opRaw.(map[string]interface{}) + if !ok { + continue + } + operation := TriggerOperation{} + if opType, ok := opMap["type"].(string); ok { + operation.Type = opType + } + if field, ok := opMap["field"].(string); ok { + operation.Field = field + } + if value, ok := opMap["value"]; ok { + operation.Value = value + } + if params, ok := opMap["params"].(map[string]interface{}); ok { + operation.Params = params + } + trigger.Operations = append(trigger.Operations, operation) + } + } + + if desc, ok := config["description"].(string); ok { + trigger.Description = desc + } + + tm.triggers.Store(key, trigger) + + // Аудит создания триггера + LogAudit("CREATE", "TRIGGER", fmt.Sprintf("%s.%s.%s", database, collection, name), map[string]interface{}{ + "event": event, + "action": trigger.Action, + "description": trigger.Description, + }) + + if tm.logger != nil { + tm.logger.Info(fmt.Sprintf("Trigger '%s' created on %s.%s for event %s", name, database, collection, event)) + } + + return nil +} + +// DropTrigger удаляет триггер +func (tm *TriggerManager) DropTrigger(collection, event, name string) error { + key := tm.getTriggerKey(collection, TriggerEvent(event), name) + if _, exists := tm.triggers.LoadAndDelete(key); !exists { + return fmt.Errorf("trigger '%s' not found on %s for event %s", name, collection, event) + } + + // Аудит удаления триггера + LogAudit("DROP", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{ + "event": event, + }) + + if tm.logger != nil { + tm.logger.Info(fmt.Sprintf("Trigger '%s' dropped from %s for event %s", name, collection, event)) + } + return nil +} + +// GetTrigger возвращает триггер по имени +func (tm *TriggerManager) GetTrigger(collection, event, name string) (*Trigger, error) { + key := tm.getTriggerKey(collection, TriggerEvent(event), name) + if val, ok := tm.triggers.Load(key); ok { + return val.(*Trigger), nil + } + return nil, fmt.Errorf("trigger not found: %s", name) +} + +// ListTriggers возвращает список всех триггеров для коллекции +func (tm *TriggerManager) ListTriggers(collection string) []*Trigger { + triggers := make([]*Trigger, 0) + tm.triggers.Range(func(key, value interface{}) bool { + trigger := value.(*Trigger) + if collection == "" || trigger.Collection == collection { + triggers = append(triggers, trigger) + } + return true + }) + return triggers +} + +// ListTriggersByEvent возвращает триггеры для конкретного события +func (tm *TriggerManager) ListTriggersByEvent(collection string, event TriggerEvent) []*Trigger { + triggers := make([]*Trigger, 0) + tm.triggers.Range(func(key, value interface{}) bool { + trigger := value.(*Trigger) + triggerEvent := trigger.GetEventType() + if trigger.Collection == collection && triggerEvent == event && trigger.IsEnabled() { + triggers = append(triggers, trigger) + } + return true + }) + return triggers +} + +// EnableTrigger включает триггер +func (tm *TriggerManager) EnableTrigger(collection, event, name string) error { + key := tm.getTriggerKey(collection, TriggerEvent(event), name) + val, ok := tm.triggers.Load(key) + if !ok { + return fmt.Errorf("trigger not found: %s", name) + } + trigger := val.(*Trigger) + trigger.mu.Lock() + trigger.Enabled = true + trigger.UpdatedAt = time.Now().UnixMilli() + trigger.mu.Unlock() + tm.triggers.Store(key, trigger) + + // Аудит включения триггера + LogAudit("ENABLE", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{ + "event": event, + }) + + return nil +} + +// DisableTrigger выключает триггер +func (tm *TriggerManager) DisableTrigger(collection, event, name string) error { + key := tm.getTriggerKey(collection, TriggerEvent(event), name) + val, ok := tm.triggers.Load(key) + if !ok { + return fmt.Errorf("trigger not found: %s", name) + } + trigger := val.(*Trigger) + trigger.mu.Lock() + trigger.Enabled = false + trigger.UpdatedAt = time.Now().UnixMilli() + trigger.mu.Unlock() + tm.triggers.Store(key, trigger) + + // Аудит выключения триггера + LogAudit("DISABLE", "TRIGGER", fmt.Sprintf("%s.%s", collection, name), map[string]interface{}{ + "event": event, + }) + + return nil +} + +// ExecuteTriggers выполняет все триггеры для данного события +// Возвращает: modifiedDocument, shouldAbort, error +func (tm *TriggerManager) ExecuteTriggers(execCtx *TriggerExecution) (*Document, bool, error) { + triggers := tm.ListTriggersByEvent(execCtx.Collection, execCtx.Event) + + if len(triggers) == 0 { + return execCtx.NewDocument, false, nil + } + + currentDoc := execCtx.NewDocument + if currentDoc == nil && execCtx.OldDocument != nil { + currentDoc = execCtx.OldDocument.Clone() + } + + for _, trigger := range triggers { + if !trigger.IsEnabled() { + continue + } + + // Проверяем условие + if trigger.Condition != nil { + if !tm.evaluateCondition(execCtx, trigger.Condition) { + continue + } + } + + startTime := time.Now() + + // Получаем типизированное действие + action := trigger.GetActionType() + + // Выполняем действие триггера + switch action { + case ActionAbort: + tm.logExecution(execCtx, trigger, "aborted", startTime) + return currentDoc, true, fmt.Errorf("operation aborted by trigger: %s", trigger.Name) + + case ActionSkip: + tm.logExecution(execCtx, trigger, "skipped", startTime) + return currentDoc, true, nil + + case ActionModify: + if currentDoc != nil { + currentDoc = tm.applyOperations(currentDoc, trigger.Operations, execCtx) + } + tm.logExecution(execCtx, trigger, "modified", startTime) + + case ActionLog: + tm.logExecution(execCtx, trigger, "logged", startTime) + if tm.logger != nil { + tm.logger.Info(fmt.Sprintf("Trigger %s executed on %s.%s (event: %s, doc: %s)", + trigger.Name, execCtx.Database, execCtx.Collection, execCtx.Event, execCtx.DocumentID)) + } + + case ActionNotify: + tm.logExecution(execCtx, trigger, "notified", startTime) + // Здесь можно отправить уведомление через WebSocket или другой канал + } + } + + return currentDoc, false, nil +} + +// evaluateCondition проверяет условие триггера +func (tm *TriggerManager) evaluateCondition(execCtx *TriggerExecution, cond *TriggerCondition) bool { + var docToCheck *Document + if execCtx.NewDocument != nil { + docToCheck = execCtx.NewDocument + } else if execCtx.OldDocument != nil { + docToCheck = execCtx.OldDocument + } else { + return false + } + + fieldValue, err := docToCheck.GetField(cond.Field) + if err != nil { + if cond.Operator == "exists" { + if existsVal, ok := cond.Value.(bool); ok && !existsVal { + return true + } + } + return false + } + + switch cond.Operator { + case "eq": + return fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", cond.Value) + case "ne": + return fmt.Sprintf("%v", fieldValue) != fmt.Sprintf("%v", cond.Value) + case "gt": + return compareNumbers(fieldValue, cond.Value) > 0 + case "lt": + return compareNumbers(fieldValue, cond.Value) < 0 + case "gte": + return compareNumbers(fieldValue, cond.Value) >= 0 + case "lte": + return compareNumbers(fieldValue, cond.Value) <= 0 + case "in": + if arr, ok := cond.Value.([]interface{}); ok { + for _, v := range arr { + if fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", v) { + return true + } + } + } + return false + case "nin": + if arr, ok := cond.Value.([]interface{}); ok { + for _, v := range arr { + if fmt.Sprintf("%v", fieldValue) == fmt.Sprintf("%v", v) { + return false + } + } + } + return true + case "exists": + if existsVal, ok := cond.Value.(bool); ok { + return existsVal + } + return true + case "regex": + if pattern, ok := cond.Value.(string); ok { + matched, _ := regexp.MatchString(pattern, fmt.Sprintf("%v", fieldValue)) + return matched + } + return false + default: + return true + } +} + +// applyOperations применяет операции к документу +func (tm *TriggerManager) applyOperations(doc *Document, ops []TriggerOperation, execCtx *TriggerExecution) *Document { + if doc == nil { + return nil + } + + result := doc.Clone() + + for _, op := range ops { + switch op.Type { + case "set": + value := tm.resolveValue(op.Value, execCtx) + result.SetField(op.Field, value) + + case "unset": + result.DeleteField(op.Field) + + case "inc": + if incVal, ok := toFloat64(op.Value); ok { + if current, err := result.GetField(op.Field); err == nil { + if currVal, ok := toFloat64(current); ok { + result.SetField(op.Field, currVal+incVal) + } + } else { + result.SetField(op.Field, incVal) + } + } + + case "mul": + if mulVal, ok := toFloat64(op.Value); ok { + if current, err := result.GetField(op.Field); err == nil { + if currVal, ok := toFloat64(current); ok { + result.SetField(op.Field, currVal*mulVal) + } + } + } + + case "rename": + if newName, ok := op.Value.(string); ok { + if val, err := result.GetField(op.Field); err == nil { + result.SetField(newName, val) + result.DeleteField(op.Field) + } + } + + case "currentDate": + result.SetField(op.Field, time.Now().UnixMilli()) + } + } + + return result +} + +// resolveValue разрешает специальные значения типа $$NOW, $$USER +func (tm *TriggerManager) resolveValue(value interface{}, execCtx *TriggerExecution) interface{} { + if strVal, ok := value.(string); ok { + switch strVal { + case "$$NOW": + return time.Now().UnixMilli() + case "$$USER": + if execCtx.User != "" { + return execCtx.User + } + return "anonymous" + case "$$ROLE": + if execCtx.Role != "" { + return execCtx.Role + } + return "anonymous" + } + } + return value +} + +// logExecution логирует выполнение триггера +// +// ИЗМЕНЕНО (2026-10-03): формат временной метки изменён на "02-01-2006 15:04:05.000". +func (tm *TriggerManager) logExecution(execCtx *TriggerExecution, trigger *Trigger, result string, startTime time.Time) { + tm.mu.Lock() + defer tm.mu.Unlock() + + duration := time.Since(startTime) + now := time.Now() + nowMs := now.UnixMilli() + nowStr := now.Format("02-01-2006 15:04:05.000") + + execCtx.TriggerName = trigger.Name + execCtx.Timestamp = now + execCtx.TimestampMs = nowMs + execCtx.TimestampStr = nowStr + execCtx.ActionResult = result + execCtx.DurationMs = duration.Milliseconds() + + if len(tm.auditLog) >= tm.maxLogSize { + tm.auditLog = tm.auditLog[1:] + } + tm.auditLog = append(tm.auditLog, execCtx) + + // Аудит выполнения триггера + LogAudit("TRIGGER_EXECUTE", "TRIGGER", fmt.Sprintf("%s.%s", trigger.Collection, trigger.Name), map[string]interface{}{ + "event": execCtx.Event, + "result": result, + "duration_ms": duration.Milliseconds(), + "document_id": execCtx.DocumentID, + }) +} + +// GetTriggerExecutionLog возвращает лог выполнения триггеров +func (tm *TriggerManager) GetTriggerExecutionLog() []*TriggerExecution { + tm.mu.RLock() + defer tm.mu.RUnlock() + + result := make([]*TriggerExecution, len(tm.auditLog)) + copy(result, tm.auditLog) + return result +} + +// GetTriggerExecutionLogFiltered возвращает отфильтрованный лог выполнения триггеров +func (tm *TriggerManager) GetTriggerExecutionLogFiltered(triggerName, collection string, fromTime, toTime int64) []*TriggerExecution { + tm.mu.RLock() + defer tm.mu.RUnlock() + + result := make([]*TriggerExecution, 0) + for _, entry := range tm.auditLog { + if triggerName != "" && entry.TriggerName != triggerName { + continue + } + if collection != "" && entry.Collection != collection { + continue + } + if fromTime > 0 && entry.TimestampMs < fromTime { + continue + } + if toTime > 0 && entry.TimestampMs > toTime { + continue + } + result = append(result, entry) + } + return result +} + +// ClearTriggerLog очищает лог выполнения триггеров +func (tm *TriggerManager) ClearTriggerLog() { + tm.mu.Lock() + defer tm.mu.Unlock() + tm.auditLog = make([]*TriggerExecution, 0) + LogAudit("CLEAR", "TRIGGER_LOG", "all", nil) +} + +// GetTriggerExecutionStats возвращает статистику выполнения триггеров +func (tm *TriggerManager) GetTriggerExecutionStats() map[string]interface{} { + tm.mu.RLock() + defer tm.mu.RUnlock() + + triggerStats := make(map[string]map[string]interface{}) + for _, entry := range tm.auditLog { + if _, ok := triggerStats[entry.TriggerName]; !ok { + triggerStats[entry.TriggerName] = map[string]interface{}{ + "total_executions": 0, + "aborted": 0, + "skipped": 0, + "modified": 0, + "logged": 0, + "notified": 0, + "total_duration_ms": int64(0), + "avg_duration_ms": float64(0), + } + } + + stats := triggerStats[entry.TriggerName] + stats["total_executions"] = stats["total_executions"].(int) + 1 + stats["total_duration_ms"] = stats["total_duration_ms"].(int64) + entry.DurationMs + + switch entry.ActionResult { + case "aborted": + stats["aborted"] = stats["aborted"].(int) + 1 + case "skipped": + stats["skipped"] = stats["skipped"].(int) + 1 + case "modified": + stats["modified"] = stats["modified"].(int) + 1 + case "logged": + stats["logged"] = stats["logged"].(int) + 1 + case "notified": + stats["notified"] = stats["notified"].(int) + 1 + } + } + + // Вычисляем среднюю длительность + for _, stats := range triggerStats { + if totalExec, ok := stats["total_executions"].(int); ok && totalExec > 0 { + if totalDuration, ok := stats["total_duration_ms"].(int64); ok { + stats["avg_duration_ms"] = float64(totalDuration) / float64(totalExec) + } + } + } + + return map[string]interface{}{ + "total_trigger_executions": len(tm.auditLog), + "triggers_stats": triggerStats, + "log_size": len(tm.auditLog), + "max_log_size": tm.maxLogSize, + } +} + +// getTriggerKey возвращает ключ для хранения триггера +func (tm *TriggerManager) getTriggerKey(collection string, event TriggerEvent, name string) string { + return fmt.Sprintf("%s|%s|%s", collection, event, name) +} + +// compareNumbers сравнивает два числа +func compareNumbers(a, b interface{}) int { + aVal, aOk := toFloat64(a) + bVal, bOk := toFloat64(b) + + if aOk && bOk { + if aVal < bVal { + return -1 + } + if aVal > bVal { + return 1 + } + return 0 + } + return 0 +} + +// ============================================================================= +// TRIGGER CONFIG BUILDER +// ============================================================================= + +// MongoDBLikeTriggerConfig создаёт конфигурацию триггера в стиле MongoDB +func MongoDBLikeTriggerConfig() *TriggerConfigBuilder { + return &TriggerConfigBuilder{ + config: make(map[string]interface{}), + ops: make([]interface{}, 0), + } +} + +// TriggerConfigBuilder строитель конфигурации триггера +type TriggerConfigBuilder struct { + config map[string]interface{} + ops []interface{} +} + +// On устанавливает событие триггера +func (b *TriggerConfigBuilder) On(event string) *TriggerConfigBuilder { + b.config["event"] = event + return b +} + +// Condition добавляет условие +func (b *TriggerConfigBuilder) Condition(field, operator string, value interface{}) *TriggerConfigBuilder { + b.config["condition"] = map[string]interface{}{ + "field": field, + "operator": operator, + "value": value, + } + return b +} + +// ConditionRegex добавляет regex условие +func (b *TriggerConfigBuilder) ConditionRegex(field, pattern string) *TriggerConfigBuilder { + b.config["condition"] = map[string]interface{}{ + "field": field, + "operator": "regex", + "value": pattern, + } + return b +} + +// Set добавляет операцию установки поля +func (b *TriggerConfigBuilder) Set(field string, value interface{}) *TriggerConfigBuilder { + b.ops = append(b.ops, map[string]interface{}{ + "type": "set", + "field": field, + "value": value, + }) + return b +} + +// Unset добавляет операцию удаления поля +func (b *TriggerConfigBuilder) Unset(field string) *TriggerConfigBuilder { + b.ops = append(b.ops, map[string]interface{}{ + "type": "unset", + "field": field, + }) + return b +} + +// Inc добавляет операцию инкремента +func (b *TriggerConfigBuilder) Inc(field string, value float64) *TriggerConfigBuilder { + b.ops = append(b.ops, map[string]interface{}{ + "type": "inc", + "field": field, + "value": value, + }) + return b +} + +// Mul добавляет операцию умножения +func (b *TriggerConfigBuilder) Mul(field string, value float64) *TriggerConfigBuilder { + b.ops = append(b.ops, map[string]interface{}{ + "type": "mul", + "field": field, + "value": value, + }) + return b +} + +// Rename добавляет операцию переименования поля +func (b *TriggerConfigBuilder) Rename(oldName, newName string) *TriggerConfigBuilder { + b.ops = append(b.ops, map[string]interface{}{ + "type": "rename", + "field": oldName, + "value": newName, + }) + return b +} + +// CurrentDate добавляет операцию установки текущей даты +func (b *TriggerConfigBuilder) CurrentDate(field string) *TriggerConfigBuilder { + b.ops = append(b.ops, map[string]interface{}{ + "type": "currentDate", + "field": field, + }) + return b +} + +// Action устанавливает действие триггера +func (b *TriggerConfigBuilder) Action(action string) *TriggerConfigBuilder { + b.config["action"] = action + return b +} + +// Description устанавливает описание триггера +func (b *TriggerConfigBuilder) Description(desc string) *TriggerConfigBuilder { + b.config["description"] = desc + return b +} + +// Build собирает конфигурацию +func (b *TriggerConfigBuilder) Build() map[string]interface{} { + b.config["operations"] = b.ops + return b.config +}