Система хуков в Dexie.js представляет собой механизм перехвата операций над таблицами до и после их выполнения. Каждый хук привязан к конкретному жизненному циклу записи: созданию, чтению, обновлению и удалению. На уровне архитектуры это позволяет внедрять слой наблюдаемости без изменения бизнес-логики запросов и без дублирования кода в каждом месте доступа к данным.
Хуки работают на уровне таблицы и активируются в контексте транзакции, что делает их частью атомарной операции. Это критически важно для аудита: любые изменения фиксируются синхронно с фактической модификацией данных, исключая расхождения между логом и состоянием базы.
Каждая таблица поддерживает несколько категорий хуков:
creating — вызывается перед добавлением записиreading — вызывается при чтении данныхupdating — вызывается перед изменением записиdeleting — вызывается перед удалением записиКаждый из них может модифицировать входные данные или полностью отменить операцию, выбросив исключение.
Пример базовой регистрации:
db.users.hook('creating', function (primKey, obj, transaction) {
obj.createdAt = new Date();
});
В этом сценарии данные обогащаются метаданными до фактической записи.
Аудит в контексте IndexedDB и Dexie.js строится на перехвате всех мутаций и сохранении их в отдельной журналируемой таблице. Такой подход обеспечивает неизменяемую историю событий.
Базовая модель аудита обычно включает:
Логирование удобно централизовать через обертку над хуками таблиц. Пример структуры audit-таблицы:
db.version(1).stores({
users: '++id, name, email',
auditLogs: '++id, table, action, key, timestamp'
});
Далее подключается универсальный механизм:
function registerAudit(tableName) {
const table = db.table(tableName);
table.hook('creating', function (primKey, obj, transaction) {
transaction._audit = transaction._audit || [];
transaction._audit.push({
table: tableName,
action: 'create',
key: primKey,
before: null,
after: obj,
timestamp: Date.now()
});
});
table.hook('updating', function (mods, primKey, obj, transaction) {
transaction._audit = transaction._audit || [];
transaction._audit.push({
table: tableName,
action: 'update',
key: primKey,
before: obj,
after: { ...obj, ...mods },
timestamp: Date.now()
});
});
table.hook('deleting', function (primKey, obj, transaction) {
transaction._audit = transaction._audit || [];
transaction._audit.push({
table: tableName,
action: 'delete',
key: primKey,
before: obj,
after: null,
timestamp: Date.now()
});
});
}
Ключевой момент заключается в том, что сами хуки не должны выполнять отдельные записи в IndexedDB вне текущей транзакции, иначе возникает риск несогласованности. Вместо этого используется финализация после завершения операции.
db.on('transaction', function (tx) {
tx.on('complete', function () {
if (!tx._audit) return;
db.auditLogs.bulkAdd(tx._audit);
});
});
Такой подход гарантирует, что лог сохраняется только в случае успешного завершения транзакции.
Полный снимок объекта часто избыточен, особенно при частых обновлениях. Более эффективная стратегия — хранение различий между состояниями.
function diff(oldObj, newObj) {
const changes = {};
for (const key in newObj) {
if (oldObj[key] !== newObj[key]) {
changes[key] = {
from: oldObj[key],
to: newObj[key]
};
}
}
return changes;
}
Использование diff внутри updating хука:
table.hook('updating', function (mods, primKey, obj) {
const changes = diff(obj, { ...obj, ...mods });
transaction._audit.push({
table: table.name,
action: 'update',
key: primKey,
changes
});
});
Хуки могут использоваться не только для логирования, но и для валидации. В контексте аудита это важно, поскольку предотвращает запись неконсистентных данных до фиксации события.
db.users.hook('creating', function (primKey, obj) {
if (!obj.email || !obj.email.includes('@')) {
throw new Error('Invalid email format');
}
});
Любая ошибка в хуке отменяет операцию, что автоматически предотвращает попадание некорректных данных в журнал изменений.
При внедрении аудита возникает риск рекурсии: запись логов сама вызывает хуки. Это приводит к бесконечным циклам.
Решение заключается в разделении контекста:
table.hook('creating', function (primKey, obj, tx) {
if (tx.name === 'readonly') return;
});
Или через явный флаг:
transaction._isAuditWrite = true;
И проверка:
if (transaction._isAuditWrite) return;
Хуки в Dexie.js выполняются синхронно в рамках транзакции. Это
означает, что нельзя использовать await внутри
обработчиков. Любая попытка асинхронного вызова нарушает модель
выполнения.
Для обхода используется буферизация данных:
Для полноценного логирования часто требуется привязка к пользователю или сессии. Это реализуется через внешний контекст:
const auditContext = {
userId: null,
sessionId: null
};
Далее контекст прокидывается в транзакции:
db.users.hook('updating', function (mods, primKey, obj, tx) {
tx._auditContext = auditContext;
});
И сохраняется вместе с логом:
{
userId: tx._auditContext.userId,
sessionId: tx._auditContext.sessionId
}
Хотя чтение не изменяет состояние базы, оно может быть важным для
аналитики и безопасности. Хук reading позволяет фиксировать
доступ к данным.
db.users.hook('reading', function (obj) {
return {
...obj,
_accessedAt: Date.now()
};
});
В аудиторных системах это используется для построения профиля активности, обнаружения аномалий доступа и контроля чувствительных данных.
При росте объема данных основная проблема аудита — нагрузка на запись. Для оптимизации применяются следующие подходы:
bulkAddПример батча:
let buffer = [];
function flush() {
if (!buffer.length) return;
db.auditLogs.bulkAdd(buffer);
buffer = [];
}
Хуки становятся наиболее мощными при совместном использовании с транзакциями. Они позволяют строить связные цепочки изменений, где каждое действие логируется как часть единого атомарного процесса.
db.transaction('rw', db.users, db.auditLogs, async () => {
await db.users.update(1, { name: 'new' });
});
Все хуки внутри этой транзакции работают в едином контексте, обеспечивая консистентность аудита.
При зрелой архитектуре логирование перестает быть вспомогательной функцией и становится отдельным слоем инфраструктуры. Хуки выполняют роль триггеров событий, а audit-таблица превращается в источник правды о состоянии системы.
Такой подход позволяет:
Структура системы становится реактивной: любое изменение данных автоматически порождает событие, фиксируемое без участия прикладного кода.