Слой абстракции над схемой

Dexie.js строится вокруг явного описания структуры IndexedDB через схему базы данных, где каждая таблица и её индексы фиксируются на этапе инициализации. Однако в реальных приложениях прямое взаимодействие со схемой на уровне version().stores() часто становится узким местом: схема начинает дублироваться в коде, миграции разрастаются, а доменная модель смешивается с деталями хранения данных. Слой абстракции над схемой решает эту проблему, отделяя описание структуры данных от логики приложения и предоставляя единый центр управления изменениями.


Базовая схема Dexie и её ограничения

В стандартной модели Dexie схема описывается через версии:

const db = new Dexie("AppDatabase");

db.version(1).stores({
  users: "++id,name,email",
  orders: "++id,userId,createdAt"
});

Каждая версия фиксирует состояние таблиц на момент миграции. Любое изменение структуры требует новой версии:

db.version(2).stores({
  users: "++id,name,email,role",
  orders: "++id,userId,createdAt,status"
});

Такая модель работает корректно, но имеет ряд ограничений:

  • отсутствует централизованное описание схемы;
  • индексы и поля размазаны по версиям;
  • сложно переиспользовать структуру между окружениями;
  • миграции становятся декларативным “снимком”, а не управляемым процессом;
  • доменная модель дублируется в типах, интерфейсах и запросах.

Концепция слоя абстракции

Слой абстракции над схемой вводит промежуточное представление структуры базы данных, независимое от версий Dexie. В этом подходе схема существует как единый объект, а версии лишь интерпретируют изменения этого объекта.

Ключевая идея заключается в разделении:

  • логическая схема — описание таблиц и полей в терминах домена;
  • физическая схема Dexie — строковое представление для IndexedDB;
  • слой трансляции — механизм преобразования логической схемы в версии Dexie.

Логическая модель схемы

Логическая схема описывает данные в структурированном виде:

const schemaModel = {
  users: {
    key: "id",
    autoIncrement: true,
    fields: {
      name: { index: true },
      email: { index: true, unique: true },
      role: { index: false }
    }
  },
  orders: {
    key: "id",
    autoIncrement: true,
    fields: {
      userId: { index: true },
      createdAt: { index: true },
      status: { index: true }
    }
  }
};

Такое представление не зависит от Dexie и не использует строковую нотацию индексов. Оно становится источником истины для всей структуры данных.


Трансляция логической схемы в Dexie stores

Dexie требует строкового описания индексов. Поэтому слой абстракции включает функцию компиляции:

function compileStoreSchema(model) {
  const stores = {};

  for (const [tableName, definition] of Object.entries(model)) {
    const indexes = [];

    if (definition.key && definition.autoIncrement) {
      indexes.push("++" + definition.key);
    } else if (definition.key) {
      indexes.push(definition.key);
    }

    for (const [field, config] of Object.entries(definition.fields)) {
      if (config.index && field !== definition.key) {
        indexes.push(field);
      }
    }

    stores[tableName] = indexes.join(",");
  }

  return stores;
}

Результатом становится объект, пригодный для Dexie:

const stores = compileStoreSchema(schemaModel);

db.version(1).stores(stores);

Версионирование через декларативное сравнение схем

Одним из ключевых элементов абстракции является вычисление различий между версиями схемы. Вместо ручного описания каждой миграции формируется дифф между двумя состояниями модели.

Сравнение может строиться на уровне:

  • добавленных таблиц;
  • удалённых таблиц;
  • добавленных индексов;
  • изменений ключей;
  • изменения autoIncrement.

Пример функции сравнения:

function diffSchemas(prev, next) {
  const changes = {
    addedTables: [],
    removedTables: [],
    modifiedTables: []
  };

  for (const table of Object.keys(next)) {
    if (!prev[table]) {
      changes.addedTables.push(table);
    }
  }

  for (const table of Object.keys(prev)) {
    if (!next[table]) {
      changes.removedTables.push(table);
    }
  }

  for (const table of Object.keys(next)) {
    if (prev[table] && JSON.stringify(prev[table]) !== JSON.stringify(next[table])) {
      changes.modifiedTables.push(table);
    }
  }

  return changes;
}

