|
|
@@ -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
|