## @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)` #### create(data, filter = undefined) Добавляет новый документ в коллекцию и возвращает его. ```js // вызываем метод `create` с передачей // нового состава первым параметром const person = await rep.create({ name: 'Rick Sanchez', dimension: 'C-137', age: 67, }); // выводим результат console.log(person); // { // id: 1, // name: 'Rick Sanchez', // dimension: 'C-137', // age: 67 // } ``` Использование параметра `filter` ```js // второй параметр принимает объект настроек // возвращаемого результата (опционально) const result = await rep.create(data, { // "fields" - если определено, то результат // будут включать только указанные поля fields: 'name', fields: ['name', 'age'], // "include" - включить в результат связанные // документы (см. Связанные документы) include: 'son', include: {son: 'hobbies'}, include: ['son', 'daughter'], }); ``` #### replaceById(id, data, filter = undefined) Заменяет существующий документ по идентификатору и возвращает его. Если идентификатор не найден, то выбрасывает исключение. ```js // идентификатор 1 имеет следующую // структуру документа // { // id: 1, // name: 'Rick Sanchez', // dimension: 'C-137', // age: 67 // } // вызываем метод `replaceById` с передачей // идентификатора и нового состава const person = await rep.replaceById(1, { name: 'Morty Smith', kind: 'a young teenage boy', age: 14, }); // выводим результат console.log(person); // { // id: 1, // name: 'Morty Smith', <= значение обновлено // kind: 'a young teenage boy', <= добавлено новое поле // age: 14 <= значение обновлено // } // поле "dimension" удалено, так как // не передавалось с новым составом ``` #### patchById(id, data, filter = undefined) Частично обновляет существующий документ по идентификатору и возвращает его. Если идентификатор не найден, то выбрасывает исключение. ```js // идентификатор 24 имеет следующий // состав документа // { // "id": 24, // "type": "airport", // "name": "Domodedovo Airport", // "code": "DME" // } // вызываем метод `patchById` с передачей // идентификатора и новых значений обновляемых // полей const result = await rep.patchById(24, { name: 'Sheremetyevo Airport', code: 'SVO', featured: true, }); // выводим результат console.log(result); // { // "id": 24, // "type": "airport", // "name": "Sheremetyevo Airport", <= значение обновлено // "code": "SVO", <= значение обновлено // "featured": true <= добавлено новое поле // } ``` #### patch(data, where = undefined) Обновляет документы и возвращает их число. Используется для обновления нескольких документов согласно условиям выборки, или всей коллекции сразу. ```js // коллекция имеет два документа // [ // { // "id": 1, // "type": null, // "name": "Bangkok" // }, // { // "id": 2, // "type": null, // "name": "Moscow" // } // ] // вызываем метод `patch` с передачей // значений для обновляемых полей const result = await rep.patch({ type: 'city', updatedAt: new Date().toISOString(), }); // выводим количество затронутых // документов console.log(result); // 2 // методом `find` просматриваем коллекцию // для проверки изменений const docs = await rep.find(); console.log(docs); // [ // { // "id": 1, // "type": "city", <= значение обновлено // "name": "Bangkok", // "updatedAt": "2023-12-02T14:13:27.649Z" <= добавлено новое поле // }, // { // "id": 2, // "type": "city", <= значение обновлено // "name": "Moscow", // "updatedAt": "2023-12-02T14:13:27.649Z" <= добавлено новое поле // } // ] ``` Условия выборки (опционально). ```js // второй параметр метода `patch` // принимает условия выборки const result = await rep.patch(data, { type: 'city', // поле "type" должно иметь значение "city" description: { // оператор "like" проверяет поле "description" like: 'the capital', // на наличие подстроки "the capital" }, updatedAt: { // оператор "lt" проверяет значение поля lt: '2023-12-02T21:00:00.000Z', // "updatedAt" на наличие более ранней даты, }, // чем указана в условии // см. "Фильтрация" }); ``` #### find(filter = undefined) Возвращает все документы коллекции или согласно условию. ```js // вызов метода `find` без аргументов // возвращает всю коллекцию const result = await rep.find(); console.log(result); // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] ``` Фильтрация результата (опционально). ```js // первый параметр может принимать // объект cо следующими настройками const result = await rep.find({ // "where" - фильтрация выборки по условию, где // указанные поля должны содержать определенные // значения (см. Фильтрация) where: {type: 'article', published: true}, where: {description: {like: 'breaking news'}}, where: {publishedAt: {lte: '2023-12-02T21:00:00.000Z'}}, // "order" - сортировка по указанному полю может // принимать постфикс DESC для обратного порядка order: 'foo', order: 'bar DESC', order: ['foo', 'bar DESC'], // "limit" - ограничение результата // "skip" - пропуск документов limit: 10, skip: 10, // "fields" - если определено, то документы выборки // будут включать только указанные поля fields: 'title', fields: ['title', 'published'], // "include" - включить в результат связанные // документы (см. Связанные документы) include: 'author', include: {author: 'city'}, include: ['author', 'categories'], }); ``` #### findOne(filter = undefined) Возвращает первый найденный документ или `undefined` ```js // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] const result = await rep.findOne(); console.log(result); // { // "id": 1, // "title": "The Forgotten Ship" // } ``` #### findById(id, filter = undefined) Поиск документа по идентификатору. Возвращает найденный документ или выбрасывает исключение. ```js // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] const result = await rep.findById(2); console.log(result); // { // "id": 2, // "title": "A Giant Bellows" // } ``` #### delete(where = undefined) Удаляет документы коллекции. Возвращает количество удаленных документов. ```js // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] const result = await rep.delete(); console.log(result); // 3 const docs = await rep.find(); console.log(docs); // [] ``` #### deleteById(id) Удаляет документ по идентификатору. Возвращает `true` в случае успеха или `false` если не найден. ```js // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] const result = await rep.deleteById(2); console.log(result); // true const docs = await rep.find(); console.log(docs); // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] ``` #### exists(id) Проверка существования документа по идентификатору. Возвращает `true` если найден, в противном случае `false`. ```js // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] const result1 = await rep.exists(2); console.log(result1); // true const result2 = await rep.exists(10); console.log(result2); // false ``` #### count(where = undefined) Подсчет количества документов. Возвращает число найденных документов. ```js // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] const result = await rep.count(); console.log(result); // 3 ``` ## Расширение При использовании метода `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('myModel'); const rep2 = schema.getRepository('myModel'); console.log(rep1 === rep2); // true ``` Если требуется изменить или расширить поведение репозитория, то перед его созданием можно переопределить его конструктор методом `setRepositoryCtor`. После чего, все создаваемые репозитории будут использовать уже пользовательский конструктор вместо стандартного. ```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.get(RepositoryRegistry).setRepositoryCtor(MyRepository); // теперь schema.getRepository(modelName) будет возвращать // экземпляр класса MyRepository ``` ## Тесты ```bash npm run test ``` ## Лицензия MIT