## @e22m4u/js-repository Абстракция для работы с базами данных для Node.js - [Установка](#Установка) - [Введение](#Введение) - [Настройка](#Настройка) - [Источник данных](#Источник-данных) - [Модель данных](#Модель-данных) - [Репозиторий](#Репозиторий) - [Фильтрация](#Фильтрация) - [Связи](#Связи) - [Тесты](#Тесты) ## Установка ```bash npm install @e22m4u/js-repository ``` Опционально устанавливаем адаптер. Например, если используется *MongoDB*, то для подключения потребуется установить [адаптер mongodb](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter) как отдельную зависимость. ```bash npm install @e22m4u/js-repository-mongodb-adapter ``` **Доступные адаптеры** | | описание | |-----------|--------------------------------------------------------------------------------------------------------------------------------| | `memory` | виртуальная база в памяти процесса (для разработки и тестирования) | | `mongodb` | MongoDB - система управления NoSQL базами (*[установка](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter))* | ## Введение Модуль позволяет спроектировать систему связанных данных, доступ к которым осуществляется посредством репозиториев. Каждый репозиторий имеет собственную модель, которая описывает структуру документа коллекции, а так же определяет связи к другим моделям. ```mermaid flowchart LR A[Источник]-->B[Модель]-->С[Ропозиторий]; ``` ## Настройка Перед началом работы с коллекциями потребуется выполнить настройку проекта, которая заключается в определении источников и моделей данных. Определения хранятся в экземпляре класса `Schema`, и первым шагом будет создание данного экземпляра. ```js import {Schema} from '@e22m4u/js-repository'; const schema = new Schema(); ``` Интерфейс `Schema` содержит три основных метода: - `defineDatasource(datasourceDef: object): this` - добавить источник - `defineModel(modelDef: object): this` - добавить модель - `getRepository(modelName: string): Repository` - получить репозиторий ## Источник данных Источник описывает способ подключения к базе и используемый адаптер. Если адаптер имеет настройки, то они передаются вместе с объектом определения источника методом `defineDatasource`, как это показано ниже. ```js schema.defineDatasource({ name: 'myMongo', // название нового источника adapter: 'mongodb', // название выбранного адаптера // настройки адаптера mongodb host: '127.0.0.1', port: 27017, database: 'data' }); ``` **Параметры источника** - `name: string` уникальное название - `adapter: string` выбранный адаптер - дополнительные параметры (зависит от адаптера) При желании можно использовать встроенный адаптер `memory`, который хранит данные в памяти процесса. У него нет специальных настроек, и он отлично подходит для тестов и прототипирования. ```js schema.defineDatasource({ name: 'myMemory', // название источника adapter: 'memory', // выбранный адаптер }); ``` ## Модель данных Модель данных описывает структуру документа и может указывать на источник, который будет использован репозиторием для доступа к коллекции. ```js // объявление модели "city" schema.defineModel({ name: 'city', // название новой модели properties: { // поля модели name: DataType.STRING, population: DataType.NUMBER, }, }); ``` Название модели должно быть уникальным, так как оно используется для определения отношений к другим моделям, и для названия таблицы в базе данных. При необходимости можно явно задать название таблицы параметром `tableName`, если оно не соответствует названию модели. Если модель отражает реальную коллекцию базы, а не является частью другой, то указывая параметр `datasource` появляется возможность получить репозиторий этой модели (см. [Репозиторий](#Репозиторий)), с помощью которого выполняются операции чтения и записи в базу. ```js schema.defineModel({ name: 'city', datasource: 'myMemory', // выбранный источник properties: { name: DataType.STRING, population: DataType.NUMBER, }, }); ``` Параметр `properties` принимает объект, ключи которого являются именами полей, а значением тип поля или объект с дополнительными параметрами. Эти настройки используются для проверки данных перед сохранением в базу и установки значений по умолчанию (если имеются). ```js schema.defineModel({ name: 'city', datasource: 'myMemory', properties: { name: DataType.STRING, population: { // расширенные параметры type: DataType.NUMBER, // тип значения default: 0, // значение по умолчанию }, }, }); ``` #### Параметры модели - `name: string` название модели (обязательно) - `tableName: string` название коллекции в базе - `datasource: string` выбранный источник данных - `properties: object` определения полей модели - `relations: object` определения связей (см. Связи) #### Типы данных - `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` значение по умолчанию ## Репозиторий Объявление модели имеет необязательный параметр `datasource`, который указывает на источник хранения данных. Наличие источника позволяет получить репозиторий по названию такой модели. Репозиторий, в свою очередь, предоставляет интерфейс для чтения и записи данных этой модели. ```js import {Schema} from '@e22m4u/js-repository'; const schema = new Schema(); // объявление источника "myMemory" schema.defineDatasource({ name: 'myMemory', adapter: 'memory', }); // объявление модели "city" schema.defineModel({ name: 'city', datasource: 'myMemory', // источник данных properties: { name: DataType.STRING, population: DataType.NUMBER, }, }); // получение репозитория модели "city" const cityRep = schema.getRepository('city'); // записываем новый документ "city" const city = await cityRep.create({ name: 'Moscow', population: 11980000 }); // выводим результат console.log(city); // { // "id": 1, // "name": "Moscow", // "population": 11980000 // } ``` #### Методы - `create(data, filter = undefined)` добавить новый документ - `replaceById(id, 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` идентификатор (первичный ключ) - `data` объект отражающий состав документа - `where` параметры выборки (см. Фильтрация) - `filter` параметры возвращаемого результата (см. Фильтрация) ## Фильтрация Некоторые методы репозитория опционально принимают параметр `where` для условий выборки и/или параметр `filter` управляющий возвращаемым значением. ### where Рассмотрим применение параметра `where` на примере метода `patch`, который выполняет поиск и обновление документов по определенному условию. Сигнатура метода `patch(data, where = undefined)` указывает на два параметра, где первый принимает объект данных, а второй является опциональным объектом выборки. ```js // вызов метода `patch` await rep.patch( // обновить значения полей // "hidden" и "updatedAt" { hidden: true, updatedAt: new Date().toISOString(), }, // только в тех документах, где поле "type" // равно значению "article", а "publishedAt" // содержит дату более раннюю, чем указана // в операторе "lte" { type: 'article', publishedAt: { lte: '2023-12-02T14:00:00.000Z', } }, ); ``` Второй аргумент метода `patch` предыдущего листинга содержит условия выборки по которым будет выполнен поиск и обновление документов. Полный список возможных операторов сравнения приводится ниже. `{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'}}` флаги регулярного выражения ### filter Методы репозитория асинхронны и возвращают `Promise` с некоторым значением. Параметр `filter` позволяет влиять на это значение более широким образом, чем просто ограничение для затрагиваемых данных. Ниже представлен список опциональных полей объекта `filter`, одним из которых является `where` (рассматривался ранее). - `where` объект выборки - `order` указание порядка - `limit` ограничение количества документов - `skip` пропуск документов - `fields` выбор необходимых полей документа - `include` включение связанных данных в результат (см. Связи) #### order Документы могут быть отсортированы по указанным полям и в нужном направлении, где `ASC` - по умолчанию, и `DESC` - в обратном порядке. ```js // на примере метода "find" const result = await rep.find({ // упорядочить по полю "featured" order: 'featured', // по полю "featured" в обратном порядке order: 'featured DESC', // по нескольким полям в разных направлениях order: ['featured ASC', 'publishedAt DESC', 'id'], }); ``` #### limit и skip При использовании метода `find` может потребоваться комбинация параметров `limit` и `skip` для механизма пагинации. ```js // первый параметр метода `find` принимает // объект настроек возвращаемого результата const result = await rep.find({ limit: 12, // вернуть не более 14и документов skip: 24, // пропустить 24 документа выборки }); ``` #### fields Сократить объем документа можно параметром `fields` указав необходимый набор полей. ```js // на примере метода "find" const result = await rep.find({ // включить в результат только поле "title" fields: 'title', // или поле "title", "createdAt" и "featured" fields: ['title', 'createdAt', 'featured'], // первичный ключ указывать не обязательно }); ``` ## Связи WIP ## Тесты ```bash npm run test ``` ## Лицензия MIT