На основе этого диффа формируется набор версий Dexie, каждая из которых отражает конкретное изменение схемы.


Построение цепочки версий Dexie

Dexie требует последовательного описания всех версий. Абстрактный слой может генерировать их автоматически:

function buildDexieVersions(db, schemaHistory) {
  schemaHistory.forEach((schema, index) => {
    const versionNumber = index + 1;
    const stores = compileStoreSchema(schema);

    db.version(versionNumber).stores(stores);
  });
}

История схемы становится массивом состояний:

const schemaHistory = [
  schemaV1,
  schemaV2,
  schemaV3
];

Такой подход исключает ручное управление версиями и уменьшает риск рассинхронизации структуры.


Отделение доменной модели от хранения

Абстракция схемы позволяет связать таблицы Dexie с доменными сущностями без привязки к строковой нотации индексов.

Пример доменной модели:

class User {
  constructor({ id, name, email, role }) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.role = role;
  }
}

Маппинг слоя хранения:

function toUserModel(row) {
  return new User(row);
}

function toUserRow(user) {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role
  };
}

Таким образом схема базы данных перестаёт проникать в бизнес-логику.


Унификация описания индексов

Встроенная строковая система Dexie ("++id,name,email") компактна, но плохо масштабируется. Абстракция позволяет выразить индексы декларативно и расширяемо:

  • уникальные индексы;
  • составные индексы;
  • скрытые технические поля;
  • условные индексы для миграций.

Пример расширенного описания:

fields: {
  email: { index: true, unique: true },
  fullName: { index: true, compound: ["firstName", "lastName"] }
}

Компилятор схемы преобразует это в корректную строку Dexie, скрывая сложность преобразования.


Управление изменениями схемы как поток событий

Абстракция схемы может рассматривать изменения как последовательность событий:

  • добавление таблицы;
  • добавление поля;
  • изменение индекса;
  • удаление индекса.

Пример представления:

const schemaEvents = [
  { type: "ADD_TABLE", table: "users" },
  { type: "ADD_FIELD", table: "users", field: "email", index: true },
  { type: "ADD_INDEX", table: "orders", field: "createdAt" }
];

Эти события затем применяются к базовой модели, формируя новую версию схемы. Такой подход делает структуру базы воспроизводимой и детерминированной.


Интеграция с миграционной логикой Dexie

Dexie поддерживает миграции через метод upgrade(), который может использоваться совместно с абстрактным слоем:

db.version(2).stores(compileStoreSchema(schemaV2)).upgrade(tx => {
  return tx.table("users").toCollection().modify(user => {
    if (!user.role) {
      user.role = "user";
    }
  });
});

Абстракция позволяет генерировать такие миграции автоматически на основе различий схемы, связывая структурные изменения с трансформацией данных.


Переиспользование схемы между окружениями

Логическая схема становится переносимым артефактом. Она может использоваться:

  • в тестовой среде;
  • в production;
  • в браузерных и Node.js окружениях с fake IndexedDB;
  • в моках и симуляторах базы.

Dexie-слой при этом остаётся тонкой адаптацией, а не источником истины.


Композиция схем

Схема может быть разделена на модули и собрана в единую структуру:

const userSchema = { ... };
const orderSchema = { ... };

const schemaModel = {
  ...userSchema,
  ...orderSchema
};

Это позволяет строить масштабируемые базы данных, где каждый модуль отвечает за свою часть структуры, не вмешиваясь в глобальное описание.


Изоляция технических деталей Dexie

Абстракция устраняет прямую зависимость от строковой DSL Dexie. В коде приложения больше не присутствуют конструкции вида:

"++id,name,email"

Вместо этого используется структурированное описание, которое:

  • легче валидировать;
  • проще тестировать;
  • удобнее версионировать;
  • проще генерировать автоматически.

Dexie остаётся только движком выполнения, а не источником описания структуры данных.