## @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[Datasource]-->B[Data Model]-->С[Repository]; ``` ## Точка входа Определения источников и моделей хранятся в экземпляре класса `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', // выбранный адаптер }); ``` ## Модель данных Когда источники определены, можно перейти к описанию моделей данных. Модель может определять как структуру какого-либо объекта, так и являться отражением реальной коллекции базы. Представьте себе коллекцию торговых точек, у каждой из которых имеются координаты `lat` и `lng`. Мы можем заранее определить модель для объекта координат методом `defineModel` и использовать ее в других коллекциях. ```js schema.defineModel({ name: 'latLng', // название новой модели properties: { // поля модели lat: DataType.NUMBER, // поле широты lng: DataType.NUMBER, // поле долготы }, }); ``` **Параметры модели:** - `name: string` уникальное название (обязательно) - `datasource: string` выбранный источник данных - `properties: object` определения полей модели - `relations: object` определения связей Параметр `properties` принимает объект, ключи которого являются именами полей, а значением тип поля или объект с дополнительными параметрами. ```js schema.defineModel({ name: 'latLng', properties: { lat: DataType.NUMBER, // краткое определение поля "lat" lng: { // расширенное определение поля "lng" type: DataType.NUMBER, // тип допустимого значения required: true, // исключает null и undefined }, }, }); ``` **Типы данных:** - `DataType.ANY` - `DataType.STRING` - `DataType.NUMBER` - `DataType.BOOLEAN` - `DataType.ARRAY` - `DataType.OBJECT` Модель `latLng` всего лишь описывает структуру объекта координат, тогда как торговая точка должна иметь реальную таблицу в базе. По аналогии с предыдущим примером, добавим модель `place`, но дополнительно укажем источник данных в параметре `datasource` ```js schema.defineModel({ name: 'place', datasource: 'myMemory', // выбранный источник данных properties: { name: DataType.STRING, // поле для названия торговой точки location: { // поле для объекта координат type: DataType.OBJECT, // допускать только объекты model: 'latLng', // модель для объекта координат }, }, }); ``` В примере выше мы использовали модель `latLng` как структуру допустимого значения поля `location`. Возможный документ данной коллекции может выглядеть так: ```json { "id": 1, "name": "Burger King", "location": { "lat": 32.412891, "lng": 34.7660061 } } ``` Стоит обратить внимание, что мы могли бы не объявлять параметр `properties`, при этом теряя возможность проверки данных перед записью в базу. ```js schema.defineModel({ name: 'place', adapter: 'myMemory', // параметр "properties" не указан }); ``` **Параметры поля:** - `type: string` тип допустимого значения (обязательно) - `itemType: string` тип элемента массива (для `type: 'array'`) - `model: string` модель объекта (для `type: 'object'`) - `primaryKey: boolean` объявить поле первичным ключом - `columnName: string` переопределение названия колонки - `columnType: string` тип колонки (определяется адаптером) - `required: boolean` объявить поле обязательным - `default: any` значение по умолчанию ## Репозиторий В отличие от `latLng`, модель `place` имеет источник данных с названием `myMemory`, который был объявлен ранее. Наличие источника позволяет получить репозиторий по названию модели. ```js const rep = schema.getRepository('place'); ``` **Методы:** - `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` включение связанных данных в результат (см. Связи) При использовании метода `find` может потребоваться комбинация параметров `limit` и `skip` для механизма пагинации. ```js // первый параметр метода `find` принимает // объект настроек возвращаемого результата const result = await rep.find({ limit: 12, skip: 24, }); ``` Документы могут быть отсортированы по указанным полям и в нужном направлении, где `ASC` - по умолчанию, или `DESC` - в обратном порядке. ```js const result = await rep.find({ // по полю "featured" order: 'featured', // по полю "publishedAt" в обратном порядке order: 'publishedAt DESC', // по нескольким полям в разных направлениях order: ['publishedAt DESC', 'featured ASC', 'id'], }); ``` Сократить объем документа можно параметром `fields` указав необходимый набор полей. ```js const result = await rep.find({ // включить в результат только поле "title" fields: 'title', // или поле "title", "createdAt" и "featured" fields: ['title', 'createdAt', 'featured'], // первичный ключ документа // указывать не обязательно }); ``` ## Тесты ```bash npm run test ``` ## Лицензия MIT