Просмотр исходного кода

chore: adds english documentation

e22m4u 11 месяцев назад
Родитель
Сommit
139917c975
2 измененных файлов с 1348 добавлено и 381 удалено
  1. 962 0
      README-ru.md
  2. 386 381
      README.md

+ 962 - 0
README-ru.md

@@ -0,0 +1,962 @@
+## @e22m4u/js-repository
+
+*[English](README.md) | Русский*
+
+Реализация паттерна «Репозиторий» для Node.js
+
+- [Установка](#Установка)
+- [Импорт](#Импорт)
+- [Описание](#Описание)
+- [Пример](#Пример)
+- [Схема](#Схема)
+- [Источник данных](#Источник-данных)
+- [Модель](#Модель)
+- [Свойства](#Свойства)
+- [Валидаторы](#Валидаторы)
+- [Трансформеры](#Трансформеры)
+- [Пустые значения](#Пустые-значения)
+- [Репозиторий](#Репозиторий)
+- [Фильтрация](#Фильтрация)
+- [Связи](#Связи)
+- [Расширение](#Расширение)
+- [TypeScript](#TypeScript)
+- [Тесты](#Тесты)
+- [Лицензия](#Лицензия)
+
+## Установка
+
+```bash
+npm install @e22m4u/js-repository
+```
+
+Опционально устанавливаем адаптер.
+
+|           | описание                                                                                                                       |
+|-----------|--------------------------------------------------------------------------------------------------------------------------------|
+| `memory`  | виртуальная база в памяти процесса (не требует установки)                                                                      |
+| `mongodb` | MongoDB - система управления NoSQL базами (*[установка](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter))* |
+
+## Импорт
+
+Модуль поддерживает ESM и CommonJS стандарты.
+
+*ESM*
+
+```js
+import {Schema} from '@e22m4u/js-repository';
+```
+
+*CommonJS*
+
+```js
+const {Schema} = require('@e22m4u/js-repository');
+```
+
+## Описание
+
+Модуль позволяет абстрагироваться от различных интерфейсов баз данных,
+представляя их как именованные *источники данных*, подключаемые к *моделям*.
+*Модель* же описывает таблицу базы, колонки которой являются свойствами
+модели. Свойства модели могут иметь определенный *тип* допустимого значения,
+набор *валидаторов* и *трансформеров*, через которые проходят данные перед
+записью в базу. Кроме того, *модель* может определять классические связи
+«один к одному», «один ко многим» и другие типы отношений между моделями.
+
+Непосредственно чтение и запись данных производится с помощью *репозитория*,
+который имеет каждая модель с объявленным *источником данных*. Репозиторий
+может фильтровать запрашиваемые документы, выполнять валидацию свойств
+согласно определению модели, и встраивать связанные данные в результат
+выборки.
+
+- *Источник данных* - определяет способ подключения к базе
+- *Модель* - описывает структуру документа и связи к другим моделям
+- *Репозиторий* - выполняет операции чтения и записи документов модели
+
+```mermaid
+flowchart TD
+
+  A[Схема]
+  subgraph Базы данных
+    B[Источник данных 1]
+    C[Источник данных 2]
+  end
+  A-->B
+  A-->C
+
+  subgraph Коллекции
+    D[Модель A]
+    E[Модель Б]
+    F[Модель В]
+    G[Модель Г]
+  end
+  B-->D
+  B-->E
+  C-->F
+  C-->G
+
+  H[Репозиторий A]
+  I[Репозиторий Б]
+  J[Репозиторий В]
+  K[Репозиторий Г]
+  D-->H
+  E-->I
+  F-->J
+  G-->K
+```
+
+## Пример
+
+Объявление источника данных, модели и добавление нового документа в коллекцию.
+
+```js
+import {Schema} from '@e22m4u/js-repository';
+import {DataType} from '@e22m4u/js-repository';
+
+// создание экземпляра Schema
+const schema = new Schema();
+
+// объявление источника "myMemory"
+schema.defineDatasource({
+  name: 'myMemory', // название нового источника
+  adapter: 'memory', // выбранный адаптер
+});
+
+// объявление модели "country"
+schema.defineModel({
+  name: 'country', // название новой модели
+  datasource: 'myMemory', // выбранный источник
+  properties: { // свойства модели
+    name: DataType.STRING, // тип "string"
+    population: DataType.NUMBER, // тип "number"
+  },
+})
+
+// получение репозитория модели "country"
+const countryRep = schema.getRepository('country');
+
+// добавление нового документа в коллекцию "country"
+const country = await countryRep.create({
+  name: 'Russia',
+  population: 143400000,
+});
+
+// вывод нового документа
+console.log(country);
+// {
+//   "id": 1,
+//   "name": "Russia",
+//   "population": 143400000,
+// }
+```
+
+## Схема
+
+Экземпляр класса `Schema` хранит определения источников данных и моделей.
+
+**Методы**
+
+- `defineDatasource(datasourceDef: object): this` - добавить источник
+- `defineModel(modelDef: object): this` - добавить модель
+- `getRepository(modelName: string): Repository` - получить репозиторий
+
+**Примеры**
+
+Импорт класса и создание экземпляра схемы.
+
+```js
+import {Schema} from '@e22m4u/js-repository';
+
+const schema = new Schema();
+```
+
+Определение нового источника.
+
+```js
+schema.defineDatasource({
+  name: 'myMemory', // название нового источника
+  adapter: 'memory', // выбранный адаптер
+});
+```
+
+Определение новой модели.
+
+```js
+schema.defineModel({
+  name: 'product', // название новой модели
+  datasource: 'myMemory', // выбранный источник
+  properties: { // свойства модели
+    name: DataType.STRING,
+    weight: DataType.NUMBER,
+  },
+});
+```
+
+Получение репозитория по названию модели.
+
+```js
+const productRep = schema.getRepository('product');
+```
+
+## Источник данных
+
+Источник хранит название выбранного адаптера и его настройки. Определение
+нового источника выполняется методом `defineDatasource` экземпляра схемы.
+
+**Параметры**
+
+- `name: string` уникальное название
+- `adapter: string` выбранный адаптер
+- параметры адаптера (если имеются)
+
+**Примеры**
+
+Определение нового источника.
+
+```js
+schema.defineDatasource({
+  name: 'myMemory', // название нового источника
+  adapter: 'memory', // выбранный адаптер
+});
+```
+
+Передача дополнительных параметров адаптера.
+
+```js
+schema.defineDatasource({
+  name: 'myMongodb',
+  adapter: 'mongodb',
+  // параметры адаптера "mongodb"
+  host: '127.0.0.1',
+  port: 27017,
+  database: 'myDatabase',
+});
+```
+
+## Модель
+
+Описывает структуру документа коллекции и связи к другим моделям. Определение
+новой модели выполняется методом `defineModel` экземпляра схемы.
+
+**Параметры**
+
+- `name: string` название модели (обязательно)
+- `base: string` название наследуемой модели
+- `tableName: string` название коллекции в базе
+- `datasource: string` выбранный источник данных
+- `properties: object` определения свойств (см. [Свойства](#Свойства))
+- `relations: object` определения связей (см. [Связи](#Связи))
+
+**Примеры**
+
+Определение модели со свойствами указанного типа.
+
+```js
+schema.defineModel({
+  name: 'user', // название новой модели
+  properties: { // свойства модели
+    name: DataType.STRING,
+    age: DataType.NUMBER,
+  },
+});
+```
+
+## Свойства
+
+Параметр `properties` находится в определении модели и принимает объект, ключи
+которого являются свойствами этой модели, а значением тип свойства или объект
+с дополнительными параметрами.
+
+**Тип данных**
+
+- `DataType.ANY` разрешено любое значение
+- `DataType.STRING` только значение типа `string`
+- `DataType.NUMBER` только значение типа `number`
+- `DataType.BOOLEAN` только значение типа `boolean`
+- `DataType.ARRAY` только значение типа `array`
+- `DataType.OBJECT` только значение типа `object`
+
+**Параметры**
+
+- `type: string` тип допустимого значения (обязательно)
+- `itemType: string` тип элемента массива (для `type: 'array'`)
+- `model: string` модель объекта (для `type: 'object'`)
+- `primaryKey: boolean` объявить свойство первичным ключом
+- `columnName: string` переопределение названия колонки
+- `columnType: string` тип колонки (определяется адаптером)
+- `required: boolean` объявить свойство обязательным
+- `default: any` значение по умолчанию
+- `validate: string | array | object` см. [Валидаторы](#Валидаторы)
+- `unique: boolean | string` проверять значение на уникальность
+
+**Параметр `unique`**
+
+Если значением параметра `unique` является `true` или `'strict'`, то выполняется
+строгая проверка на уникальность. В этом режиме [пустые значения](#Пустые-значения)
+так же подлежат проверке, где `null` и `undefined` не могут повторяться более одного
+раза.
+
+Режим `'sparse'` проверяет только значения с полезной нагрузкой, исключая
+[пустые значения](#Пустые-значения), список которых отличается в зависимости
+от типа свойства. Например, для типа `string` пустым значением будет `undefined`,
+`null` и `''` (пустая строка).
+
+- `unique: true | 'strict'` строгая проверка на уникальность
+- `unique: 'sparse'` исключить из проверки [пустые значения](#Пустые-значения)
+- `unique: false | 'nonUnique'` не проверять на уникальность (по умолчанию)
+
+В качестве значений параметра `unique` можно использовать предопределенные
+константы как эквивалент строковых значений `strict`, `sparse` и `nonUnique`.
+
+- `PropertyUniqueness.STRICT`
+- `PropertyUniqueness.SPARSE`
+- `PropertyUniqueness.NON_UNIQUE`
+
+**Примеры**
+
+Краткое определение свойств модели.
+
+```js
+schema.defineModel({
+  name: 'city',
+  properties: { // свойства модели
+    name: DataType.STRING, // тип свойства "string"
+    population: DataType.NUMBER, // тип свойства "number"
+  },
+});
+```
+
+Расширенное определение свойств модели.
+
+```js
+schema.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.UNIQUE, // проверять уникальность
+    },
+  },
+});
+```
+
+Фабричное значение по умолчанию. Возвращаемое значение функции будет
+определено в момент записи документа.
+
+```js
+schema.defineModel({
+  name: 'article',
+  properties: { // свойства модели
+    tags: {
+      type: DataType.ARRAY, // тип свойства "array" (обязательно)
+      itemType: DataType.STRING, // тип элемента "string"
+      default: () => [], // фабричное значение
+    },
+    createdAt: {
+      type: DataType.STRING, // тип свойства "string" (обязательно)
+      default: () => new Date().toISOString(), // фабричное значение
+    },
+  },
+});
+```
+
+## Валидаторы
+
+Кроме проверки типа, дополнительные условия можно задать с помощью
+валидаторов, через которые будет проходить значение свойства перед
+записью в базу. Исключением являются [пустые значения](#Пустые-значения),
+которые не подлежат проверке.
+
+- `minLength: number` минимальная длинна строки или массива
+- `maxLength: number` максимальная длинна строки или массива
+- `regexp: string | RegExp` проверка по регулярному выражению
+
+**Пример**
+
+Валидаторы указываются в объявлении свойства модели параметром
+`validate`, который принимает объект с их названиями и настройками.
+
+```js
+schema.defineModel({
+  name: 'user',
+  properties: {
+    name: {
+      type: DataType.STRING,
+      validate: { // валидаторы свойства "name"
+        minLength: 2, // минимальная длинна строки
+        maxLength: 24, // максимальная длинна строки
+      },
+    },
+  },
+});
+```
+
+### Пользовательские валидаторы
+
+Валидатором является функция, в которую передается значение соответствующего
+поля перед записью в базу. Если во время проверки функция возвращает `false`,
+то выбрасывается стандартная ошибка. Подмена стандартной ошибки возможна
+с помощью выброса пользовательской ошибки непосредственно внутри функции.
+
+Регистрация пользовательского валидатора выполняется методом `addValidator`
+сервиса `PropertyValidatorRegistry`, который принимает новое название
+и функцию для проверки значения.
+
+**Пример**
+
+```js
+// создание валидатора для запрета
+// всех символов кроме чисел
+const numericValidator = (input) => {
+  return /^[0-9]+$/.test(String(input));
+}
+
+// регистрация валидатора "numeric"
+schema
+  .get(PropertyValidatorRegistry)
+  .addValidator('numeric', numericValidator);
+
+// использование валидатора в определении
+// свойства "code" для новой модели
+schema.defineModel({
+  name: 'document',
+  properties: {
+    code: {
+      type: DataType.STRING,
+      validate: 'numeric',
+    },
+  },
+});
+```
+
+## Трансформеры
+
+С помощью трансформеров производится модификация значений определенных
+полей перед записью в базу. Трансформеры позволяют указать какие изменения
+нужно производить с входящими данными. Исключением являются
+[пустые значения](#Пустые-значения), которые не подлежат трансформации.
+
+- `trim` удаление пробельных символов с начала и конца строки
+- `toUpperCase` перевод строки в верхний регистр
+- `toLowerCase` перевод строки в нижний регистр
+- `toTitleCase` перевод строки в регистр заголовка
+
+**Пример**
+
+Трансформеры указываются в объявлении свойства модели параметром
+`transform`, который принимает название трансформера. Если требуется
+указать несколько названий, то используется массив. Если трансформер
+имеет настройки, то используется объект, где ключом является название
+трансформера, а значением его параметры.
+
+```js
+schema.defineModel({
+  name: 'user',
+  properties: {
+    name: {
+      type: DataType.STRING,
+      transform: [ // трансформеры свойства "name"
+        'trim', // удалить пробелы в начале и конце строки
+        'toTitleCase', // перевод строки в регистр заголовка
+      ],
+    },
+  },
+});
+```
+
+## Пустые значения
+
+Разные типы свойств имеют свои наборы пустых значений. Эти наборы
+используются для определения наличия полезной нагрузки в значении
+свойства. Например, параметр `default` в определении свойства
+устанавливает значение по умолчанию, только если входящее значение
+является пустым. Параметр `required` исключает пустые значения
+выбрасывая ошибку. А параметр `unique` в режиме `sparse` наоборот
+допускает дублирование пустых значений уникального свойства.
+
+| тип         | пустые значения           |
+|-------------|---------------------------|
+| `'any'`     | `undefined`, `null`       |
+| `'string'`  | `undefined`, `null`, `''` |
+| `'number'`  | `undefined`, `null`, `0`  |
+| `'boolean'` | `undefined`, `null`       |
+| `'array'`   | `undefined`, `null`, `[]` |
+| `'object'`  | `undefined`, `null`, `{}` |
+
+## Репозиторий
+
+Выполняет операции чтения и записи документов определенной модели.
+Получить репозиторий можно методом `getRepository` экземпляра схемы.
+
+**Методы**
+
+- `create(data, filter = undefined)` добавить новый документ
+- `replaceById(id, data, filter = undefined)` заменить весь документ
+- `replaceOrCreate(data, filter = undefined)` заменить или создать новый
+- `patchById(id, data, filter = undefined)` частично обновить документ
+- `patch(data, where = undefined)` обновить все документы или по условию
+- `find(filter = undefined)` найти все документы или по условию
+- `findOne(filter = undefined)` найти первый документ или по условию
+- `findById(id, filter = undefined)` найти документ по идентификатору
+- `delete(where = undefined)` удалить все документы или по условию
+- `deleteById(id)` удалить документ по идентификатору
+- `exists(id)` проверить существование по идентификатору
+- `count(where = undefined)` подсчет всех документов или по условию
+
+**Аргументы**
+
+- `id: number|string` идентификатор (первичный ключ)
+- `data: object` объект отражающий состав документа
+- `where: object` параметры выборки (см. [Фильтрация](#Фильтрация))
+- `filter: object` параметры возвращаемого результата (см. [Фильтрация](#Фильтрация))
+
+**Примеры**
+
+Получение репозитория по названию модели.
+
+```js
+const countryRep = schema.getRepository('country');
+```
+
+Добавление нового документа в коллекцию.
+
+```js
+const res = await countryRep.create({
+  name: 'Russia',
+  population: 143400000,
+});
+
+console.log(res);
+// {
+//   "id": 1,
+//   "name": "Russia",
+//   "population": 143400000,
+// }
+```
+
+Поиск документа по идентификатору.
+
+```js
+const res = await countryRep.findById(1);
+
+console.log(res);
+// {
+//   "id": 1,
+//   "name": "Russia",
+//   "population": 143400000,
+// }
+```
+
+Удаление документа по идентификатору.
+
+```js
+const res = await countryRep.deleteById(1);
+
+console.log(res); // true
+```
+
+## Фильтрация
+
+Некоторые методы репозитория принимают объект настроек влияющий
+на возвращаемый результат. Максимально широкий набор таких настроек
+имеет первый параметр метода `find`, где ожидается объект содержащий
+набор опций указанных ниже.
+
+- `where: object` объект выборки
+- `order: string[]` указание порядка
+- `limit: number` ограничение количества документов
+- `skip: number` пропуск документов
+- `fields: string[]` выбор необходимых свойств модели
+- `include: object` включение связанных данных в результат
+
+### where
+
+Параметр принимает объект с условиями выборки и поддерживает широкий
+набор операторов сравнения.
+
+`{foo: 'bar'}` поиск по значению свойства `foo`  
+`{foo: {eq: 'bar'}}` оператор равенства `eq`  
+`{foo: {neq: 'bar'}}` оператор неравенства `neq`  
+`{foo: {gt: 5}}` оператор "больше" `gt`  
+`{foo: {lt: 10}}` оператор "меньше" `lt`  
+`{foo: {gte: 5}}` оператор "больше или равно" `gte`  
+`{foo: {lte: 10}}` оператор "меньше или равно" `lte`  
+`{foo: {inq: ['bar', 'baz']}}` равенство одного из значений `inq`  
+`{foo: {nin: ['bar', 'baz']}}` исключение значений массива `nin`  
+`{foo: {between: [5, 10]}}` оператор диапазона `between`  
+`{foo: {exists: true}}` оператор наличия значения `exists`  
+`{foo: {like: 'bar'}}` оператор поиска подстроки `like`  
+`{foo: {ilike: 'BaR'}}` регистронезависимая версия `ilike`  
+`{foo: {nlike: 'bar'}}` оператор исключения подстроки `nlike`  
+`{foo: {nilike: 'BaR'}}` регистронезависимая версия `nilike`  
+`{foo: {regexp: 'ba.+'}}` оператор регулярного выражения `regexp`  
+`{foo: {regexp: 'ba.+', flags: 'i'}}` флаги регулярного выражения
+
+*i. Условия можно объединять операторами `and`, `or` и `nor`.*
+
+**Примеры**
+
+Применение условий выборки при подсчете документов.
+
+```js
+const res = await rep.count({
+  authorId: 251,
+  publishedAt: {
+    lte: '2023-12-02T14:00:00.000Z',
+  },
+});
+```
+
+Применение оператора `or` при удалении документов.
+
+```js
+const res = await rep.delete({
+  or: [
+    {draft: true},
+    {title: {like: 'draft'}},
+  ],
+});
+```
+
+### 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"
+    },
+  },
+});
+```
+
+## Связи
+
+Параметр `relations` находится в определении модели и принимает
+объект, ключ которого является названием связи, а значением объект
+с параметрами.
+
+**Параметры**
+
+- `type: string` тип связи
+- `model: string` название целевой модели
+- `foreignKey: string` свойство текущей модели для идентификатора цели
+- `polymorphic: boolean|string` объявить связь полиморфной*
+- `discriminator: string` свойство текущей модели для названия целевой*
+
+*i. Полиморфный режим позволяет динамически определять целевую модель
+по ее названию, которое хранит документ в свойстве-дискриминаторе.*
+
+**Тип связи**
+
+- `belongsTo` - текущая модель содержит свойство для идентификатора цели
+- `hasOne` - обратная сторона `belongsTo` по принципу "один к одному"
+- `hasMany` - обратная сторона `belongsTo` по принципу "один ко многим"
+- `referencesMany` - документ содержит массив с идентификаторами целевой модели
+
+**Примеры**
+
+Объявление связи `belongsTo`
+
+```js
+schema.defineModel({
+  name: 'user',
+  relations: {
+    role: { // название связи
+      type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
+      model: 'role', // название целевой модели
+      foreignKey: 'roleId', // внешний ключ (необязательно)
+      // если "foreignKey" не указан, то свойство внешнего
+      // ключа формируется согласно названию связи
+      // с добавлением постфикса "Id"
+    },
+  },
+});
+```
+
+Объявление связи `hasMany`
+
+```js
+schema.defineModel({
+  name: 'role',
+  relations: {
+    users: { // название связи
+      type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
+      model: 'user', // название целевой модели
+      foreignKey: 'roleId', // внешний ключ из целевой модели на текущую
+    },
+  },
+});
+```
+
+Объявление связи `referencesMany`
+
+```js
+schema.defineModel({
+  name: 'article',
+  relations: {
+    categories: { // название связи
+      type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов
+      model: 'category', // название целевой модели
+      foreignKey: 'categoryIds', // внешний ключ (необязательно)
+      // если "foreignKey" не указан, то свойство внешнего
+      // ключа формируется согласно названию связи
+      // с добавлением постфикса "Ids"
+    },
+  },
+});
+```
+
+Полиморфная версия `belongsTo`
+
+```js
+schema.defineModel({
+  name: 'file',
+  relations: {
+    reference: { // название связи
+      type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
+      // полиморфный режим позволяет хранить название целевой модели
+      // в свойстве-дискриминаторе, которое формируется согласно
+      // названию связи с постфиксом "Type", и в данном случае
+      // название целевой модели хранит "referenceType",
+      // а идентификатор документа "referenceId"
+      polymorphic: true,
+    },
+  },
+});
+```
+
+Полиморфная версия `belongsTo` с указанием свойств.
+
+```js
+schema.defineModel({
+  name: 'file',
+  relations: {
+    reference: { // название связи
+      type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
+      polymorphic: true, // название целевой модели хранит дискриминатор
+      foreignKey: 'referenceId', // свойство для идентификатора цели
+      discriminator: 'referenceType', // свойство для названия целевой модели
+    },
+  },
+});
+```
+
+Полиморфная версия `hasMany` с указанием названия связи целевой модели.
+
+```js
+schema.defineModel({
+  name: 'letter',
+  relations: {
+    attachments: { // название связи
+      type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
+      model: 'file', // название целевой модели
+      polymorphic: 'reference', // название полиморфной связи целевой модели
+    },
+  },
+});
+```
+
+Полиморфная версия `hasMany` с указанием свойств целевой модели.
+
+```js
+schema.defineModel({
+  name: 'letter',
+  relations: {
+    attachments: { // название связи
+      type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
+      model: 'file', // название целевой модели
+      polymorphic: true, // название текущей модели находится в дискриминаторе
+      foreignKey: 'referenceId', // свойство целевой модели для идентификатора
+      discriminator: 'referenceType', // свойство целевой модели для названия текущей
+    },
+  },
+});
+```
+
+## Расширение
+
+Метод `getRepository` экземпляра схемы проверяет наличие существующего
+репозитория для указанной модели и возвращает его. В противном случае
+создается новый экземпляр, который будет сохранен для последующих
+обращений к методу.
+
+```js
+import {Schema} from '@e22m4u/js-repository';
+import {Repository} from '@e22m4u/js-repository';
+
+// const schema = new Schema();
+// schema.defineDatasource ...
+// schema.defineModel ...
+
+const rep1 = schema.getRepository('model');
+const rep2 = schema.getRepository('model');
+console.log(rep1 === rep2); // true
+```
+
+Подмена стандартного конструктора репозитория выполняется методом
+`setRepositoryCtor` сервиса `RepositoryRegistry`, который находится
+в контейнере экземпляра схемы. После чего все новые репозитории будут
+создаваться указанным конструктором вместо стандартного.
+
+```js
+import {Schema} from '@e22m4u/js-repository';
+import {Repository} from '@e22m4u/js-repository';
+import {RepositoryRegistry} from '@e22m4u/js-repository';
+
+class MyRepository extends Repository {
+  /*...*/
+}
+
+// const schema = new Schema();
+// schema.defineDatasource ...
+// schema.defineModel ...
+
+schema.get(RepositoryRegistry).setRepositoryCtor(MyRepository);
+const rep = schema.getRepository('model');
+console.log(rep instanceof MyRepository); // true
+```
+
+*i. Так как экземпляры репозитория кэшируется, то замену конструктора
+следует выполнять до обращения к методу `getRepository`.*
+
+## TypeScript
+
+Получение типизированного репозитория с указанием интерфейса модели.
+
+```ts
+import {Schema} from '@e22m4u/js-repository';
+import {DataType} from '@e22m4u/js-repository';
+import {RelationType} from '@e22m4u/js-repository';
+
+// const schema = new Schema();
+// schema.defineDatasource ...
+// schema.defineModel ...
+
+// определение модели "city"
+schema.defineModel({
+  name: 'city',
+  datasource: 'myDatasource',
+  properties: {
+    title: DataType.STRING,
+    timeZone: DataType.STRING,
+  },
+  relations: {
+    country: {
+      type: RelationType.BELONGS_TO,
+      model: 'country',
+    },
+  },
+});
+
+// определение интерфейса "city"
+interface City {
+  id: number;
+  title?: string;
+  timeZone?: string;
+  countryId?: number;
+  country?: Country;
+}
+
+// получаем репозиторий по названию модели
+// указывая ее тип и тип идентификатора
+const cityRep = schema.getRepository<City, number>('city');
+```
+
+## Тесты
+
+```bash
+npm run test
+```
+
+## Лицензия
+
+MIT

Разница между файлами не показана из-за своего большого размера
+ 386 - 381
README.md


Некоторые файлы не были показаны из-за большого количества измененных файлов