## @e22m4u/js-repository Модуль для работы с базами данных для Node.js - [Установка](#Установка) - [Описание](#Описание) - [Пример](#Пример) - [Схема](#Схема) - [Источник данных](#Источник-данных) - [Модель](#Модель) - [Свойства](#Свойства) - [Валидаторы](#Валидаторы) - [Репозиторий](#Репозиторий) - [Фильтрация](#Фильтрация) - [Связи](#Связи) - [Расширение](#Расширение) - [TypeScript](#TypeScript) - [Тесты](#Тесты) - [Лицензия](#Лицензия) ## Установка ```bash npm install @e22m4u/js-repository ``` Опционально устанавливаем адаптер. | | описание | |-----------|--------------------------------------------------------------------------------------------------------------------------------| | `memory` | виртуальная база в памяти процесса (не требует установки) | | `mongodb` | MongoDB - система управления NoSQL базами (*[установка](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter))* | ## Описание Модуль позволяет объединить несколько баз данных в единую абстракцию «Схема». - *Источник данных* - определяет способ подключения к базе - *Модель* - описывает структуру документа и связи к другим моделям - *Репозиторий* - выполняет операции чтения и записи документов модели Схема ## Пример Определение источника данных, модели и добавление нового документа в коллекцию. ```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` см. [Валидаторы](#Валидаторы) **Примеры** Краткое определение свойств модели. ```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, // значение по умолчанию }, }, }); ``` Фабричное значение по умолчанию. Возвращаемое значение функции будет определено в момент записи документа. ```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, // максимальная длинна строки }, }, }, }); ``` ## Репозиторий Выполняет операции чтения и записи документов определенной модели. Получить репозиторий можно методом `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; // открыть свойства (опционально) [property: string]: unknown; } // получаем репозиторий по названию модели // указывая ее тип и тип идентификатора const cityRep = schema.getRepository('city'); ``` ## Тесты ```bash npm run test ``` ## Лицензия MIT