## @e22m4u/js-repository

![npm version](https://badge.fury.io/js/@e22m4u%2Fjs-repository.svg)
![license](https://img.shields.io/badge/license-mit-blue.svg)

ORM/ODM библиотека для Node.js, реализующая слой управления данными на основе
паттерна «Репозиторий». Унифицирует интерфейс различных баз данных, фильтрацию
и работу со связанными документами независимо от источника.

## Содержание

- [Установка](#установка)
- [Импорт](#импорт)
- [Описание](#описание)
  - [Терминология](#терминология)
- [Базовый пример](#пример)
- [Схема баз данных](#схема-баз-данных)
- [Источник данных](#источник-данных)
- [Модель](#модель)
  - [Свойства](#свойства)
- [Репозиторий](#репозиторий)
  - [create](#repositorycreate)
  - [replaceById](#repositoryreplacebyid)
  - [replaceOrCreate](#repositoryreplaceorcreate)
  - [patchById](#repositorypatchbyid)
  - [patch](#repositorypatch)
  - [find](#repositoryfind)
  - [findOne](#repositoryfindone)
  - [findById](#repositoryfindbyid)
  - [delete](#repositorydelete)
  - [deleteById](#repositorydeletebyid)
  - [exists](#repositoryexists)
  - [count](#repositorycount)
- [Фильтрация](#фильтрация)
- [Связи](#связи)
  - [Belongs To](#belongs-to)
  - [Has One](#has-one)
  - [Has Many](#has-many)
  - [References Many](#references-many)
  - [Belongs To (полиморфная)](#belongs-to-полиморфная-версия)
  - [Has One (полиморфная)](#has-one-полиморфная-версия)
  - [Has Many (полиморфная)](#has-many-полиморфная-версия)
- [Расширение](#расширение)
- [TypeScript](#typescript)
- [Тесты](#тесты)
- [Лицензия](#лицензия)

## Установка

```bash
npm install @e22m4u/js-repository
```

Опционально устанавливается нужный адаптер.

| адаптер   | описание                                          |                                                                                    |
|-----------|---------------------------------------------------|------------------------------------------------------------------------------------|
| `memory`  | Виртуальная база данных в памяти процесса Node.js | *встроенный*                                                                       |
| `mongodb` | MongoDB - документо-ориентированная база данных   | [*установка*](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter) |

**Утилиты**

- [@e22m4u/js-repository-json-schema](https://www.npmjs.com/package/@e22m4u/js-repository-json-schema)  
  *Генератор JSON Schema для моделей репозитория*

## Импорт

Модуль поддерживает ESM и CommonJS стандарты.

*ESM*

```js
import {DatabaseSchema} from '@e22m4u/js-repository';
```

*CommonJS*

```js
const {DatabaseSchema} = require('@e22m4u/js-repository');
```

## Описание

Модуль позволяет абстрагироваться от различных интерфейсов баз данных,
представляя их как именованные *источники данных*, подключаемые к *моделям*.
*Модель* же описывает таблицу базы, колонки которой являются свойствами
модели. Свойства модели могут иметь определенный *тип* допустимого значения.
Кроме того, *модель* может определять классические связи «один к одному»,
«один ко многим» и другие типы отношений между моделями.

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

- *Источник данных* - определяет способ подключения к базе;
- *Модель* - описывает структуру документа и связи к другим моделям;
- *Репозиторий* - выполняет операции чтения и записи документов модели;

```mermaid
flowchart TD

  A[DatabaseSchema]
  subgraph Базы данных
    B[Источник данных 1]
    C[Источник данных 2]
  end
  A-->B
  A-->C

  subgraph Коллекции
    D[Модель A]
    E[Модель B]
    F[Модель C]
    G[Модель D]
  end
  B-->D
  B-->E
  C-->F
  C-->G

  H[Репозиторий A]
  I[Репозиторий B]
  J[Репозиторий C]
  K[Репозиторий D]
  D-->H
  E-->I
  F-->J
  G-->K
```

### Терминология

Описание ключевых понятий.

- **Схема баз данных** (*DatabaseSchema*) - хранит определения источников
данных и моделей. Связывает модели с источниками данных и предоставляет
доступ к экземплярам репозитория.

- **Источник данных** (*Datasource*) - именованная конфигурация подключения
к базе данных. Содержит название используемого адаптера и параметры соединения.

- **Адаптер** (*Adapter*) - низкоуровневая реализация для взаимодействия с
конкретной СУБД. Обеспечивает совместимость хранилища с унифицированным
интерфейсом репозитория.

- **Коллекция** (*Collection*) - физическая область хранения записей или
документов в базе данных, ассоциированная с конкретной моделью. В контексте
реляционных баз данных является эквивалентом таблицы.

- **Модель** (*Model*) - структурное описание коллекции базы данных. Содержит
определения свойств документа и отношений с другими моделями.

- **Свойство** (*Property*) - описание отдельного поля модели. Определяет тип
данных, значение по умолчанию, обязательность наличия и правила проверки
на уникальность.

- **Связь** (*Relation*) - абстракция отношений между моделями. Поддерживает
классические отношения и полиморфный режим динамического определения
целевой модели через поле дискриминатора.

- **Дискриминатор** (*Discriminator*) - свойство документа в рамках полиморфной
связи. Хранит строковое название целевой модели, что позволяет динамически
определять связанную коллекцию при выполнении запросов.

- **Репозиторий** (*Repository*) - абстрактный фасад для работы с данными
конкретной модели. Изолирует логику приложения от деталей реализации
базы данных. Выполняет операции записи, поиска, обновления и удаления.

## Пример

Пример демонстрирует создание экземпляра `DatabaseSchema`, объявление источника
данных `myDb` и модели `country`. Далее в коллекцию добавляется новый документ,
содержимого которого выводится в консоль.

```
   Страна (country)
┌─────────────────────────┐
│  id: 1                  │
│  name: "Russia"         │
│  population: 143400000  │
└─────────────────────────┘
```

```js
import {DataType} from '@e22m4u/js-repository';
import {DatabaseSchema} from '@e22m4u/js-repository';

// создание экземпляра DatabaseSchema
const dbs = new DatabaseSchema();

// объявление источника "myDb"
dbs.defineDatasource({
  name: 'myDb',      // название нового источника
  adapter: 'memory', // выбранный адаптер
});

// объявление модели "country"
dbs.defineModel({
  name: 'country',    // название новой модели
  datasource: 'myDb', // выбранный источник
  properties: {       // свойства модели
    name: DataType.STRING,       // тип "string"
    population: DataType.NUMBER, // тип "number"
  },
})

// получение репозитория модели
const countryRep = dbs.getRepository('country');

// добавление нового документа в коллекцию
const country = await countryRep.create({
  name: 'Russia',
  population: 143400000,
});

// вывод нового документа
console.log(country);
// {
//   id: 1,
//   name: 'Russia',
//   population: 143400000,
// }
```

В следующем блоке определяется модель `city` со связью `belongsTo` к модели
`country` из примера выше. Затем создается новый документ города, связанный
с ранее созданной страной. После создания нового документа, выполняется запрос
на извлечение данного города с включением связанной страны.

```
   Страна (country)                  Город (city)
┌─────────────────────────┐       ┌─────────────────────────┐
│  id: 1  <───────────────│───┐   │  id: 1                  │
│  name: "Russia"         │   │   │  name: "Moscow"         │
│  population: 143400000  │   └───│─ countryId: 1           │
└─────────────────────────┘       └─────────────────────────┘
```

```js
// объявление модели "city" со связью к "country"
dbs.defineModel({
  name: 'city',
  datasource: 'myDb',
  properties: {
    name: DataType.STRING,
    countryId: DataType.NUMBER,
    // внешний ключ "countryId" указывать не обязательно,
    // но для проверки типа значения перед записью в базу
    // рекомендуется, так как адаптер "memory" по умолчанию
    // создает числовые идентификаторы
  },
  relations: {
    // определение связи "country" позволит автоматически включать
    // связанные документы с помощью опции "include" при запросах
    // из данной коллекции через методы репозитория
    country: {
      type: RelationType.BELONGS_TO, // тип связи: принадлежит к...
      model: 'country',              // название целевой модели
      foreignKey: 'countryId',       // поле с внешним ключом (не обязательно)
      // если внешний ключ соответствует `relationName` + `Id`,
      // то указывать опцию `foreignKey` не обязательно
    },
  },
});

// получение репозитория для модели "city"
const cityRep = dbs.getRepository('city');

// создание нового города и его привязка к стране через country.id
const city = await cityRep.create({
  name: 'Moscow',
  countryId: country.id, // использование id созданной ранее страны
});

console.log(city);
// {
//   id: 1,
//   name: 'Moscow',
//   countryId: 1,
// }

// извлечение города по идентификатору с включением связанной страны
const cityWithCountry = await cityRep.findById(city.id, {
  include: 'country',
});

console.log(cityWithCountry);
// {
//   id: 1,
//   name: 'Moscow',
//   countryId: 1,
//   country: {
//     id: 1,
//     name: 'Russia',
//     population: 143400000
//   }
// }
```

## Схема баз данных

Экземпляр класса `DatabaseSchema` (далее *«Схема»*) хранит определения
источников данных и моделей. Схема связывает модели с источниками данных
и используется для получения репозиториев, которые выполняют операции
чтения и записи.

**Методы**

- `defineDatasource(datasourceDef: object): this` - добавить источник;
- `defineModel(modelDef: object): this` - добавить модель;
- `getRepository(modelName: string): Repository` - получить репозиторий;

**Примеры**

Импорт класса и создание экземпляра схемы.

```js
import {DatabaseSchema} from '@e22m4u/js-repository';

const dbs = new DatabaseSchema();
```

Определение нового источника.

```js
dbs.defineDatasource({
  name: 'myDb', // название нового источника
  adapter: 'memory', // выбранный адаптер
});
```

Определение новой модели.

```js
dbs.defineModel({
  name: 'product', // название новой модели
  datasource: 'myDb', // выбранный источник
  properties: { // свойства модели
    name: DataType.STRING,
    weight: DataType.NUMBER,
  },
});
```

Получение репозитория по названию модели.

```js
const productRep = dbs.getRepository('product');
```

## Источник данных

Источник хранит название выбранного адаптера и его настройки. Определение
нового источника выполняется методом `defineDatasource` экземпляра
`DatabaseSchema`.

**Параметры**

- `name: string` уникальное название;
- `adapter: string` выбранный адаптер;
- параметры адаптера (если имеются);

**Примеры**

Определение нового источника.

```js
dbs.defineDatasource({
  name: 'myDb', // название нового источника
  adapter: 'memory', // выбранный адаптер
});
```

Передача дополнительных параметров на примере MongoDB адаптера *([установка](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter))*.

```js
dbs.defineDatasource({
  name: 'myDb',
  adapter: 'mongodb',
  // параметры адаптера "mongodb"
  host: '127.0.0.1',
  port: 27017,
  database: 'myDatabase',
});
```

## Модель

Описывает структуру документа коллекции и связи к другим моделям. Определение
новой модели выполняется методом `defineModel` экземпляра `DatabaseSchema`.

**Параметры**

- `name: string` название модели (обязательно);
- `base: string` название наследуемой модели;
- `tableName: string` название коллекции в базе;
- `datasource: string` выбранный источник данных;
- `properties: object` определения свойств (см. [Свойства](#свойства));
- `relations: object` определения связей (см. [Связи](#связи));

**Примеры**

Определение модели со свойствами указанного типа.

```js
dbs.defineModel({
  name: 'user', // название новой модели
  properties: { // свойства модели
    name: DataType.STRING,
    age: DataType.NUMBER,
  },
});
```

### Свойства

Параметр `properties` находится в определении модели и принимает объект, ключи
которого являются свойствами этой модели, а значением тип свойства или объект
с дополнительными параметрами. Параметры свойства используется репозиторием
для проверки документов перед сохранением.

```js
dbs.defineModel({
  name: 'user',
  properties: {
    // краткая форма (только тип)
    name: DataType.STRING,
    // расширенное определение (см. далее)
    email: {
      type: DataType.STRING,
      unique: PropertyUniqueness.SPARSE,
      default: '',
    },
  },
});
```

*i. В контексте реляционных баз данных свойства отражают колонки таблицы.*

**Типы данных**

- `DataType.ANY` разрешено любое значение;
- `DataType.STRING` только значение типа `string`;
- `DataType.NUMBER` только значение типа `number`;
- `DataType.BOOLEAN` только значение типа `boolean`;
- `DataType.ARRAY` только значение типа `array`;
- `DataType.OBJECT` только значение типа `object`;

**Параметры свойства**

Объект с параметрами (расширенное определение) позволяет задавать значения
по умолчанию, правила проверки на уникальность и обязательность наличия данных.
Для массивов и вложенных объектов предусмотрено указание типа элемента
и название модели.

- `type: string` тип допустимого значения (обязательно);
- `itemType?: string` тип элемента массива (для `type: 'array'`);
- `itemModel?: string` модель элемента массива (для `type: 'array'`);
- `model?: string` модель объекта (для `type: 'object'`);
- `primaryKey?: boolean` объявить свойство первичным ключом;
- `columnName?: string` переопределение названия колонки;
- `columnType?: string` тип колонки (определяется адаптером);
- `required?: boolean` объявить свойство обязательным;
- `default?: any` значение по умолчанию (заменяет `undefined` и `null`);
- `unique?: boolean | string` проверять значение на уникальность;

**Параметр `unique`**

Если значением параметра `unique` является `true` или `"strict"`, то выполняется
строгая проверка на уникальность. В этом режиме любое значение данного свойства
не может быть представлено более одного раза.

- `unique: true | 'strict'` строгая проверка на уникальность;
- `unique: 'sparse'` исключить из проверки ложные значения;
- `unique: false | 'nonUnique'` не проверять на уникальность (по умолчанию);

Режим `"sparse"` исключает из проверки на уникальность ложные значения с точки
зрения JavaScript. Указанные ниже значения будут проигнорированы при проверке
в данном режиме.

- `""` пустая строка;
- `0` число ноль;
- `false` логическое отрицание;
- `undefined` и `null`;

В качестве значений параметра `unique` можно использовать предопределенные
константы как эквивалент строковых значений `strict`, `sparse` и `nonUnique`.

- `PropertyUniqueness.STRICT`;
- `PropertyUniqueness.SPARSE`;
- `PropertyUniqueness.NON_UNIQUE`;

**Примеры**

Краткое определение свойств модели.

```js
dbs.defineModel({
  name: 'city',
  properties: { // свойства модели
    name: DataType.STRING, // тип свойства "string"
    population: DataType.NUMBER, // тип свойства "number"
  },
});
```

Расширенное определение свойств модели.

```js
dbs.defineModel({
  name: 'city',
  properties: { // свойства модели
    name: {
      type: DataType.STRING, // тип свойства "string" (обязательно)
      required: true, // исключение значений undefined и null
    },
    population: {
      type: DataType.NUMBER, // тип свойства "number" (обязательно)
      default: 0, // значение по умолчанию
    },
    code: {
      type: DataType.NUMBER, // тип свойства "number" (обязательно)
      unique: PropertyUniqueness.STRICT, // проверять уникальность
    },
  },
});
```

Фабричное значение по умолчанию. Возвращаемое значение функции будет
определено в момент записи документа.

```js
dbs.defineModel({
  name: 'article',
  properties: { // свойства модели
    tags: {
      type: DataType.ARRAY, // тип свойства "array" (обязательно)
      itemType: DataType.STRING, // тип элемента "string"
      default: () => [], // фабричное значение
    },
    createdAt: {
      type: DataType.STRING, // тип свойства "string" (обязательно)
      default: () => new Date().toISOString(), // фабричное значение
    },
  },
});
```

## Репозиторий

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

**Методы**

- [`create(data, filter = undefined)`](#repositorycreate) создать новый документ;
- [`replaceById(id, data, filter = undefined)`](#repositoryreplacebyid) заменить документ полностью;
- [`replaceOrCreate(data, filter = undefined)`](#repositoryreplaceorcreate) заменить или создать новый;
- [`patchById(id, data, filter = undefined)`](#repositorypatchbyid) обновить документ частично;
- [`patch(data, where = undefined)`](#repositorypatch) обновить все документы или по условию;
- [`find(filter = undefined)`](#repositoryfind) найти все документы или по условию;
- [`findOne(filter = undefined)`](#repositoryfindone) найти первый документ или по условию;
- [`findById(id, filter = undefined)`](#repositoryfindbyid) найти документ по идентификатору;
- [`delete(where = undefined)`](#repositorydelete) удалить все документы или по условию;
- [`deleteById(id)`](#repositorydeletebyid) удалить документ по идентификатору;
- [`exists(id)`](#repositoryexists) проверить существование по идентификатору;
- [`count(where = undefined)`](#repositorycount) подсчет всех документов или по условию;

**Аргументы**

- `id: number|string` идентификатор (первичный ключ);
- `data: object` данные документа (используется при записи);
- `where: object` условия фильтрации (см. [Фильтрация](#фильтрация));
- `filter: object` параметры выборки (см. [Фильтрация](#фильтрация));

**Получение репозитория**

Получить репозиторий можно с помощью метода `getRepository()` экземпляра
`DatabaseSchema`. В качестве аргумента метод принимает название модели.
Обязательным условием является наличие у модели определенного [источника данных](#источник-данных) (`datasource`), так как репозиторий напрямую взаимодействует
с базой данных через указанный в источнике адаптер.

```js
// объявление источника
dbs.defineDatasource({
  name: 'myDatasource',
  adapter: 'memory', // адаптер
});

// объявление модели
dbs.defineModel({
  name: 'myModel',
  datasource: 'myDatasource',
  // properties: { ... },
  // relations: { ... }
});

// получение репозитория модели
const modelRep = dbs.getRepository('myModel');
```

При первом вызове `getRepository('myModel')` будет создан и сохранен новый
экземпляр репозитория. Все последующие вызовы с тем же названием модели будут
возвращать уже существующий экземпляр.

### repository.create

Создает новый документ в коллекции на основе переданных данных. Возвращает
созданный документ с присвоенным идентификатором.

Сигнатура:

```ts
create(
  data: WithOptionalId<FlatData, IdName>,
  filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
```

**Примеры**

Создание нового документа.

```js
const newProduct = await productRep.create({
  name: 'Laptop',
  price: 1200,
});
console.log(newProduct);
// {
//   id: 1,
//   name: 'Laptop',
//   price: 1200,
// }
```

Создание документа с возвратом определенных полей.

```js
const product = await productRep.create(
  {name: 'Mouse', price: 25},
  {fields: ['id', 'name']},
);
console.log(product);
// {
//   id: 2,
//   name: 'Mouse',
// }
```

Создание документа с включением связанных данных в результат.

```js
// предполагается, что модель Product имеет связь "category"
// (опция "include" влияет только на возвращаемый результат)
const product = await productRep.create(
  {name: 'Keyboard', price: 75, categoryId: 10},
  {include: 'category'},
);
console.log(product);
// {
//   id: 3,
//   name: 'Keyboard',
//   price: 75,
//   categoryId: 10,
//   category: {id: 10, name: 'Electronics'}
// }
```

### repository.replaceById

Полностью заменяет существующий документ по его идентификатору. Все предыдущие
данные документа, кроме идентификатора, удаляются. Поля, которые не были
переданы в `data`, будут отсутствовать в итоговом документе (если для них
не задано значение по умолчанию).

Сигнатура:

```ts
replaceById(
  id: IdType,
  data: WithoutId<FlatData, IdName>,
  filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
```

**Примеры**

Замена документа по идентификатору.

```js
// исходный документ
// {
//   id: 1,
//   name: 'Laptop',
//   price: 1200,
//   inStock: true
// }

const updatedProduct = await productRep.replaceById(1, {
  name: 'Laptop Pro',
  price: 1500,
});
console.log(updatedProduct);
// {
//   id: 1,
//   name: 'Laptop Pro',
//   price: 1500
// }
// свойство "inStock" удалено
```

### repository.replaceOrCreate

Заменяет существующий документ, если в переданных данных присутствует
идентификатор, который уже существует в коллекции. В противном случае,
если идентификатор не указан или не найден, создает новый документ.

Сигнатура:

```ts
replaceOrCreate(
  data: WithOptionalId<FlatData, IdName>,
  filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
```

**Примеры**

Создание нового документа, если `id: 3` не существует.

```js
const product = await productRep.replaceOrCreate({
  id: 3,
  name: 'Keyboard',
  price: 75,
});
console.log(product);
// {
//   id: 3,
//   name: 'Keyboard',
//   price: 75,
// }
```

Замена существующего документа с `id: 1`.

```js
const updatedProduct = await productRep.replaceOrCreate({
  id: 1,
  name: 'Laptop Pro',
  price: 1500,
});
console.log(updatedProduct);
// {
//   id: 1,
//   name: 'Laptop Pro',
//   price: 1500,
// }
```

### repository.patchById

Частично обновляет существующий документ по его идентификатору, изменяя
только переданные поля. Остальные поля документа остаются без изменений.

Сигнатура:

```ts
patchById(
  id: IdType,
  data: PartialWithoutId<FlatData, IdName>,
  filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
```

**Примеры**

Частичное обновление документа по идентификатору.

```js
// исходный документ с id: 1
// {
//   id: 1,
//   name: 'Laptop Pro',
//   price: 1500
// }

const updatedProduct = await productRep.patchById(1, {
  price: 1450,
});
console.log(updatedProduct);
// {
//   id: 1,
//   name: 'Laptop Pro',
//   price: 1450
// }
```

### repository.patch

Частично обновляет один или несколько документов, соответствующих условиям
`where`. Изменяются только переданные поля, остальные остаются без изменений.
Возвращает количество обновленных документов. Если `where` не указан,
обновляет все документы в коллекции.

Сигнатура:

```ts
patch(
  data: PartialWithoutId<FlatData, IdName>,
  where?: WhereClause<FlatData>,
): Promise<number>;
```

**Примеры**

Обновление документов по условию.

```js
// обновит все товары с ценой меньше 30
const updatedCount = await productRep.patch(
  {inStock: false},
  {price: {lt: 30}},
);
```

Обновление всех документов.

```js
// добавит или обновит поле updatedAt для всех документов
const totalCount = await productRep.patch({
  updatedAt: new Date(),
});
```

### repository.find

Находит все документы, соответствующие условиям фильтрации, и возвращает их
в виде массива. Если фильтр не указан, возвращает все документы коллекции.

Сигнатура:

```ts
find(filter?: FilterClause<FlatData>): Promise<FlatData[]>;
```

**Примеры**

Поиск всех документов.

```js
const allProducts = await productRep.find();
```

Поиск документов по условию `where`.

```js
const cheapProducts = await productRep.find({
  where: {price: {lt: 100}},
});
```

Поиск с сортировкой и ограничением выборки.

```js
const latestProducts = await productRep.find({
  order: 'createdAt DESC',
  limit: 10,
});
```

### repository.findOne

Находит первый документ, соответствующий условиям фильтрации. Возвращает
`undefined`, если документы не найдены.

Сигнатура:

```ts
findOne(
  filter?: FilterClause<FlatData>,
): Promise<FlatData | undefined>;
```

**Примеры**

Поиск одного документа по условию.

```js
const expensiveProduct = await productRep.findOne({
  where: {price: {gt: 1000}},
  order: 'price DESC',
});
```

Обработка случая, когда документ не найден.

```js
const product = await productRep.findOne({
  where: {name: 'Non-existent Product'},
});
if (!product) {
  console.log('Product not found.');
}
```

### repository.findById

Поиск документа по идентификатору. Если документ не найден, выбрасывается
ошибка.

Сигнатура:

```ts
findById(
  id: IdType,
  filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;
```

**Примеры**

Поиск документа по `id`.

```js
try {
  const product = await productRep.findById(1);
  console.log(product);
} catch (error) {
  console.error('Product with id 1 is not found.');
}
```

Поиск документа с включением связанных данных.

```js
const product = await productRep.findById(1, {
  include: 'category',
});
```

### repository.delete

Удаляет один или несколько документов, соответствующих условиям `where`.
Возвращает количество удаленных документов. Если `where` не указан, удаляет
все документы в коллекции.

Сигнатура:

```ts
delete(where?: WhereClause<FlatData>): Promise<number>;
```

**Примеры**

Удаление документов по условию.

```js
const deletedCount = await productRep.delete({
  inStock: false,
});
```

Удаление всех документов.

```js
const totalCount = await productRep.delete();
```

### repository.deleteById

Удаляет один документ по его уникальному идентификатору. Возвращает `true`,
если документ был найден и удален, в противном случае `false`.

Сигнатура:

```ts
deleteById(id: IdType): Promise<boolean>;
```

**Примеры**

Удаление документа по `id`.

```js
const wasDeleted = await productRep.deleteById(1);
if (wasDeleted) {
  console.log('The document was deleted.');
} else {
  console.log('No document found to delete.');
}
```

### repository.exists

Проверяет существование документа с указанным идентификатором. Возвращает
`true`, если документ существует, иначе `false`.

Сигнатура:

```ts
exists(id: IdType): Promise<boolean>;
```

**Примеры**

Проверка существования документа по `id`.

```js
const productExists = await productRep.exists(1);
if (productExists) {
  console.log('A document with id 1 exists.');
}
```

### repository.count

Подсчитывает количество документов, соответствующих условиям `where`. Если
`where` не указан, возвращает общее количество документов в коллекции.

Сигнатура:

```ts
count(where?: WhereClause<FlatData>): Promise<number>;
```

**Примеры**

Подсчет документов по условию.

```js
const cheapCount = await productRep.count({
  price: {lt: 100},
});
```

Подсчет всех документов.

```js
const totalCount = await productRep.count();
```

## Фильтрация

Некоторые методы репозитория принимают объект настроек, влияющий
на возвращаемый результат. Максимально широкий набор таких настроек
имеет первый параметр метода `find`, где ожидается объект содержащий
набор опций указанных ниже.

- `where: object` условия фильтрации по свойствам документа;
- `order: string|string[]` сортировка по указанным свойствам;
- `limit: number` ограничение количества документов;
- `skip: number` пропуск документов (пагинация);
- `fields: string|string[]` выбор необходимых свойств модели;
- `include: object` включение связанных данных в результат;

**Пример**

```js
// для запроса используется метод репозитория "find"
// с передачей объекта фильтрации первым аргументом
const news = await newsRepository.find({
  where: {
    title: {like: '%Moscow%'},
    publishedAt: {gte: '2025-10-15T00:00:00.000Z'},
    tags: {inq: ['world', 'politic']},
    hidden: false,
  },
  order: 'publishedAt DESC',
  limit: 12,
  skip: 24,
  fields: ['title', 'annotation', 'body'],
  include: ['author', 'category'],
})
```

### where

Параметр принимает объект с условиями выборки и поддерживает следующий набор
операторов сравнения.

- [Поиск по значению](#поиск-по-значению-сокращенная-форма)
- [`eq`](#eq-строгое-равенство) строгое равенство;
- [`neq`](#neq-неравенство) неравенство;
- [`gt`](#gt-больше-чем) больше чем;
- [`lt`](#lt-меньше-чем) меньше чем;
- [`gte`](#gte-больше-или-равно) больше или равно;
- [`lte`](#lte-меньше-или-равно) меньше или равно;
- [`inq`](#inq-в-списке) в списке;
- [`nin`](#nin-не-в-списке) не в списке;
- [`between`](#between-диапазон) диапазон;
- [`exists`](#exists-наличие-свойства) наличие свойства;
- [`like`](#like-шаблон) *SQL*-подобный шаблон;
- [`nlike`](#nlike-исключающий-шаблон) исключающий шаблон;
- [`ilike`](#ilike-регистронезависимый-шаблон) регистронезависимый шаблон;
- [`nilike`](#nilike-регистронезависимый-шаблон-исключения) регистронезависимый шаблон исключения;
- [`regexp`](#regexp-регулярное-выражение) регулярное выражение;

Условия можно объединять логическими операторами:

- [`and`](#and-логическое-и) логическое *И*;
- [`or`](#or-логическое-или) логическое *ИЛИ*;

#### Поиск по значению (сокращенная форма)

Находит документы, у которых значение указанного свойства в точности равно
переданному значению. Это сокращенная запись для оператора `{eq: ...}`.

```js
// найдет все документы, где age равен 21
const res = await rep.find({
  where: {
    age: 21,
  },
});
```

#### `eq` (строгое равенство)

Находит документы, у которых значение свойства равно указанному.

```js
// найдет все документы, где age равен 21
const res = await rep.find({
  where: {
    age: {eq: 21},
  },
});
```

#### `neq` (неравенство)

Находит документы, у которых значение свойства не равно указанному.

```js
// найдет все документы, где age не равен 21
const res = await rep.find({
  where: {
    age: {neq: 21},
  },
});
```

#### `gt` (больше чем)

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

```js
// найдет документы, где age больше 30
const res = await rep.find({
  where: {
    age: {gt: 30},
  },
});
```

#### `lt` (меньше чем)

Находит документы, у которых значение свойства строго меньше указанного.

```js
// найдет документы, где age меньше 30
const res = await rep.find({
  where: {
    age: {lt: 30},
  },
});
```

#### `gte` (больше или равно)

Находит документы, у которых значение свойства больше или равно указанному.

```js
// найдет документы, где age больше или равен 30
const res = await rep.find({
  where: {
    age: {gte: 30},
  },
});
```

#### `lte` (меньше или равно)

Находит документы, у которых значение свойства меньше или равно указанному.

```js
// найдет документы, где age меньше или равен 30
const res = await rep.find({
  where: {
    age: {lte: 30},
  },
});
```

#### `inq` (в списке)

Находит документы, у которых значение свойства совпадает с одним из значений
в предоставленном массиве.

```js
// найдет документы, где name - 'John' или 'Mary'
const res = await rep.find({
  where: {
    name: {inq: ['John', 'Mary']},
  },
});
```

#### `nin` (не в списке)

Находит документы, у которых значение свойства отсутствует в предоставленном
массиве.

```js
// найдет все документы, кроме тех, где name - 'John' или 'Mary'
const res = await rep.find({
  where: {
    name: {nin: ['John', 'Mary']},
  },
});
```

#### `between` (диапазон)

Находит документы, у которых значение свойства находится в указанном диапазоне
(включая границы).

```js
// найдет документы, где age находится в диапазоне от 20 до 30 включительно
const res = await rep.find({
  where: {
    age: {between: [20, 30]},
  },
});
```

#### `exists` (наличие свойства)

Проверяет наличие или отсутствие свойства в документе. Не проверяет значение
свойства.

- `true` свойство должно существовать (даже если его значение `null`);
- `false` свойство должно отсутствовать;

```js
// найдет документы, у которых есть свойство 'nickname'
const res1 = await rep.find({
  where: {
    nickname: {exists: true},
  },
});

// найдет документы, у которых нет свойства 'nickname'
const res2 = await rep.find({
  where: {
    nickname: {exists: false},
  },
});
```

#### `like` (шаблон)

Выполняет сопоставление с шаблоном, с учетом регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)).

```js
// найдет {name: 'John Doe'}, но не {name: 'john doe'}
const res = await rep.find({
  where: {
    name: {like: 'John%'},
  },
});
```

#### `nlike` (исключающий шаблон)

Находит документы, которые не соответствуют шаблону, с учетом регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)).

```js
// найдет все, кроме тех, что начинаются на 'John'
const res = await rep.find({
  where: {
    name: {nlike: 'John%'},
  },
});
```

#### `ilike` (регистронезависимый шаблон)

Выполняет сопоставление с шаблоном без учета регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)).

```js
// найдет {name: 'John Doe'} и {name: 'john doe'}
const res = await rep.find({
  where: {
    name: {ilike: 'john%'},
  },
});
```

#### `nilike` (регистронезависимый шаблон исключения)

Находит строки, которые не соответствуют шаблону, без учета регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)).

```js
// найдет все, кроме тех, что начинаются на 'John' или 'john'
const res = await rep.find({
  where: {
    name: {nilike: 'john%'},
  },
});
```

#### `regexp` (регулярное выражение)

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

```js
// найдет документы, где name начинается с 'J'
const res1 = await rep.find({
  where: {
    name: {regexp: '^J'},
  },
});

// найдет документы, где name начинается с 'J' или 'j' (регистронезависимо)
const res2 = await rep.find({
  where: {
    name: {regexp: '^j', flags: 'i'},
  },
});
```

#### `and` (логическое И)

Объединяет несколько условий в массив, требуя, чтобы каждое условие было
выполнено.

```javascript
// найдет документы, где surname равен 'Smith' И age равен 21
const res = await rep.find({
  where: {
    and: [
      {surname: 'Smith'},
      {age: 21}
    ],
  },
});
```

#### `or` (логическое ИЛИ)

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

```javascript
// найдет документы, где name равен 'James' ИЛИ age больше 30
const res = await rep.find({
  where: {
    or: [
      {name: 'James'},
      {age: {gt: 30}}
    ],
  },
});
```

#### Операторы сопоставления с шаблоном

Операторы `like`, `nlike`, `ilike`, `nilike` предназначены для фильтрации
строковых свойств на основе сопоставления с шаблоном, подобно оператору
`LIKE` в SQL. Они позволяют находить значения, которые соответствуют
определённой структуре, используя специальные символы.

**`%`** соответствует любой последовательности из нуля или более символов:

- `'А%'` найдет все строки, начинающиеся на "А";  
- `'%а'` найдет все строки, заканчивающиеся на "а";  
- `'%слово%'` найдет все строки, содержащие "слово" в любом месте;

**`_`** соответствует ровно одному любому символу:

- `'к_т'` найдет "кот", "кит", но не "крот" или "кт";
- `'кот_'` найдет "коты", "коту" и "кота", но не "кот" или "котов";

Если нужно найти сами символы `%` или `_` как часть строки, их необходимо
экранировать с помощью обратного слэша `\`:

- `'100\%'` найдет строку "100%";
- `'file\_name'` найдет строку "file_name";
- `'path\\to'` найдет строку "path\to";

### order

Параметр упорядочивает выборку по указанным свойствам модели. Обратное
направление порядка можно задать постфиксом `DESC` в названии свойства.

**Примеры**

Упорядочить по полю `createdAt`

```js
const res = await rep.find({
  order: 'createdAt',
});
```

Упорядочить по полю `createdAt` в обратном порядке.

```js
const res = await rep.find({
  order: 'createdAt DESC',
});
```

Упорядочить по нескольким свойствам в разных направлениях.

```js
const res = await rep.find({
  order: [
    'title',
    'price ASC',
    'featured DESC',
  ],
});
```

*i. Направление порядка `ASC` указывать необязательно.*

### include

Параметр включает связанные документы в результат вызываемого метода.
Названия включаемых связей должны быть определены в текущей модели.
(см. [Связи](#связи))

**Примеры**

Включение связи по названию.

```js
const res = await rep.find({
  include: 'city',
});
```

Включение вложенных связей.

```js
const res = await rep.find({
  include: {
    city: 'country',
  },
});
```

Включение нескольких связей массивом.

```js
const res = await rep.find({
  include: [
    'city',
    'address',
    'employees'
  ],
});
```

Использование фильтрации включаемых документов.

```js
const res = await rep.find({
  include: {
    relation: 'employees', // название связи
    scope: { // фильтрация документов "employees"
      where: {hidden: false}, // условия выборки
      order: 'id', // порядок документов
      limit: 10, // ограничение количества
      skip: 5, // пропуск документов
      fields: ['name', 'surname'], // только указанные поля
      include: 'city', // включение связей для "employees"
    },
  },
});
```

## Связи

Связи позволяют описывать отношения между моделями, что дает возможность
автоматически встраивать связанные данные с помощью опции `include`
в методах репозитория. Ниже приводится пример автоматического разрешения
связи при использовании метода `findById`.

```
         Роль (role)
      ┌────────────────────┐
      │  id: 3  <──────────│────┐
      │  name: 'Manager'   │    │
      └────────────────────┘    │
                                │
     Пользователь (user)        │
  ┌────────────────────────┐    │
  │  id: 1                 │    │
  │  name: 'John Doe'      │    │
  │  roleId: 3   ──────────│────┘
  │  cityId: 24  ──────────│────┐
  └────────────────────────┘    │
                                │
         Город (city)           │
      ┌────────────────────┐    │
      │  id: 24  <─────────│────┘
      │  name: 'Moscow'    │
      └────────────────────┘
```

```js
// запрос документа коллекции "users",
// включая связанные данные (role и city)
const user = await userRep.findById(1, {
  include: ['role', 'city'],
});

console.log(user);
// {
//   id: 1,
//   name: 'John Doe',
//   roleId: 3,
//   role: {
//     id: 3,
//     name: 'Manager'
//   },
//   cityId: 24,
//   city: {
//     id: 24,
//     name: 'Moscow'
//   }
// }
```

### Определение связи

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

```js
import {
  DataType,
  RelationType,
  DatabaseSchema,
} from '@e22m4u/js-repository';

dbs.defineModel({
  name: 'user',
  datasource: 'memory',
  properties: {
    name: DataType.STRING,
  },
  relations: {
    // связь role -> параметры
    role: {
      type: RelationType.BELONGS_TO,
      model: 'role',
    },
    // связь city -> параметры
    city: {
      type: RelationType.BELONGS_TO,
      model: 'city',
    },
  },
});
```

**Основные параметры**

- `type: string` тип связи (обязательно);
- `model: string` название целевой модели (обязательно для некоторых типов);
- `foreignKey: string` свойство текущей модели для идентификатора цели;

*i. Для типов Belongs To и References Many значение параметра `foreignKey`
можно опустить, так как генерируется автоматически по названию связи.*

**Полиморфный режим**

- `polymorphic: boolean|string` объявление полиморфной связи;
- `discriminator: string` свойство текущей модели для названия цели;

*i. Полиморфный режим позволяет динамически определять целевую модель
по ее названию, которое хранит документ в свойстве-дискриминаторе.*

### Типы связей

- [Belongs To](#belongs-to)  
  Текущая модель ссылается на целевую по идентификатору.  
  `type: "belongsTo"` или `type: RelationType.BELONGS_TO`  
  
- [Has One](#has-one)  
  Обратная сторона `belongsTo` по принципу *"один к одному"*.  
  `type: "hasOne"` или `type: RelationType.HAS_ONE`  

- [Has Many](#has-many)  
  Обратная сторона `belongsTo` по принципу *"один ко многим"*.  
  `type: "hasMany"` или `type: RelationType.HAS_MANY`  

- [References Many](#references-many)  
  Текущая модель ссылается на целевую через массив идентификаторов.  
  `type: "referencesMany"` или `type: RelationType.REFERENCES_MANY`  

Полиморфные версии:

- [Belongs To (полиморфная)](#belongs-to-полиморфная-версия)
- [Has One (полиморфная)](#has-one-полиморфная-версия)
- [Has Many (полиморфная)](#has-many-полиморфная-версия)

Параметр `type` в определении связи принимает строку с названием типа. Чтобы исключить опечатку, рекомендуется использовать константы объекта `RelationType`
указанные ниже.

- `RelationType.BELONGS_TO`
- `RelationType.HAS_ONE`
- `RelationType.HAS_MANY`
- `RelationType.REFERENCES_MANY`

#### Belongs To

Текущая модель ссылается на целевую по идентификатору.

```
    Текущая (user)                  Целевая (role)
┌─────────────────────────┐       ┌─────────────────────────┐
│   id: 1                 │   ┌───│─> id: 5                 │
│   roleId: 5  ───────────│───┤   │   ...                   │
│   ...                   │   │   └─────────────────────────┘
└─────────────────────────┘   │ 
┌─────────────────────────┐   │   
│   id: 2                 │   │
│   roleId: 5  ───────────│───┘   
│   ...                   │       
└─────────────────────────┘       
```

Определение связи:

```js
dbs.defineModel({
  name: 'user',
  relations: {
    role: { // название связи
      type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
      model: 'role', // название целевой модели
      foreignKey: 'roleId', // внешний ключ (необязательно)
      // если "foreignKey" не указан, то свойство внешнего
      // ключа формируется согласно названию связи
      // с добавлением постфикса "Id"
    },
  },
});
```

Пример:

```js
import {
  DataType,
  RelationType,
  DatabaseSchema,
} from '@e22m4u/js-repository';

const dbs = new DatabaseSchema();

// источник данных
dbs.defineDatasource({
  name: 'myDb',
  adapter: 'memory',
});

// модель роли
dbs.defineModel({
  name: 'role',
  datasource: 'myDb',
  properties: {
    name: DataType.STRING,
  },
});

// модель пользователя
dbs.defineModel({
  name: 'user',
  datasource: 'myDb',
  properties: {
    name: DataType.STRING,
    roleId: DataType.NUMBER, // не обязательно
  },
  relations: {
    role: {
      type: RelationType.BELONGS_TO,
      model: 'role',
      foreignKey: 'roleId', // не обязательно
    },
  },
});

// создание роли
const roleRep = dbs.getRepository('role');
const role = await roleRep.create({
  id: 5,
  name: 'Manager',
});
console.log(role);
// {
//   id: 5,
//   name: 'manager'
// }

// создание пользователя
const userRep = dbs.getRepository('user');
const user = await userRep.create({
  id: 1,
  name: 'John Doe',
  roleId: role.id,
});
console.log(user);
// {
//   id: 1,
//   name: 'John Doe',
//   roleId: 5
// }

// извлечение пользователя и связанной роли (опция "include")
const userWithRole = await userRep.findById(user.id, {include: 'role'});
console.log(userWithRole);
// {
//   id: 1,
//   name: 'John Doe',
//   roleId: 5,
//   role: {
//     id: 5,
//     name: 'Manager'
//   }
// }
```

#### Has One

Обратная сторона `belongsTo` по принципу *"один к одному"*.

```
    Текущая (profile)               Целевая (user)
┌─────────────────────────┐       ┌─────────────────────────┐
│   id: 5  <──────────────│───┐   │   id: 1                 │
│   ...                   │   └───│── profileId: 5          │
└─────────────────────────┘       │   ...                   │
                                  └─────────────────────────┘
```

Определение связи:

```js
// dbs.defineModel({
//   name: 'user',
//   relations: {
//     profile: {
//       type: RelationType.BELONGS_TO,
//       model: 'profile',
//     },
//   },
// });

dbs.defineModel({
  name: 'profile',
  relations: {
    user: { // название связи
      type: RelationType.HAS_ONE, // целевая модель ссылается на текущую
      model: 'user', // название целевой модели
      foreignKey: 'profileId', // внешний ключ из целевой модели на текущую
    },
  },
});
```

#### Has Many

Обратная сторона `belongsTo` по принципу *"один ко многим"*.

```
    Текущая (role)                  Целевая (user)
┌─────────────────────────┐       ┌─────────────────────────┐
│   id: 5  <──────────────│───┐   │   id: 1                 │
│   ...                   │   ├───│── roleId: 5             │
└─────────────────────────┘   │   │   ...                   │
                              │   └─────────────────────────┘
                              │   ┌─────────────────────────┐
                              │   │   id: 2                 │
                              └───│── roleId: 5             │
                                  │   ...                   │
                                  └─────────────────────────┘
```

Определение связи:

```js
// dbs.defineModel({
//   name: 'user',
//   relations: {
//     role: {
//       type: RelationType.BELONGS_TO,
//       model: 'role',
//     },
//   },
// });

dbs.defineModel({
  name: 'role',
  relations: {
    users: { // название связи
      type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
      model: 'user', // название целевой модели
      foreignKey: 'roleId', // внешний ключ целевой модели
    },
  },
});
```

#### References Many

Текущая модель ссылается на целевую через массив идентификаторов.

```
    Текущая (article)                 Целевая (category)
┌─────────────────────────┐       ┌─────────────────────────┐
│   id: 1                 │   ┌───│─> id: 5                 │
│   categoryIds: [5, 6] ──│───┤   │   ...                   │
│   ...                   │   │   └─────────────────────────┘
└─────────────────────────┘   │   ┌─────────────────────────┐
                              └───│─> id: 6                 │
                                  │   ...                   │
                                  └─────────────────────────┘
```

Определение связи:

```js
// dbs.defineModel({name: 'category', ...

dbs.defineModel({
  name: 'article',
  relations: {
    categories: { // название связи
      type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов
      model: 'category', // название целевой модели
      foreignKey: 'categoryIds', // внешний ключ (необязательно)
      // если "foreignKey" не указан, то свойство внешнего
      // ключа формируется согласно названию связи
      // с добавлением постфикса "Ids"
    },
  },
});
```

#### Belongs To (полиморфная версия)

Текущая модель ссылается на целевую по идентификатору. Название целевой модели
определяется свойством-дискриминатором.

```
    Текущая (file)               ┌──────> Целевая 1 (letter)
┌─────────────────────────────┐  │    ┌─────────────────────────┐
│   id: 1                     │  │ ┌──│─> id: 10                │
│   referenceType: 'letter'  ─│──┘ │  │   ...                   │
│   referenceId: 10  ─────────│────┘  └─────────────────────────┘
└─────────────────────────────┘
                                 ┌──────> Целевая 2 (user)
┌─────────────────────────────┐  │    ┌─────────────────────────┐
│   id: 2                     │  │ ┌──│─> id: 5                 │
│   referenceType: 'user'  ───│──┘ │  │   ...                   │
│   referenceId: 5  ──────────│────┘  └─────────────────────────┘
└─────────────────────────────┘
```

Определение связи:

```js
dbs.defineModel({
  name: 'file',
  relations: {
    reference: { // название связи
      type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
      // полиморфный режим позволяет хранить название целевой модели
      // в свойстве-дискриминаторе, которое формируется согласно
      // названию связи с постфиксом "Type", и в данном случае
      // название целевой модели хранит "referenceType",
      // а идентификатор документа "referenceId"
      polymorphic: true,
    },
  },
});
```

Определение связи с указанием свойств:

```js
dbs.defineModel({
  name: 'file',
  relations: {
    reference: { // название связи
      type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
      polymorphic: true, // название целевой модели хранит дискриминатор
      foreignKey: 'referenceId', // свойство для идентификатора цели
      discriminator: 'referenceType', // свойство для названия целевой модели
    },
  },
});
```

#### Has One (полиморфная версия)

Обратная сторона полиморфная связи `belongsTo` по принципу *"один к одному"*.

```
    Текущая (company)  <───────┐      Целевая (license)
┌─────────────────────────┐    │  ┌─────────────────────────┐
│   id: 10  <─────────────│──┐ │  │   id: 1                 │
│   ...                   │  │ └──│── ownerType: 'company'  │
└─────────────────────────┘  └────│── ownerId: 10           │
                                  └─────────────────────────┘
```

Определение связи с указанием названия связи целевой модели:

```js
// dbs.defineModel({
//   name: 'license',
//   relations: {
//     owner: {
//       type: RelationType.BELONGS_TO,
//       polymorphic: true,
//     },
//   },
// });

dbs.defineModel({
  name: 'company',
  relations: {
    license: { // название связи
      type: RelationType.HAS_ONE, // целевая модель ссылается на текущую
      model: 'license', // название целевой модели
      polymorphic: 'owner', // название полиморфной связи целевой модели
    },
  },
});
```

Определение связи с указанием свойств целевой модели:

```js
// dbs.defineModel({
//   name: 'license',
//   relations: {
//     owner: {
//       type: RelationType.BELONGS_TO,
//       polymorphic: true,
//       foreignKey: 'ownerId',
//       discriminator: 'ownerType',
//     },
//   },
// });

dbs.defineModel({
  name: 'company',
  relations: {
    license: { // название связи
      type: RelationType.HAS_ONE, // целевая модель ссылается на текущую
      model: 'license', // название целевой модели
      polymorphic: true, // название текущей модели находится в дискриминаторе
      foreignKey: 'ownerId', // свойство целевой модели для идентификатора
      discriminator: 'ownerType', // свойство целевой модели для названия текущей
    },
  },
});
```

#### Has Many (полиморфная версия)

Обратная сторона полиморфная связи `belongsTo` по принципу *"один ко многим"*.

```
    Текущая (letter)  <─────────┐      Целевая (file)
┌──────────────────────────┐    │  ┌────────────────────────────┐
│   id: 10  <──────────────│──┐ │  │   id: 1                    │
│   ...                    │  │ ├──│── referenceType: 'letter'  │
└──────────────────────────┘  ├─│──│── referenceId: 10          │
                              │ │  └────────────────────────────┘
                              │ │  ┌────────────────────────────┐
                              │ │  │   id: 2                    │
                              │ └──│── referenceType: 'letter'  │
                              └────│── referenceId: 10          │
                                   └────────────────────────────┘
```

Определение связи с указанием названия связи целевой модели:

```js
// dbs.defineModel({
//   name: 'file',
//   relations: {
//     reference: {
//       type: RelationType.BELONGS_TO,
//       polymorphic: true,
//     },
//   },
// });

dbs.defineModel({
  name: 'letter',
  relations: {
    attachments: { // название связи
      type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
      model: 'file', // название целевой модели
      polymorphic: 'reference', // название полиморфной связи целевой модели
    },
  },
});
```

Определение связи с указанием свойств целевой модели:

```js
// dbs.defineModel({
//   name: 'file',
//   relations: {
//     reference: {
//       type: RelationType.BELONGS_TO,
//       polymorphic: true,
//       foreignKey: 'referenceId',
//       discriminator: 'referenceType',
//     },
//   },
// });

dbs.defineModel({
  name: 'letter',
  relations: {
    attachments: { // название связи
      type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
      model: 'file', // название целевой модели
      polymorphic: true, // название текущей модели находится в дискриминаторе
      foreignKey: 'referenceId', // свойство целевой модели для идентификатора
      discriminator: 'referenceType', // свойство целевой модели для названия текущей
    },
  },
});
```

## Расширение

Метод `getRepository` экземпляра `DatabaseSchema` проверяет наличие
существующего  репозитория для указанной модели и возвращает его.
В противном случае создается новый экземпляр, который будет сохранен
для последующих обращений к методу.

```js
import {Repository} from '@e22m4u/js-repository';
import {DatabaseSchema} from '@e22m4u/js-repository';

// const dbs = new DatabaseSchema();
// dbs.defineDatasource ...
// dbs.defineModel ...

const rep1 = dbs.getRepository('model');
const rep2 = dbs.getRepository('model');
console.log(rep1 === rep2); // true
```

Подмена стандартного конструктора репозитория выполняется методом
`setRepositoryCtor` сервиса `RepositoryRegistry`, который находится
в сервис-контейнере экземпляра `DatabaseSchema`. После чего все новые
репозитории будут создаваться указанным конструктором вместо стандартного.

```js
import {Repository} from '@e22m4u/js-repository';
import {DatabaseSchema} from '@e22m4u/js-repository';
import {RepositoryRegistry} from '@e22m4u/js-repository';

class MyRepository extends Repository {
  /*...*/
}

// const dbs = new DatabaseSchema();
// dbs.defineDatasource ...
// dbs.defineModel ...

dbs.getService(RepositoryRegistry).setRepositoryCtor(MyRepository);
const rep = dbs.getRepository('model');
console.log(rep instanceof MyRepository); // true
```

*i. Так как экземпляры репозитория кэшируется, то замену конструктора
следует выполнять до обращения к методу `getRepository`.*

## TypeScript

Получение типизированного репозитория с указанием интерфейса модели.

```ts
import {DataType} from '@e22m4u/js-repository';
import {RelationType} from '@e22m4u/js-repository';
import {DatabaseSchema} from '@e22m4u/js-repository';

// const dbs = new DatabaseSchema();
// dbs.defineDatasource ...

// определение модели "city"
dbs.defineModel({
  name: 'city',
  datasource: 'myDatasource',
  properties: {
    name: DataType.STRING,
    timeZone: DataType.STRING,
  },
});

// определение интерфейса "city"
interface City {
  id: number;
  name?: string;
  timeZone?: string;
}

// при получении репозитория нужной модели
// можно указать тип документов
const cityRep = dbs.getRepository<City>('city');

// теперь, методы репозитория возвращают
// тип City вместо Record<string, unknown>
const city: City = await cityRep.create({
  name: 'Moscow',
  timeZone: 'Europe/Moscow',
});
```

## Тесты

```bash
npm run test
```

## Лицензия

MIT
