## @e22m4u/js-repository Реализация паттерна «Репозиторий» для работы с базами данных в Node.js - [Установка](#установка) - [Импорт](#импорт) - [Описание](#описание) - [Пример](#пример) - [Схема](#схема) - [Источник данных](#источник-данных) - [Модель](#модель) - [Свойства](#свойства) - [Валидаторы](#валидаторы) - [Глобальные валидаторы](#глобальные-валидаторы) - [Регистрация глобальных валидаторов](#регистрация-глобальных-валидаторов) - [Локальные валидаторы](#локальные-валидаторы) - [Трансформеры](#трансформеры) - [Глобальные трансформеры](#глобальные-трансформеры) - [Регистрация глобальных трансформеров](#регистрация-глобальных-трансформеров) - [Локальные трансформеры](#локальные-трансформеры) - [Пустые значения](#пустые-значения) - [Переопределение пустых значений](#переопределение-пустых-значений) - [Репозиторий](#репозиторий) - [create](#repositorycreate) - [replaceById](#repositoryreplacebyid) - [replaceOrCreate](#repositoryreplaceorcreate) - [patchById](#repositorypatchbyid) - [patch](#repositorypatch) - [find](#repositoryfind) - [findOne](#repositoryfindone) - [findById](#repositoryfindbyid) - [delete](#repositorydelete) - [deleteById](#repositorydeletebyid) - [exists](#repositoryexists) - [count](#repositorycount) - [Фильтрация](#фильтрация) - [Связи](#связи) - [Расширение](#расширение) - [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 {DatabaseSchema} from '@e22m4u/js-repository'; ``` *CommonJS* ```js const {DatabaseSchema} = 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 {DataType} from '@e22m4u/js-repository'; import {DatabaseSchema} from '@e22m4u/js-repository'; // создание экземпляра DatabaseSchema const dbs = new DatabaseSchema(); // объявление источника "myMemory" dbs.defineDatasource({ name: 'myMemory', // название нового источника adapter: 'memory', // выбранный адаптер }); // объявление модели "country" dbs.defineModel({ name: 'country', // название новой модели datasource: 'myMemory', // выбранный источник properties: { // свойства модели name: DataType.STRING, // тип "string" population: DataType.NUMBER, // тип "number" }, }) // получение репозитория модели const countryRep = dbs.getRepository('country'); // добавление нового документа в коллекцию const country = await countryRep.create({ name: 'Russia', population: 143400000, }); // вывод нового документа console.log(country); // { // id: 1, // name: 'Russia', // population: 143400000, // } ``` ## Схема Экземпляр класса `DatabaseSchema` хранит определения источников данных и моделей. **Методы** - `defineDatasource(datasourceDef: object): this` - добавить источник - `defineModel(modelDef: object): this` - добавить модель - `getRepository(modelName: string): Repository` - получить репозиторий **Примеры** Импорт класса и создание экземпляра схемы. ```js import {DatabaseSchema} from '@e22m4u/js-repository'; const dbs = new DatabaseSchema(); ``` Определение нового источника. ```js dbs.defineDatasource({ name: 'myMemory', // название нового источника adapter: 'memory', // выбранный адаптер }); ``` Определение новой модели. ```js dbs.defineModel({ name: 'product', // название новой модели datasource: 'myMemory', // выбранный источник properties: { // свойства модели name: DataType.STRING, weight: DataType.NUMBER, }, }); ``` Получение репозитория по названию модели. ```js const productRep = dbs.getRepository('product'); ``` ## Источник данных Источник хранит название выбранного адаптера и его настройки. Определение нового источника выполняется методом `defineDatasource` экземпляра `DatabaseSchema`. **Параметры** - `name: string` уникальное название - `adapter: string` выбранный адаптер - параметры адаптера (если имеются) **Примеры** Определение нового источника. ```js dbs.defineDatasource({ name: 'myMemory', // название нового источника adapter: 'memory', // выбранный адаптер }); ``` Передача дополнительных параметров адаптера. ```js dbs.defineDatasource({ name: 'myMongodb', adapter: 'mongodb', // параметры адаптера "mongodb" host: '127.0.0.1', port: 27017, database: 'myDatabase', }); ``` ## Модель Описывает структуру документа коллекции и связи к другим моделям. Определение новой модели выполняется методом `defineModel` экземпляра `DatabaseSchema`. **Параметры** - `name: string` название модели (обязательно) - `base: string` название наследуемой модели - `tableName: string` название коллекции в базе - `datasource: string` выбранный источник данных - `properties: object` определения свойств (см. [Свойства](#Свойства)) - `relations: object` определения связей (см. [Связи](#Связи)) **Примеры** Определение модели со свойствами указанного типа. ```js dbs.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 | Function | 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 dbs.defineModel({ name: 'city', properties: { // свойства модели name: DataType.STRING, // тип свойства "string" population: DataType.NUMBER, // тип свойства "number" }, }); ``` Расширенное определение свойств модели. ```js dbs.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.STRICT, // проверять уникальность }, }, }); ``` Фабричное значение по умолчанию. Возвращаемое значение функции будет определено в момент записи документа. ```js dbs.defineModel({ name: 'article', properties: { // свойства модели tags: { type: DataType.ARRAY, // тип свойства "array" (обязательно) itemType: DataType.STRING, // тип элемента "string" default: () => [], // фабричное значение }, createdAt: { type: DataType.STRING, // тип свойства "string" (обязательно) default: () => new Date().toISOString(), // фабричное значение }, }, }); ``` ## Валидаторы Валидаторы используются для проверки значения свойства перед записью в базу. Проверка значения валидатором выполняется сразу после проверки типа, указанного в определении свойства модели. [Пустые значения](#пустые-значения) пропускают проверку валидаторами, так как не имеют полезной нагрузки. ### Глобальные валидаторы Модуль поставляется с набором глобальных валидаторов: - `regexp` проверка по регулярному выражению, *параметр: `string | RegExp` - регулярное выражение;* - `maxLength` максимальная длина строки или массива, *параметр: `number` - максимальная длина;* - `minLength` минимальная длина строки или массива, *параметр: `number` - минимальная длина;* Валидаторы указанные ниже находятся в разработке: - `isLowerCase` проверка регистра (только строчные буквы); - `isUpperCase` проверка регистра (только прописные буквы); - `isEmail` проверка формата электронного адреса; **Примеры** Использование глобального валидатора. ```js dbs.defineModel({ name: 'user', properties: { email: { type: DataType.STRING, validate: 'isEmail', }, }, }); ``` Использование глобальных валидаторов в виде массива. ```js dbs.defineModel({ name: 'user', properties: { email: { type: DataType.STRING, validate: [ 'isEmail', 'isLowerCase', ], }, }, }); ``` Использование глобальных валидаторов с передачей аргументов. ```js dbs.defineModel({ name: 'user', properties: { name: { type: DataType.STRING, validate: { minLength: 2, maxLength: 24, regexp: /^[a-zA-Z-']+$/, }, }, }, }); ``` Глобальные валидаторы без параметров могут принимать любые аргументы. ```js dbs.defineModel({ name: 'user', properties: { email: { type: DataType.STRING, validate: { maxLength: 100, // так как валидатор "isEmail" не имеет параметров, // его определение допускает передачу любого значения // в качестве аргумента isEmail: true, }, }, }, }); ``` ### Регистрация глобальных валидаторов Валидатором является функция, в которую передается значение соответствующего поля перед записью в базу. Если во время проверки функция возвращает `false`, то выбрасывается стандартная ошибка. Подмена стандартной ошибки возможна с помощью выброса пользовательской ошибки непосредственно внутри проверяющей функции. Регистрация глобального валидатора выполняется методом `addValidator` сервиса `PropertyValidatorRegistry`, который принимает название валидатора и функцию для проверки значения. **Примеры** Регистрация глобального валидатора для проверки формата UUID. ```js import {createError} from 'http-errors'; import {format} from '@e22m4u/js-format'; import {Errorf} from '@e22m4u/js-format'; import {PropertyValidatorRegistry} from '@e22m4u/js-repository'; // получение экземпляра сервиса const pvr = dbs.getService(PropertyValidatorRegistry); // регулярные выражения для разных версий UUID const uuidRegex = { any: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, v4: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, }; // регистрация глобального валидатора "isUuid", // принимающего объект настроек со свойством "version" pvr.addValidator('isUuid', (value, options, context) => { // value - проверяемое значение; // options - параметры валидатора; // context - информация о проверяемом свойстве; console.log(options); // { // version: 'v4' // } console.log(context); // { // validatorName: 'isUuid', // modelName: 'device', // propName: 'deviceId' // } // пустые значения не передаются в валидаторы // (условие ниже никогда не сработает) if (typeof value !== 'string') return false; // поиск регулярного выражения для указанной // версии UUID (из параметров валидатора) const version = options?.version || 'any'; const regex = uuidRegex[version]; // если регулярное выражение не найдено, // то выбрасывается внутренняя ошибка if (!regex) throw new Errorf( 'Invalid UUID version %v specified for validator.', version, ); // при неудачной проверке выбрасывается // ошибка 400 BadRequest if (!regex.test(value)) { const versionString = version !== 'any' ? ` (version ${version})` : ''; throw createError(400, format( 'The property %v of the model %v must be a valid UUID%s.', context.propName, context.modelName, versionString, )); } // при успешной проверке возвращается true, // в противном случае выбрасывается стандартная // ошибка проверки return true; }); ``` Использование глобального валидатора в определении свойства. ```js // определение модели "device" dbs.defineModel({ name: 'device', properties: { deviceId: { type: DataType.STRING, required: true, validate: { // значение {version: 'v4'} будет передаваться // вторым аргументом в функцию-валидатор isUuid: {version: 'v4'}, }, }, }, }); ``` ### Локальные валидаторы Функция-валидатор может быть передана непосредственно в определении свойства без предварительной регистрации. Для этого достаточно передать функцию в параметр `validate` в качестве значения или элемента массива наряду с другими валидаторами. **Примеры** Использование локального валидатора для проверки сложности пароля. ```js // валидатор `passwordStrength` проверяет сложность пароля function passwordStrength(value, options, context) { // value - проверяемое значение; // options - не используется; // context - информация о свойстве; console.log(context); // { // validatorName: 'passwordStrength', // modelName: 'user', // propName: 'password' // } const errors = []; if (value.length < 8) errors.push('must be at least 8 characters long'); if (!/\d/.test(value)) errors.push('must contain at least one number'); if (!/[a-zA-Z]/.test(value)) errors.push('must contain at least one letter'); // если одно из условий сработало, // то выбрасывается ошибка if (errors.length > 0) throw createError(400, format( 'Value of the property %v of the model %v %s.', context.propName, context.modelName, errors.join(', '), )); // при успешной проверке возвращается true, // в противном случае выбрасывается стандартная // ошибка проверки return true; } // определение модели "user" dbs.defineModel({ name: 'user', properties: { password: { type: DataType.STRING, required: true, validate: passwordStrength, // <= // или // validate: [passwordStrength, ...] }, }, }); ``` Использование анонимной функции-валидатора для проверки слага. ```js // определение модели "article" dbs.defineModel({ name: 'article', properties: { slug: { type: DataType.STRING, validate: (value) => { const re = /^[a-z0-9]+(-[a-z0-9]+)*$/; return re.test(value); }, }, }, }); ``` ## Трансформеры Трансформеры используются для модификации значения свойства перед проверкой типа и передачей данных в базу. Трансформеры позволяют автоматически очищать или приводить данные к нужному формату. [Пустые значения](#Пустые-значения) не передаются в трансформеры, так как не имеют полезной нагрузки. ### Глобальные трансформеры Модуль поставляется с набором глобальных трансформеров: - `trim` удаление пробельных символов с начала и конца строки; - `toUpperCase` перевод строки в верхний регистр; - `toLowerCase` перевод строки в нижний регистр; Трансформеры указанные ниже находятся в разработке: - `cut` усечение строки или массива до указанной длины, *параметр: `number` - максимальная длина;* - `truncate` усечение строки с добавлением троеточия, *параметр: `number` - максимальная длина;* - `capitalize` перевод первой буквы каждого слова в верхний регистр, *параметр: `{firstWordOnly?: boolean}`;* **Примеры** Использование глобального трансформера. ```js dbs.defineModel({ name: 'user', properties: { username: { type: DataType.STRING, transform: 'toLowerCase', }, }, }); ``` Использование глобальных трансформеров в виде массива. ```js dbs.defineModel({ name: 'user', properties: { firstName: { type: DataType.STRING, transform: [ 'trim', 'capitalize', ], }, }, }); ``` Использование глобальных трансформеров с передачей аргументов. ```js dbs.defineModel({ name: 'article', properties: { annotation: { type: DataType.STRING, transform: { truncate: 200, capitalize: {firstWordOnly: true}, }, }, }, }); ``` Глобальные трансформеры без параметров могут принимать любые аргументы. ```js dbs.defineModel({ name: 'user', properties: { firstName: { type: DataType.STRING, transform: { cut: 60, // так как трансформер "trim" не имеет параметров, // его определение допускает передачу любого значения // в качестве аргумента trim: true, }, }, }, }); ``` ### Регистрация глобальных трансформеров Трансформером является функция, которая принимает значение свойства и возвращает новое значение. Функция может быть как синхронной, так и асинхронной (возвращать `Promise`). Регистрация глобального трансформера выполняется методом `addTransformer` сервиса `PropertyTransformerRegistry`, который принимает название трансформера и саму функцию. **Примеры** Регистрация глобального трансформера для удаления HTML-тегов. ```js import {PropertyTransformerRegistry} from '@e22m4u/js-repository'; // получение экземпляра сервиса const ptr = dbs.getService(PropertyTransformerRegistry); // регистрация глобального трансформера "stripTags" ptr.addTransformer('stripTags', (value, options, context) => { // value - трансформируемое значение; // options - настройки трансформера (если переданы); // context - информация о свойстве; console.log(context); // { // transformerName: 'stripTags', // modelName: 'comment', // propName: 'text' // } if (typeof value !== 'string') return value; // возвращаем как есть, если не строка return value.replace(/<[^>]*>?/gm, ''); }); ``` Использование глобального трансформера в определении модели. ```js dbs.defineModel({ name: 'comment', properties: { text: { type: DataType.STRING, transform: 'stripTags', }, }, }); ``` ### Локальные трансформеры Функция-трансформер может быть передана непосредственно в определении свойства без предварительной регистрации. Для этого достаточно передать функцию в параметр `transform` в качестве значения или элемента массива. **Примеры** Использование локального трансформера для нормализации имен. ```js // функция для нормализации имени function normalizeName(value, options, context) { // value - трансформируемое значение // options - не используется // context - информация о свойстве if (!value || typeof value !== 'string') return value; return value .trim() // удаление пробелов в начале и конце .toLowerCase() // перевод к нижнему регистру .split(' ') // разделение на слова // перевод к верхнему регистру первой буквы каждого слова .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); // сборка массива в строку } // определение модели "user" dbs.defineModel({ name: 'user', properties: { firstName: { type: DataType.STRING, transform: normalizeName, // <= }, lastName: { type: DataType.STRING, transform: normalizeName, // <= }, }, }); ``` Использование локального асинхронного трансформера для хэширования пароля. ```js import * as bcrypt from 'bcrypt'; // асинхронная функция для хеширования значения async function hash(value, options, context) { // value - трансформируемое значение // options - не используется // context - информация о свойстве console.log(context); // { // transformerName: 'hash', // modelName: 'user', // propName: 'password' // } const saltRounds = 10; return bcrypt.hash(value, saltRounds); } // определение модели "user" dbs.defineModel({ name: 'user', properties: { password: { type: DataType.STRING, transform: hash, // <= // или // transform: [hash, ...] }, }, }); ``` Использование анонимной функции-трансформера для коррекции слага. ```js dbs.defineModel({ name: 'article', properties: { slug: { type: DataType.STRING, transform: (value) => { if (typeof value !== 'string') return value; return value.toLowerCase().replace(/\s+/g, '-'); }, }, }, }); ``` ## Пустые значения Разные типы свойств имеют свои наборы пустых значений. Эти наборы используются для определения наличия полезной нагрузки в значении свойства. Например, параметр `default` в определении свойства устанавливает значение по умолчанию, только если входящее значение является пустым. Параметр `required` исключает пустые значения выбрасывая ошибку. А параметр `unique` в режиме `sparse` наоборот допускает дублирование пустых значений уникального свойства, поскольку они не участвуют в проверке. | тип | пустые значения | |-------------|---------------------------| | `'any'` | `undefined`, `null` | | `'string'` | `undefined`, `null`, `''` | | `'number'` | `undefined`, `null`, `0` | | `'boolean'` | `undefined`, `null` | | `'array'` | `undefined`, `null`, `[]` | | `'object'` | `undefined`, `null`, `{}` | ### Переопределение пустых значений Набор пустых значений для любого типа данных можно переопределить. Управление этими наборами осуществляется через специальный сервис, который предоставляет модуль [@e22m4u/js-empty-values](https://www.npmjs.com/package/@e22m4u/js-empty-values) (не требует установки). **EmptyValuesService** Для переопределения пустых значений необходимо получить экземпляр класса `EmptyValuesService` из контейнера схемы и вызвать метод, который принимает тип данных и массив новых значений. Интерфейс: ```ts class EmptyValuesService { /** * Установить пустые значения * для определенного типа данных. * * @param dataType Тип данных. * @param emptyValues Массив новых пустых значений. */ setEmptyValuesOf( dataType: DataType, emptyValues: unknown[], ): this; } ``` **Пример** По умолчанию, для числовых свойств значение `0` считается пустым. Следующий пример демонстрирует, как изменить это поведение, оставив в качестве пустых значений только `undefined` и `null`. ```js import {DataType} from '@e22m4u/js-repository'; import {DatabaseSchema} from '@e22m4u/js-repository'; import {EmptyValuesService} from '@e22m4u/js-empty-values'; const dbs = new DatabaseSchema(); // получение сервиса для работы с пустыми значениями const emptyValuesService = dbs.getService(EmptyValuesService); // переопределение пустых значений для типа DataType.NUMBER emptyValuesService.setEmptyValuesOf(DataType.NUMBER, [undefined, null]); ``` После этого, значение `0` для свойств типа `DataType.NUMBER` больше не будет считаться пустым и будет проходить проверки валидаторами, а также не будет заменяться значением по умолчанию. ## Репозиторий Выполняет операции чтения и записи документов определенной модели. Получить репозиторий можно методом `getRepository` экземпляра `DatabaseSchema`. **Методы** - [`create(data, filter = undefined)`](#repositorycreate) создать новый документ; - [`replaceById(id, data, filter = undefined)`](#repositoryreplacebyid) заменить документ полностью; - [`replaceOrCreate(data, filter = undefined)`](#repositoryreplaceorcreate) заменить или создать новый; - [`patchById(id, data, filter = undefined)`](#repositorypatchbyid) обновить документ частично; - [`patch(data, where = undefined)`](#repositorypatch) обновить все документы или по условию; - [`find(filter = undefined)`](#repositoryfind) найти все документы или по условию; - [`findOne(filter = undefined)`](#repositoryfindone) найти первый документ или по условию; - [`findById(id, filter = undefined)`](#repositoryfindbyid) найти документ по идентификатору; - [`delete(where = undefined)`](#repositorydelete) удалить все документы или по условию; - [`deleteById(id)`](#repositorydeletebyid) удалить документ по идентификатору; - [`exists(id)`](#repositoryexists) проверить существование по идентификатору; - [`count(where = undefined)`](#repositorycount) подсчет всех документов или по условию; **Аргументы** - `id: number|string` идентификатор (первичный ключ) - `data: object` данные документа (используется при записи) - `where: object` условия фильтрации (см. [Фильтрация](#Фильтрация)) - `filter: object` параметры выборки (см. [Фильтрация](#Фильтрация)) Получение репозитория по названию модели. ```js const countryRep = dbs.getRepository('country'); ``` ### repository.create Создает новый документ в коллекции на основе переданных данных. Возвращает созданный документ с присвоенным идентификатором. **Сигнатура:** ```ts create( data: WithOptionalId, filter?: ItemFilterClause, ): Promise; ``` **Примеры** Создание нового документа. ```js const newProduct = await productRep.create({ name: 'Laptop', price: 1200, }); console.log(newProduct); // { // id: 1, // name: 'Laptop', // price: 1200, // } ``` Создание документа с возвратом определенных полей. ```js const product = await productRep.create( {name: 'Mouse', price: 25}, {fields: ['id', 'name']}, ); console.log(product); // { // id: 2, // name: 'Mouse', // } ``` Создание документа с включением связанных данных в результат. ```js // предполагается, что модель Product имеет связь "category" // (опция "include" влияет только на возвращаемый результат) const product = await productRep.create( {name: 'Keyboard', price: 75, categoryId: 10}, {include: 'category'}, ); console.log(product); // { // id: 3, // name: 'Keyboard', // price: 75, // categoryId: 10, // category: {id: 10, name: 'Electronics'} // } ``` ### repository.replaceById Полностью заменяет существующий документ по его идентификатору. Все предыдущие данные документа, кроме идентификатора, удаляются. Поля, которые не были переданы в `data`, будут отсутствовать в итоговом документе (если для них не задано значение по умолчанию). **Сигнатура:** ```ts replaceById( id: IdType, data: WithoutId, filter?: ItemFilterClause, ): Promise; ``` **Примеры** Замена документа по идентификатору. ```js // исходный документ // { // id: 1, // name: 'Laptop', // price: 1200, // inStock: true // } const updatedProduct = await productRep.replaceById(1, { name: 'Laptop Pro', price: 1500, }); console.log(updatedProduct); // { // id: 1, // name: 'Laptop Pro', // price: 1500 // } // свойство "inStock" удалено ``` ### repository.replaceOrCreate Заменяет существующий документ, если в переданных данных присутствует идентификатор, который уже существует в коллекции. В противном случае, если идентификатор не указан или не найден, создает новый документ. **Сигнатура:** ```ts replaceOrCreate( data: WithOptionalId, filter?: ItemFilterClause, ): Promise; ``` **Примеры** Создание нового документа, если `id: 3` не существует. ```js const product = await productRep.replaceOrCreate({ id: 3, name: 'Keyboard', price: 75, }); console.log(product); // { // id: 3, // name: 'Keyboard', // price: 75, // } ``` Замена существующего документа с `id: 1`. ```js const updatedProduct = await productRep.replaceOrCreate({ id: 1, name: 'Laptop Pro', price: 1500, }); console.log(updatedProduct); // { // id: 1, // name: 'Laptop Pro', // price: 1500, // } ``` ### repository.patchById Частично обновляет существующий документ по его идентификатору, изменяя только переданные поля. Остальные поля документа остаются без изменений. **Сигнатура:** ```ts patchById( id: IdType, data: PartialWithoutId, filter?: ItemFilterClause, ): Promise; ``` **Примеры** Частичное обновление документа по идентификатору. ```js // исходный документ с id: 1 // { // id: 1, // name: 'Laptop Pro', // price: 1500 // } const updatedProduct = await productRep.patchById(1, { price: 1450, }); console.log(updatedProduct); // { // id: 1, // name: 'Laptop Pro', // price: 1450 // } ``` ### repository.patch Частично обновляет один или несколько документов, соответствующих условиям `where`. Изменяются только переданные поля, остальные остаются без изменений. Возвращает количество обновленных документов. Если `where` не указан, обновляет все документы в коллекции. **Сигнатура:** ```ts patch( data: PartialWithoutId, where?: WhereClause, ): Promise; ``` **Примеры** Обновление документов по условию. ```js // обновит все товары с ценой меньше 30 const updatedCount = await productRep.patch( {inStock: false}, {price: {lt: 30}}, ); ``` Обновление всех документов. ```js // добавит или обновит поле updatedAt для всех документов const totalCount = await productRep.patch({ updatedAt: new Date(), }); ``` ### repository.find Находит все документы, соответствующие условиям фильтрации, и возвращает их в виде массива. Если фильтр не указан, возвращает все документы коллекции. **Сигнатура:** ```ts find(filter?: FilterClause): Promise; ``` **Примеры** Поиск всех документов. ```js const allProducts = await productRep.find(); ``` Поиск документов по условию `where`. ```js const cheapProducts = await productRep.find({ where: {price: {lt: 100}}, }); ``` Поиск с сортировкой и ограничением выборки. ```js const latestProducts = await productRep.find({ order: 'createdAt DESC', limit: 10, }); ``` ### repository.findOne Находит первый документ, соответствующий условиям фильтрации. Возвращает `undefined`, если документы не найдены. **Сигнатура:** ```ts findOne( filter?: FilterClause, ): Promise; ``` **Примеры** Поиск одного документа по условию. ```js const expensiveProduct = await productRep.findOne({ where: {price: {gt: 1000}}, order: 'price DESC', }); ``` Обработка случая, когда документ не найден. ```js const product = await productRep.findOne({ where: {name: 'Non-existent Product'}, }); if (!product) { console.log('Product not found.'); } ``` ### repository.findById Находит один документ по его уникальному идентификатору. Если документ не найден, выбрасывается ошибка. **Сигнатура:** ```ts findById( id: IdType, filter?: ItemFilterClause, ): Promise; ``` **Примеры** Поиск документа по `id`. ```js try { const product = await productRep.findById(1); console.log(product); } catch (error) { console.error('Product with id 1 is not found.'); } ``` Поиск документа с включением связанных данных. ```js const product = await productRep.findById(1, { include: 'category', }); ``` ### repository.delete Удаляет один или несколько документов, соответствующих условиям `where`. Возвращает количество удаленных документов. Если `where` не указан, удаляет все документы в коллекции. **Сигнатура:** ```ts delete(where?: WhereClause): Promise; ``` **Примеры** Удаление документов по условию. ```js const deletedCount = await productRep.delete({ inStock: false, }); ``` Удаление всех документов. ```js const totalCount = await productRep.delete(); ``` ### repository.deleteById Удаляет один документ по его уникальному идентификатору. Возвращает `true`, если документ был найден и удален, в противном случае `false`. **Сигнатура:** ```ts deleteById(id: IdType): Promise; ``` **Примеры** Удаление документа по `id`. ```js const wasDeleted = await productRep.deleteById(1); if (wasDeleted) { console.log('The document was deleted.'); } else { console.log('No document found to delete.'); } ``` ### repository.exists Проверяет существование документа с указанным идентификатором. Возвращает `true`, если документ существует, иначе `false`. **Сигнатура:** ```ts exists(id: IdType): Promise; ``` **Примеры** Проверка существования документа по `id`. ```js const productExists = await productRep.exists(1); if (productExists) { console.log('A document with id 1 exists.'); } ``` ### repository.count Подсчитывает количество документов, соответствующих условиям `where`. Если `where` не указан, возвращает общее количество документов в коллекции. **Сигнатура:** ```ts count(where?: WhereClause): Promise; ``` **Примеры** Подсчет документов по условию. ```js const cheapCount = await productRep.count({ price: {lt: 100}, }); ``` Подсчет всех документов. ```js const totalCount = await productRep.count(); ``` ## Фильтрация Некоторые методы репозитория принимают объект настроек влияющий на возвращаемый результат. Максимально широкий набор таких настроек имеет первый параметр метода `find`, где ожидается объект содержащий набор опций указанных ниже. - `where: object` условия фильтрации по свойствам документа; - `order: string|string[]` сортировка по указанным свойствам; - `limit: number` ограничение количества документов; - `skip: number` пропуск документов (пагинация); - `fields: string|string[]` выбор необходимых свойств модели; - `include: object` включение связанных данных в результат; **Пример** ```js // для запроса используется метод репозитория "find" // с передачей объекта фильтрации первым аргументом const news = await newsRepository.find({ where: { title: {like: '%Moscow%'}, publishedAt: {gte: '2025-10-15T00:00:00.000Z'}, tags: {inq: ['world', 'politic']}, hidden: false, }, order: 'publishedAt DESC', limit: 12, skip: 24, fields: ['title', 'annotation', 'body'], include: ['author', 'category'], }) ``` ### where Параметр принимает объект с условиями выборки и поддерживает следующий набор операторов сравнения. - [Поиск по значению](#поиск-по-значению-сокращенная-форма) - [`eq`](#eq-строгое-равенство) (строгое равенство) - [`neq`](#neq-неравенство) (неравенство) - [`gt`](#gt-больше-чем) (больше чем) - [`lt`](#lt-меньше-чем) (меньше чем) - [`gte`](#gte-больше-или-равно) (больше или равно) - [`lte`](#lte-меньше-или-равно) (меньше или равно) - [`inq`](#inq-в-списке) (в списке) - [`nin`](#nin-не-в-списке) (не в списке) - [`between`](#between-диапазон) (диапазон) - [`exists`](#exists-наличие-свойства) (наличие свойства) - [`like`](#like-шаблон) (шаблон) - [`nlike`](#nlike-исключающий-шаблон) (исключающий шаблон) - [`ilike`](#ilike-регистронезависимый-шаблон) (регистронезависимый шаблон) - [`nilike`](#nilike-регистронезависимый-шаблон-исключения) (регистронезависимый шаблон исключения) - [`regexp`](#regexp-регулярное-выражение) (регулярное выражение) Условия можно объединять логическими операторами: - [`and`](#and-логическое-и) (логическое И) - [`or`](#or-логическое-или) (логическое ИЛИ) #### Поиск по значению (сокращенная форма) Находит документы, у которых значение указанного свойства в точности равно переданному значению. Это сокращенная запись для оператора `{eq: ...}`. ```js // найдет все документы, где age равен 21 const res = await rep.find({ where: { age: 21, }, }); ``` #### `eq` (строгое равенство) Находит документы, у которых значение свойства равно указанному. ```js // найдет все документы, где age равен 21 const res = await rep.find({ where: { age: {eq: 21}, }, }); ``` #### `neq` (неравенство) Находит документы, у которых значение свойства не равно указанному. ```js // найдет все документы, где age не равен 21 const res = await rep.find({ where: { age: {neq: 21}, }, }); ``` #### `gt` (больше чем) Находит документы, у которых значение свойства строго больше указанного. ```js // найдет документы, где age больше 30 const res = await rep.find({ where: { age: {gt: 30}, }, }); ``` #### `lt` (меньше чем) Находит документы, у которых значение свойства строго меньше указанного. ```js // найдет документы, где age меньше 30 const res = await rep.find({ where: { age: {lt: 30}, }, }); ``` #### `gte` (больше или равно) Находит документы, у которых значение свойства больше или равно указанному. ```js // найдет документы, где age больше или равен 30 const res = await rep.find({ where: { age: {gte: 30}, }, }); ``` #### `lte` (меньше или равно) Находит документы, у которых значение свойства меньше или равно указанному. ```js // найдет документы, где age меньше или равен 30 const res = await rep.find({ where: { age: {lte: 30}, }, }); ``` #### `inq` (в списке) Находит документы, у которых значение свойства совпадает с одним из значений в предоставленном массиве. ```js // найдет документы, где name - 'John' или 'Mary' const res = await rep.find({ where: { name: {inq: ['John', 'Mary']}, }, }); ``` #### `nin` (не в списке) Находит документы, у которых значение свойства отсутствует в предоставленном массиве. ```js // найдет все документы, кроме тех, где name - 'John' или 'Mary' const res = await rep.find({ where: { name: {nin: ['John', 'Mary']}, }, }); ``` #### `between` (диапазон) Находит документы, у которых значение свойства находится в указанном диапазоне (включая границы). ```js // найдет документы, где age находится в диапазоне от 20 до 30 включительно const res = await rep.find({ where: { age: {between: [20, 30]}, }, }); ``` #### `exists` (наличие свойства) Проверяет наличие или отсутствие свойства в документе. Не проверяет значение свойства. - `true` свойство должно существовать (даже если его значение `null`); - `false` свойство должно отсутствовать; ```js // найдет документы, у которых есть свойство 'nickname' const res1 = await rep.find({ where: { nickname: {exists: true}, }, }); // найдет документы, у которых нет свойства 'nickname' const res2 = await rep.find({ where: { nickname: {exists: false}, }, }); ``` #### `like` (шаблон) Выполняет сопоставление с шаблоном, с учетом регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)). ```js // найдет {name: 'John Doe'}, но не {name: 'john doe'} const res = await rep.find({ where: { name: {like: 'John%'}, }, }); ``` #### `nlike` (исключающий шаблон) Находит документы, которые не соответствуют шаблону, с учетом регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)). ```js // найдет все, кроме тех, что начинаются на 'John' const res = await rep.find({ where: { name: {nlike: 'John%'}, }, }); ``` #### `ilike` (регистронезависимый шаблон) Выполняет сопоставление с шаблоном без учета регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)). ```js // найдет {name: 'John Doe'} и {name: 'john doe'} const res = await rep.find({ where: { name: {ilike: 'john%'}, }, }); ``` #### `nilike` (регистронезависимый шаблон исключения) Находит строки, которые не соответствуют шаблону, без учета регистра (см. [подробнее](#операторы-сопоставления-с-шаблоном)). ```js // найдет все, кроме тех, что начинаются на 'John' или 'john' const res = await rep.find({ where: { name: {nilike: 'john%'}, }, }); ``` #### `regexp` (регулярное выражение) Находит документы, у которых значение строкового свойства соответствует указанному регулярному выражению. Может быть передано в виде строки или объекта `RegExp`. ```js // найдет документы, где name начинается с 'J' const res1 = await rep.find({ where: { name: {regexp: '^J'}, }, }); // найдет документы, где name начинается с 'J' или 'j' (регистронезависимо) const res2 = await rep.find({ where: { name: {regexp: '^j', flags: 'i'}, }, }); ``` #### `and` (логическое И) Объединяет несколько условий в массив, требуя, чтобы каждое условие было выполнено. ```javascript // найдет документы, где surname равен 'Smith' И age равен 21 const res = await rep.find({ where: { and: [ {surname: 'Smith'}, {age: 21} ], }, }); ``` #### `or` (логическое ИЛИ) Объединяет несколько условий в массив, требуя, чтобы хотя бы одно из них было выполнено. ```javascript // найдет документы, где name равен 'James' ИЛИ age больше 30 const res = await rep.find({ where: { or: [ {name: 'James'}, {age: {gt: 30}} ], }, }); ``` #### Операторы сопоставления с шаблоном Операторы `like`, `nlike`, `ilike`, `nilike` предназначены для фильтрации строковых свойств на основе сопоставления с шаблоном, подобно оператору `LIKE` в SQL. Они позволяют находить значения, которые соответствуют определённой структуре, используя специальные символы. **`%`** соответствует любой последовательности из нуля или более символов: - `'А%'` найдет все строки, начинающиеся на "А"; - `'%а'` найдет все строки, заканчивающиеся на "а"; - `'%слово%'` найдет все строки, содержащие "слово" в любом месте; **`_`** соответствует ровно одному любому символу: - `'к_т'` найдет "кот", "кит", но не "крот" или "кт"; - `'кот_'` найдет "коты", "коту" и "кота", но не "кот" или "котов"; Если нужно найти сами символы `%` или `_` как часть строки, их необходимо экранировать с помощью обратного слэша `\`: - `'100\%'` найдет строку "100%"; - `'file\_name'` найдет строку "file_name"; - `'path\\to'` найдет строку "path\to"; ### 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" }, }, }); ``` ## Связи Связи позволяют описывать отношения между моделями, что дает возможность автоматически встраивать связанные данные с помощью опции `include` в методах репозитория. Ниже приводится пример автоматического разрешения связи при использовании метода `findById`. ```js // запрос документа коллекции "users", // включая связанные данные (role и city) const user = await userRep.findById(1, { include: ['role', 'city'], }); console.log(user); // { // id: 1, // name: 'John Doe', // roleId: 3, // role: { // id: 3, // name: 'Manager' // }, // cityId: 24, // city: { // id: 24, // name: 'Moscow' // } // } ``` ### Определение связи Свойство `relations` в определении модели принимает объект, ключи которого являются названиями связей, а значения их параметрами. В дальнейшем название связи можно будет использовать в опции `include` методах репозитория. ```js import {DataType} from '@e22m4u/js-repository'; import {RelationType} from '@e22m4u/js-repository'; dbs.defineModel({ name: 'user', datasource: 'memory', properties: { name: DataType.STRING, }, relations: { // связь role -> параметры role: { type: RelationType.BELONGS_TO, model: 'role', }, // связь city -> параметры city: { type: RelationType.BELONGS_TO, model: 'city', }, }, }); ``` **Основные параметры** - `type: string` тип связи (обязательно); - `model: string` название целевой модели (обязательно для некоторых типов); - `foreignKey: string` свойство текущей модели для идентификатора цели; *i. Для типов Belongs To и References Many значение параметра `foreignKey` можно опустить, так как генерируется автоматически по названию связи.* **Полиморфный режим** - `polymorphic: boolean|string` объявление полиморфной связи; - `discriminator: string` свойство текущей модели для названия цели; *i. Полиморфный режим позволяет динамически определять целевую модель по ее названию, которое хранит документ в свойстве-дискриминаторе.* **Типы связи** - `belongsTo` - текущая модель ссылается на целевую по идентификатору; - `hasOne` - обратная сторона `belongsTo` по принципу "один к одному"; - `hasMany` - обратная сторона `belongsTo` по принципу "один ко многим"; - `referencesMany` - модель ссылается через массив идентификаторов; Параметр `type` в определении связи принимает строку с названием типа. Чтобы исключить опечатку, рекомендуется использовать константы объекта `RelationType` указанные ниже. - `RelationType.BELONGS_TO` - `RelationType.HAS_ONE` - `RelationType.HAS_MANY` - `RelationType.REFERENCES_MANY` **Примеры** Объявление связи `belongsTo`. ```js dbs.defineModel({ name: 'user', relations: { role: { // название связи type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую model: 'role', // название целевой модели foreignKey: 'roleId', // внешний ключ (необязательно) // если "foreignKey" не указан, то свойство внешнего // ключа формируется согласно названию связи // с добавлением постфикса "Id" }, }, }); ``` Объявление связи `hasMany`. ```js dbs.defineModel({ name: 'role', relations: { users: { // название связи type: RelationType.HAS_MANY, // целевая модель ссылается на текущую model: 'user', // название целевой модели foreignKey: 'roleId', // внешний ключ из целевой модели на текущую }, }, }); ``` Объявление связи `referencesMany`. ```js dbs.defineModel({ name: 'article', relations: { categories: { // название связи type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов model: 'category', // название целевой модели foreignKey: 'categoryIds', // внешний ключ (необязательно) // если "foreignKey" не указан, то свойство внешнего // ключа формируется согласно названию связи // с добавлением постфикса "Ids" }, }, }); ``` Полиморфная версия `belongsTo`. ```js dbs.defineModel({ name: 'file', relations: { reference: { // название связи type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую // полиморфный режим позволяет хранить название целевой модели // в свойстве-дискриминаторе, которое формируется согласно // названию связи с постфиксом "Type", и в данном случае // название целевой модели хранит "referenceType", // а идентификатор документа "referenceId" polymorphic: true, }, }, }); ``` Полиморфная версия `belongsTo` с указанием свойств. ```js dbs.defineModel({ name: 'file', relations: { reference: { // название связи type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую polymorphic: true, // название целевой модели хранит дискриминатор foreignKey: 'referenceId', // свойство для идентификатора цели discriminator: 'referenceType', // свойство для названия целевой модели }, }, }); ``` Полиморфная версия `hasMany` с указанием названия связи целевой модели. ```js dbs.defineModel({ name: 'letter', relations: { attachments: { // название связи type: RelationType.HAS_MANY, // целевая модель ссылается на текущую model: 'file', // название целевой модели polymorphic: 'reference', // название полиморфной связи целевой модели }, }, }); ``` Полиморфная версия `hasMany` с указанием свойств целевой модели. ```js dbs.defineModel({ name: 'letter', relations: { attachments: { // название связи type: RelationType.HAS_MANY, // целевая модель ссылается на текущую model: 'file', // название целевой модели polymorphic: true, // название текущей модели находится в дискриминаторе foreignKey: 'referenceId', // свойство целевой модели для идентификатора discriminator: 'referenceType', // свойство целевой модели для названия текущей }, }, }); ``` ## Расширение Метод `getRepository` экземпляра `DatabaseSchema` проверяет наличие существующего репозитория для указанной модели и возвращает его. В противном случае создается новый экземпляр, который будет сохранен для последующих обращений к методу. ```js import {Repository} from '@e22m4u/js-repository'; import {DatabaseSchema} from '@e22m4u/js-repository'; // const dbs = new DatabaseSchema(); // dbs.defineDatasource ... // dbs.defineModel ... const rep1 = dbs.getRepository('model'); const rep2 = dbs.getRepository('model'); console.log(rep1 === rep2); // true ``` Подмена стандартного конструктора репозитория выполняется методом `setRepositoryCtor` сервиса `RepositoryRegistry`, который находится в сервис-контейнере экземпляра `DatabaseSchema`. После чего все новые репозитории будут создаваться указанным конструктором вместо стандартного. ```js import {Repository} from '@e22m4u/js-repository'; import {DatabaseSchema} from '@e22m4u/js-repository'; import {RepositoryRegistry} from '@e22m4u/js-repository'; class MyRepository extends Repository { /*...*/ } // const dbs = new DatabaseSchema(); // dbs.defineDatasource ... // dbs.defineModel ... dbs.getService(RepositoryRegistry).setRepositoryCtor(MyRepository); const rep = dbs.getRepository('model'); console.log(rep instanceof MyRepository); // true ``` *i. Так как экземпляры репозитория кэшируется, то замену конструктора следует выполнять до обращения к методу `getRepository`.* ## TypeScript Получение типизированного репозитория с указанием интерфейса модели. ```ts import {DataType} from '@e22m4u/js-repository'; import {RelationType} from '@e22m4u/js-repository'; import {DatabaseSchema} from '@e22m4u/js-repository'; // const dbs = new DatabaseSchema(); // dbs.defineDatasource ... // dbs.defineModel ... // определение модели "city" dbs.defineModel({ name: 'city', datasource: 'myDatasource', properties: { name: DataType.STRING, timeZone: DataType.STRING, }, relations: { country: { type: RelationType.BELONGS_TO, model: 'country', }, }, }); // определение интерфейса "city" interface City { id: number; name?: string; timeZone?: string; countryId?: number; country?: Country; } // получаем репозиторий по названию модели // указывая ее тип и тип идентификатора const cityRep = dbs.getRepository('city'); ``` ## Тесты ```bash npm run test ``` ## Лицензия MIT