## @e22m4u/js-repository Абстракция для работы с базами данных для Node.js B[Модель A]-->D[Репозиторий А] A[Источник данных]-->C[Модель Б]-->E[Репозиторий Б] ``` ## Пример Перед началом работы с коллекцией нужно определить источник и модель данных. Определения хранятся в экземпляре класса `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` - получить репозиторий Далее определяется источник и выбранный адаптер. Количество источников зависит от числа используемых баз данных. ```js schema.defineDatasource({ name: 'myMemory', // название нового источника adapter: 'memory', // выбранный адаптер }); ``` Теперь добавим модель отражающую структуру документа. Представим коллекцию `city` с двумя полями, где строковое поле `name` в качестве названия города, и `population` как численность населения. ```js schema.defineModel({ name: 'city', // название модели datasource: 'myMemory', // выбранный источник properties: { name: DataType.STRING, // поле типа "string" population: DataType.NUMBER, // поле типа "number" }, }); ``` Модель гарантирует, что тип указанных полей будет проверен перед записью в коллекцию. Осталось получить репозиторий данной модели для чтения и записи документов. ```js // получаем репозиторий модели "city" const cityRep = schema.getRepository('city'); // добавляем новый документ const city = await cityRep.create({ name: 'Moscow', population: 11980000, }); // выводим результат console.log(city); // { // "id": 1, // "name": "Moscow", // "population": 11980000 // } ``` Последующие обращения к методу `getRepository` с названием модели `city` будут возвращать все тот же экземпляр репозитория данной модели по принципу «одиночки». ## Источник данных Каждый источник имеет свое уникальное название и выбранный адаптер. Экземпляр класса `Schema` имеет метод `defineDatasource` с помощью которого объявляется новый источник. ```js import {Schema} from '@e22m4u/js-repository'; const schema = new Schema(); schema.defineDatasource({ name: 'datasource1', // название нового источника adapter: 'memory', // выбранный адаптер }); ``` Если адаптер имеет параметры, то они передаются с объектом определения источника. ```js schema.defineDatasource({ name: 'datasource2', adapter: 'mongodb', // настройки адаптера "mongodb" host: '127.0.0.1', port: 27017, database: 'data' }); ``` Параметры источника: - `name: string` уникальное название - `adapter: string` выбранный адаптер - параметры адаптера (если имеются) **Адаптер «memory»** Встроенный адаптер `memory` хранит данные в памяти процесса, не требует установки и отлично подходит для тестов и прототипирования. ```js import {Schema} from '@e22m4u/js-repository'; const schema = new Schema(); schema.defineDatasource({ name: 'myMemory', adapter: 'memory', // адаптер "memory" }); schema.defineModel({ name: 'fruit', datasource: 'myMemory', properties: { name: DataType.STRING, description: DataType.STRING, }, }); const fruitRep = schema.getRepository('fruit'); const pineapple = fruitRep.create({ name: 'Pineapple', description: 'A tropical fruit' }); console.log(pineapple); // { // "id": 1, // "name": "Pineapple", // "description": "A tropical fruit" // } ``` Адаптер `memory` использует автоинкрементные идентификаторы типа `number`, но может принимать и строковые ключи, если их значение явно указано в документе. ```js // ... const banana = fruitRep.create({ id: 'fruit-1', // ключ типа "string" name: 'Banana', description: 'An edible fruit' }); console.log(banana); // { // "id": "fruit-1", // "name": 'Banana', // "description": "An edible fruit" // } ``` ## Модель данных Модель описывает структуру документа определенной коллекции. Экземпляр класса `Schema` имеет метод `defineModel` с помощью которого объявляется новая модель. ```js import {Schema} from '@e22m4u/js-repository'; const schema = new Schema(); // объявление модели "region" schema.defineModel({ name: 'region', // название модели properties: { // поля модели name: DataType.STRING, population: DataType.NUMBER, }, }); // документ "region" может выглядеть так // { // "id": 1, // "name": "Asia", // "population": 4753079727 // } ``` Параметры модели: - `name: string` название модели (обязательно) - `base: string` название наследуемой модели - `tableName: string` название коллекции в базе - `datasource: string` выбранный источник данных - `properties: object` определения полей модели - `relations: object` определения связей (см. [Связи](#Связи)) **Название модели** Название должно быть уникальным, так как оно используется для определения связей и соответствует названию коллекции в базе. При необходимости можно явно задать название коллекции параметром `tableName`, если оно не соответствует названию модели. ```js schema.defineModel({ name: 'city', // название модели tableName: 'CITIES', // название коллекции }); ``` **Источник данных** Если модель отражает реальную коллекцию базы, а не является частью другой, то указав название источника в параметре `datasource` можно получить репозиторий модели методом `getRepository` по ее названию. ```js // модель "city" schema.defineModel({ name: 'city', datasource: 'myDatasource', // выбранный источник }); // репозиторий модели "city" const cityRep = schema.getRepository('city'); // await cityRep.create({name: 'St. Petersburg'}); // ... ``` Параметры поля: - `type: string` тип допустимого значения (обязательно) - `itemType: string` тип элемента массива (для `type: 'array'`) - `model: string` модель объекта (для `type: 'object'`) - `primaryKey: boolean` объявить поле первичным ключом - `columnName: string` переопределение названия колонки - `columnType: string` тип колонки (определяется адаптером) - `required: boolean` объявить поле обязательным - `default: any` значение по умолчанию Типы данных: - `DataType.ANY` разрешено любое значение - `DataType.STRING` только значение типа `string` - `DataType.NUMBER` только значение типа `number` - `DataType.BOOLEAN` только значение типа `boolean` - `DataType.ARRAY` только значение типа `array` - `DataType.OBJECT` только значение типа `object` **Поля документа** Параметр `properties` принимает объект, ключи которого являются именами полей, а значением тип поля или объект с дополнительными параметрами. Эти настройки используются для проверки данных перед сохранением в базу и установки значений по умолчанию (если имеются). ```js schema.defineModel({ name: 'city', properties: { name: DataType.STRING, // поле "name" типа "string" population: { // поле "population" типа "number" type: DataType.NUMBER, default: 0, // значение по умолчанию }, }, }); ``` Когда требуется указать значение по умолчанию, установить флаг `required` или объявить более сложный тип допустимого значения, то вместо базового определения используется объект с расширенными настройками. ```js schema.defineModel({ name: 'place', properties: { // базовое определение поля "name" name: DataType.STRING, // расширенное определение поля "location" // с дополнительными параметрами "model" // и "required" location: { type: DataType.OBJECT, model: 'latLng', required: true, }, // расширенное определение поля "keywords" // с дополнительными параметрами "itemType" // и "default" keywords: { type: DataType.ARRAY, itemType: DataType.STRING, default: () => [], }, }, }); ``` **Наследование** Модель может наследовать поля и связи используя параметр `base`, куда передается название базовой модели. При этом наследуемые настройки можно переопределять не затрагивая родителя. ```js // модель "area" schema.defineModel({ name: 'area', properties: { name: DataType.STRING, population: DataType.NUMBER, }, }); // модель "city" schema.defineModel({ name: 'city', base: 'area', // базовая модель properties: { timezone: DataType.STRING, }, }); // документ "city" может выглядеть так // { // "id": 1, // "name": "Moscow", <= унаследовано // "population": 11980000, <= унаследовано // "timezone": "Europe/Moscow" // } ``` ## Репозиторий Объявление модели имеет необязательный параметр `datasource`, который указывает на источник хранения данных. Наличие источника позволяет получить репозиторий по названию такой модели. Репозиторий, в свою очередь, предоставляет интерфейс для чтения и записи данных этой модели. ```js import {Schema} from '@e22m4u/js-repository'; const schema = new Schema(); // объявление источника "myMemory" schema.defineDatasource({ name: 'myMemory', adapter: 'memory', }); // объявление модели "city" schema.defineModel({ name: 'city', datasource: 'myMemory', // источник данных properties: { name: DataType.STRING, population: DataType.NUMBER, }, }); // получение репозитория модели "city" const cityRep = schema.getRepository('city'); // записываем новый документ "city" const city = await cityRep.create({ name: 'Moscow', population: 11980000 }); // выводим результат console.log(city); // { // "id": 1, // "name": "Moscow", // "population": 11980000 // } ``` #### Методы - `create(data, filter = undefined)` добавить новый документ - `replaceById(id, data, filter = undefined)` заменить весь документ - `patchById(id, data, filter = undefined)` частично обновить документ - `patch(data, where = undefined)` обновить все документы или по условию - `find(filter = undefined)` найти все документы или по условию - `findOne(filter = undefined)` найти первый документ или по условию - `findById(id, filter = undefined)` найти документ по идентификатору - `delete(where = undefined)` удалить все документы или по условию - `deleteById(id)` удалить документ по идентификатору - `exists(id)` проверить существование по идентификатору - `count(where = undefined)` подсчет всех документов или по условию #### Параметры - `id` идентификатор (первичный ключ) - `data` объект отражающий состав документа - `where` параметры выборки (см. Фильтрация) - `filter` параметры возвращаемого результата (см. Фильтрация) ## Фильтрация Некоторые методы репозитория опционально принимают параметр `where` для условий выборки и/или параметр `filter` управляющий возвращаемым значением. ### where Рассмотрим применение параметра `where` на примере метода `patch`, который выполняет поиск и обновление документов по определенному условию. Сигнатура метода `patch(data, where = undefined)` указывает на два параметра, где первый принимает объект данных, а второй является опциональным объектом выборки. ```js // вызов метода `patch` await rep.patch( // обновить значения полей // "hidden" и "updatedAt" { hidden: true, updatedAt: new Date().toISOString(), }, // только в тех документах, где поле "type" // равно значению "article", а "publishedAt" // содержит дату более раннюю, чем указана // в операторе "lte" { type: 'article', publishedAt: { lte: '2023-12-02T14:00:00.000Z', } }, ); ``` Второй аргумент метода `patch` предыдущего листинга содержит условия выборки по которым будет выполнен поиск и обновление документов. Полный список возможных операторов сравнения приводится ниже. `{foo: 'bar'}` поиск по значению поля `foo` `{foo: {eq: 'bar'}}` оператор равенства `eq` `{foo: {neq: 'bar'}}` оператор неравенства `neq` `{foo: {gt: 5}}` оператор "больше" `gt` `{foo: {lt: 10}}` оператор "меньше" `lt` `{foo: {gte: 5}}` оператор "больше или равно" `gte` `{foo: {lte: 10}}` оператор "меньше или равно" `lte` `{foo: {inq: ['bar', 'baz']}}` равенство одного из значений `inq` `{foo: {nin: ['bar', 'baz']}}` исключение значений массива `nin` `{foo: {between: [5, 10]}}` оператор диапазона `between` `{foo: {exists: true}}` оператор наличия значения `exists` `{foo: {like: 'bar'}}` оператор поиска подстроки `like` `{foo: {ilike: 'BaR'}}` регистронезависимая версия `ilike` `{foo: {nlike: 'bar'}}` оператор исключения подстроки `nlike` `{foo: {nilike: 'BaR'}}` регистронезависимая версия `nilike` `{foo: {regexp: 'ba.+'}}` оператор регулярного выражения `regexp` `{foo: {regexp: 'ba.+', flags: 'i'}}` флаги регулярного выражения ### filter Методы репозитория асинхронны и возвращают `Promise` с некоторым значением. Параметр `filter` позволяет влиять на это значение более широким образом, чем просто ограничение для затрагиваемых данных. Ниже представлен список опциональных полей объекта `filter`, одним из которых является `where` (рассматривался ранее). - `where` объект выборки - `order` указание порядка - `limit` ограничение количества документов - `skip` пропуск документов - `fields` выбор необходимых полей документа - `include` включение связанных данных в результат (см. Связи) #### order Документы могут быть отсортированы по указанным полям и в нужном направлении, где `ASC` - по умолчанию, и `DESC` - в обратном порядке. ```js // на примере метода "find" const result = await rep.find({ // упорядочить по полю "featured" order: 'featured', // по полю "featured" в обратном порядке order: 'featured DESC', // по нескольким полям в разных направлениях order: ['featured ASC', 'publishedAt DESC', 'id'], }); ``` #### limit и skip При использовании метода `find` может потребоваться комбинация параметров `limit` и `skip` для механизма пагинации. ```js // первый параметр метода `find` принимает // объект настроек возвращаемого результата const result = await rep.find({ limit: 12, // вернуть не более 14и документов skip: 24, // пропустить 24 документа выборки }); ``` #### fields Сократить объем документа можно параметром `fields` указав необходимый набор полей. ```js // на примере метода "find" const result = await rep.find({ // включить в результат только поле "title" fields: 'title', // или поле "title", "createdAt" и "featured" fields: ['title', 'createdAt', 'featured'], // первичный ключ указывать не обязательно }); ``` ## Связи WIP ```js // коллекция имеет три документа // [ // { // "id": 1, // "type": "city", // "name": "Bangkok", // "hidden": true // }, // { // "id": 2, // "type": "country", // "name": "Thailand", // "hidden": true // }, // { // "id": 3, // "type": "city", // "name": "Moscow", // "hidden": true // } // ] // вызов метода `patch` с передачей // значений для обновляемых полей const result = await rep.patch({ hidden: false, updatedAt: new Date().toISOString(), }); // вывод количество затронутых документов console.log(result); // 3 ``` #### create(data, filter = undefined) Добавляет новый документ в коллекцию и возвращает его. ```js const result = await rep.create({ name: 'Rick Sanchez', dimension: 'C-137', age: 67, }); // вывод результата console.log(result); // { // id: 1, <= определено базой данных // name: 'Rick Sanchez', // dimension: 'C-137', // age: 67 // } ``` Ограничение количества возвращаемых полей. ```js const result = await rep.create( // состав нового документа { name: 'Rick Sanchez', dimension: 'C-137', age: 67, }, // параметры возвращаемого результата { fields: [ 'name', 'age', ] } ); // вывод результата console.log(result); // { // id: 1, // name: 'Rick Sanchez', // age: 67 // } ``` Использование второго параметра `filter` метода `create` позволяет ограничивать количество возвращаемых данных, и разрешать связи к другим коллекциям (см. Связи). ```js // подготовка состава для нового документа const data = { name: 'Rick Sanchez', dimension: 'C-137', age: 67, biographyId: 59, // связь "biography" (см. belongsTo) pictureIds: [345, 346], // связь "pictures" (см. referencesMany) } // подготовка параметра "filter" const filter = { // "fields" - если определено, то результат // будут включать только указанные поля fields: [ 'name', 'biographyId' 'pictureIds', ], // "include" - включение в результат // связанных документов (см. Связи) include: [ 'biography', 'pictures', ], } // вызов метода `create` и вывод результата const result = await rep.create(data, filter); console.log(result); // { // "name": "Rick Sanchez", // "biographyId": 59, // "biography": { <= разрешение связи // "id": 59, // "annotation": "This article is about Rick Sanchez", // "body": "He is a genius scientist whose ..." // } // "pictureIds": [345, 346], // "pictures": [ <= разрешение связи // { // "id": 345, // "mime": "image/jpeg", // "file": "/uploads/rick345.jpg" // }, // { // "id": 346, // "mime": "image/jpeg", // "file": "/uploads/rick346.jpg" // } // ], // } // // поля "age" и "dimension" исключены // из результата опцией "fields" // // документы "biography" и "pictures" // встроены в результат опцией "include" ``` #### replaceById(id, data, filter = undefined) Заменяет существующий документ по идентификатору и возвращает его. Если идентификатор не найден, то выбрасывает исключение. ```js // документ с идентификатором 12 // имеет следующую структуру // { // id: 12, // name: 'Rick Sanchez', // dimension: 'C-137', // age: 67 // } // вызов метода `replaceById` с передачей // идентификатора и нового состава const result = await rep.replaceById(12, { name: 'Morty Smith', kind: 'a young teenage boy', age: 14, }); // вывод результата console.log(result); // { // id: 12, // name: 'Morty Smith', <= обновлено // kind: 'a young teenage boy', <= новое поле // age: 14 <= обновлено // } // // поле "dimension" удалено, так как // не передавалось с новым составом ``` Использование третьего параметра `filter` метода `replaceById` позволяет ограничивать количество возвращаемых данных, и разрешать связи к другим коллекциям (см. Связи). ```js // подготовка состава для заменяемого документа const data = { name: 'Morty Smith', kind: 'a young teenage boy', age: 14, biographyId: 61, // связь "biography" (см. belongsTo) pictureIds: [347, 348], // связь "pictures" (см. referencesMany) } // подготовка параметра "filter" const filter = { // "fields" - если определено, то результат // будут включать только указанные поля fields: [ 'name', 'biographyId' 'pictureIds', ], // "include" - включение в результат // связанных документов (см. Связи) include: [ 'biography', 'pictures', ], } // вызов метода `replaceById` и вывод результата const result = await rep.replaceById(12, data, filter); console.log(result); // { // "name": "Morty Smith", // "biographyId": 61, <= новое поле // "biography": { <= разрешение связи // "id": 61, // "annotation": "This article is about Morty Smith", // "body": "Currently, Morty is 14 years old ..." // }, // "pictureIds": [347, 348], <= новое поле // "picture": [ <= разрешение связи // { // "id": 347, // "mime": "image/jpeg", // "file": "/uploads/morty347.jpg" // }, // { // "id": 348, // "mime": "image/jpeg", // "file": "/uploads/morty348.jpg" // } // ] // } // // поля "kind" и "age" записаны в документ, // но исключены из результата опцией "fields" // // документы "biography" и "pictures" // встроены в результат опцией "include" ``` #### 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 <= новое поле // } ``` Использование третьего параметра `filter` метода `patchById` позволяет ограничивать количество возвращаемых данных, и разрешать связи к другим коллекциям (см. Связи). ```js // подготовка обновляемых данных const data = { name: 'Sheremetyevo Airport', code: 'SVO', featured: true, cityId: 231, // связь "city" (см. belongsTo) companyIds: [513, 514], // связь "companies" (см. referencesMany) } // подготовка параметра `filter` const filter = { // "fields" - если определено, то результат // будут включать только указанные поля fields: [ 'name', 'cityId', 'companyIds', ], // "include" - включение в результат // связанных документов (см. Связи) include: [ 'city', 'companies', ], } // вызов метода `patchById` и вывод результата const result = await rep.patchById(24, data, filter); console.log(result); // { // "id": 24, // "name": "Sheremetyevo Airport", <= обновлено // "cityId": 231, <= новое поле // "city": { <= разрешение связи // "id": 231, // "name": "Moscow" // }, // "companyIds": [513, 514], <= новое поле // "companies": [ <= разрешение связи // { // "id": 513, // "name": "S7 Airlines" // }, // { // "id": 514, // "name": "Aeroflot Airlines" // }, // ] // } // // поля "code" и "featured" обновлены, // но исключены из ответа опцией "fields" // // документы "city" и "companies" // встроены в результат опцией "include" ``` #### patch(data, where = undefined) Обновляет документы и возвращает их число. Используется для обновления нескольких документов согласно условиям выборки, или всей коллекции сразу. ```js // коллекция имеет три документа // [ // { // "id": 1, // "type": "city", // "name": "Bangkok", // "hidden": true // }, // { // "id": 2, // "type": "country", // "name": "Thailand", // "hidden": true // }, // { // "id": 3, // "type": "city", // "name": "Moscow", // "hidden": true // } // ] // вызов метода `patch` с передачей // значений для обновляемых полей const result = await rep.patch({ hidden: false, updatedAt: new Date().toISOString(), }); // вывод количество затронутых документов console.log(result); // 3 // просмотр коллекции методом `find` // для проверки изменений const docs = await rep.find(); console.log(docs); // [ // { // "id": 1, // "type": "city", // "name": "Bangkok", // "hidden": false, <= обновлено // "updatedAt": "2023-12-02T14:00:00.000Z" <= новое поле // }, // { // "id": 2, // "type": "country", // "name": "Thailand", // "hidden": false, <= обновлено // "updatedAt": "2023-12-02T14:00:00.000Z" <= новое поле // }, // { // "id": 3, // "type": "city", // "name": "Moscow", // "hidden": false, <= обновлено // "updatedAt": "2023-12-02T14:00:00.000Z" <= новое поле // } // ] ``` Использование второго параметра `where` метода `patch` позволяет определить условия выборки обновляемых документов. ```js // вызов метода `patch` с передачей // условий выборки вторым параметром const result = await rep.patch( // новые значения полей { hidden: false, updatedAt: new Date().toISOString(), }, // условия выборки { type: 'city', // только если "type" равен "city" hidden: true, // и "hidden" имеет значение true } ); // вывод количества затронутых документов console.log(result); // 2 // по условиям выборки обновлено // только 2 документа из 3-х ``` #### find(filter = undefined) Возвращает все документы коллекции или согласно условию. ```js // вызов метода `find` без аргументов // возвращает все документы коллекции const result = await rep.find(); console.log(result); // [ // { // "id": 1, // "type": "article", // "title": "The Forgotten Ship", // "publishedAt": "2023-12-02T08:00:00.000Z", // "featured": true // }, // { // "id": 2, // "type": "article", // "title": "A Giant Bellows", // "publishedAt": "2023-12-02T12:00:00.000Z", // "featured": false // }, // { // "id": 3, // "type": "letter", // "title": "Hundreds of bottles", // "publishedAt": null, // "featured": false // } // ] ``` Фильтрация результата (опционально). ```js // первый параметр может принимать // объект cо следующими настройками const result = await rep.find({ // "where" - фильтрация выборки по условию, где // указанные поля должны содержать определенные // значения (см. Фильтрация) where: {type: 'article', featured: true}, where: {title: {like: 'the forgotten'}}, where: {publishedAt: {lte: '2023-12-02T21:00:00.000Z'}}, // "order" - сортировка по указанному полю может // принимать постфикс DESC для обратного порядка order: 'featured', order: 'publishedAt DESC', order: ['publishedAt DESC', 'featured ASC', 'id'], // "limit" - ограничение результата // "skip" - пропуск документов limit: 10, skip: 10, // "fields" - если определено, то результат // будет включать только указанные поля fields: 'title', fields: ['title', 'featured'], // "include" - включить в результат связанные // документы (см. Связи) include: 'author', include: {author: 'role'}, include: ['author', 'categories'], }); ``` #### findOne(filter = undefined) Возвращает первый найденный документ или `undefined` ```js // коллекция имеет три документа // [ // { // "id": 1, // "type": "article", // "title": "The Forgotten Ship", // "publishedAt": "2023-12-02T08:00:00.000Z", // "featured": true // }, // { // "id": 2, // "type": "article", // "title": "A Giant Bellows", // "publishedAt": "2023-12-02T12:00:00.000Z", // "featured": false // }, // { // "id": 3, // "type": "letter", // "title": "Hundreds of bottles", // "publishedAt": null, // "featured": false // } // ] // вызов метода `findOne` без аргументов // возвращает первый документ коллекции const result = await rep.findOne(); console.log(result); // { // "id": 1, // "type": "article", // "title": "The Forgotten Ship", // "publishedAt": "2023-12-02T08:00:00.000Z", // "featured": true // } ``` Фильтрация результата (опционально). ```js // первый параметр метода `find` принимает // объект настроек возвращаемого результата const result = await rep.findOne({ // "where" - фильтрация выборки по условию, где // указанные поля должны содержать определенные // значения (см. Фильтрация) where: {type: 'article', featured: true}, where: {title: {like: 'the forgotten'}}, where: {publishedAt: {lte: '2023-12-02T21:00:00.000Z'}}, // "order" - сортировка по указанному полю может // принимать постфикс DESC для обратного порядка order: 'featured', order: 'publishedAt DESC', order: ['publishedAt DESC', 'featured ASC', 'id'], // "skip" - пропуск документов skip: 10, // "fields" - если определено, то результат // будет включать только указанные поля fields: 'title', fields: ['title', 'featured'], // "include" - включить в результат связанные // документы (см. Связи) include: 'author', include: {author: 'role'}, include: ['author', 'categories'], }); ``` #### findById(id, filter = undefined) Поиск документа по идентификатору. Возвращает найденный документ или выбрасывает исключение. ```js // коллекция содержит три документа // [ // { // "id": 1, // "title": "The Forgotten Ship", // "featured": true // }, // { // "id": 2, // "title": "A Giant Bellows", // "featured": false // }, // { // "id": 3, // "title": "Hundreds of bottles", // "featured": false // } // ] // вызов метода `findById` с передачей // идентификатора искомого документа const result = await rep.findById(2); console.log(result); // { // "id": 2, // "title": "A Giant Bellows", // "featured": false // } ``` Использование параметра `filter` (опционально). ```js // второй параметр метода `findById` принимает // объект настроек возвращаемого результата const result = await rep.findById(2, { // "fields" - если определено, то результат // будут включать только указанные поля fields: 'title', fields: ['title', 'featured'], // "include" - включить в результат связанные // документы (см. Связи) include: 'author', include: {author: 'role'}, include: ['author', 'categories'], }); ``` #### delete(where = undefined) Удаляет все документы коллекции или согласно условию. Возвращает количество удаленных документов. ```js // коллекция имеет три документа // [ // { // "id": 1, // "title": "The Forgotten Ship", // "featured": true // }, // { // "id": 2, // "title": "A Giant Bellows", // "featured": false // }, // { // "id": 3, // "title": "Hundreds of bottles", // "featured": false // } // ] // вызов метода `delete` без аргументов // удалит все содержимое коллекции const result = await rep.delete(); console.log(result); // 3 // просмотр коллекции методом `find` // для проверки изменений const docs = await rep.find(); console.log(docs); // [] ``` Условия выборки (опционально). ```js // первый параметр метода `delete` // принимает условия выборки const result = await rep.delete({ title: { like: 'bellows', // оператор "like" проверяет поле "title" }, // на содержание подстроки "bellows" featured: false, // значение поля "featured" должно быть false // см. Фильтрация }); // вывод результата console.log(result); // 1 ``` #### deleteById(id) Удаляет документ по идентификатору. Возвращает `true` в случае успеха или `false` если не найден. ```js // коллекция имеет три документа // [ // { // "id": 1, // "title": "The Forgotten Ship" // }, // { // "id": 2, // "title": "A Giant Bellows" // }, // { // "id": 3, // "title": "Hundreds of bottles" // } // ] // вызов метода `deleteById` с передачей // идентификатора удаляемого документа const result = await rep.deleteById(2); // вывод результата console.log(result); // true // просмотр коллекции методом `find` // для проверки изменений 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" // } // ] // вызов метода `exists` с передачей // существующего идентификатора 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", // "featured": true // }, // { // "id": 2, // "title": "A Giant Bellows", // "featured": false // }, // { // "id": 3, // "title": "Hundreds of bottles", // "featured": false // } // ] // вызов метода `count` без аргументов // возвращает общее число документов const result = await rep.count(); console.log(result); // 3 ``` Условия выборки (опционально). ```js // первый параметр метода `count` // принимает условия выборки const result = await rep.count({ featured: { // оператор "neq" проверяет поле "featured" neq: true, // на неравенство значению true } // см. Фильтрация }); // вывод результата console.log(result); // 2 ``` ## Расширение При использовании метода `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 ``` ### Filter Большинство методов репозитория принимают объект `filter` для фильтрации возвращаемого ответа. Описание параметров объекта: - **where** *(условия выборки данных из базы)* примеры: `where: {foo: 'bar'}` поиск по значению поля `foo` `where: {foo: {eq: 'bar'}}` оператор равенства `eq` `where: {foo: {neq: 'bar'}}` оператор неравенства `neq` `where: {foo: {gt: 5}}` оператор "больше" `gt` `where: {foo: {lt: 10}}` оператор "меньше" `lt` `where: {foo: {gte: 5}}` оператор "больше или равно" `gte` `where: {foo: {lte: 10}}` оператор "меньше или равно" `lte` `where: {foo: {inq: ['bar', 'baz']}}` равенство одного из значений `inq` `where: {foo: {nin: ['bar', 'baz']}}` исключение значений массива `nin` `where: {foo: {between: [5, 10]}}` оператор диапазона `between` `where: {foo: {exists: true}}` оператор наличия значения `exists` `where: {foo: {like: 'bar'}}` оператор поиска подстроки `like` `where: {foo: {ilike: 'BaR'}}` регистронезависимая версия `ilike` `where: {foo: {nlike: 'bar'}}` оператор исключения подстроки `nlike` `where: {foo: {nilike: 'BaR'}}` регистронезависимая версия `nilike` `where: {foo: {regexp: 'ba.+'}}` оператор регулярного выражения `regexp` `where: {foo: {regexp: 'ba.+', flags: 'i'}}` флаги регулярного выражения - **order** *(упорядочить записи по полю)* примеры: `order: 'foo'` порядок по полю `foo` `order: 'foo ASC'` явное указание порядка `order: 'foo DESC'` инвертировать порядок `order: ['foo', 'bar ASC', 'baz DESC']` по нескольким полям - **limit** *(не более N записей)* примеры: `limit: 0` не ограничивать `limit: 14` не более 14-и - **skip** *(пропуск первых N записей)* примеры: `skip: 0` выборка без пропуска `skip: 10` пропустить 10 объектов выборки - **include** *(включение связанных данных в результат)* примеры: `include: 'foo'` включение связи `foo` `include: ['foo', 'bar']` включение `foo` и `bar` `include: {foo: 'bar'}` включение вложенной связи `foo` ## Связи Параметр `relations` описывает набор связей к другим моделям. Понятия: - источник связи *- модель в которой определена данная связь* - целевая модель *- модель на которую ссылается источник связи* Типы: - `belongsTo` - ссылка на целевую модель находится в источнике - `hasOne` - ссылка на источник находится в целевой модели (one-to-one) - `hasMany` - ссылка на источник находится в целевой модели (one-to-many) - `referencesMany` - массив ссылок на целевую модель находится в источнике Параметры: - `type: string` тип связи - `model: string` целевая модель - `foreignKey: string` поле для идентификатора цели - `polymorphic: boolean|string` объявить связь полиморфной* - `discriminator: string` поле для названия целевой модели (для `polymorphic: true`) *i. Полиморфный режим `belongsTo` позволяет динамически определять цель связи, где имя целевой модели хранится в отдельном поле, рядом с `foreignKey`* #### BelongsTo Связь заказа к покупателю через поле `customerId` ```js schema.defineModel({ // ... relations: { // ... customer: { type: 'belongsTo', model: 'customer', foreignKey: 'customerId', // опционально }, }, }); ``` Полиморфная версия ```js schema.defineModel({ // ... relations: { // ... customer: { type: 'belongsTo', polymorphic: true, foreignKey: 'customerId', // опционально discriminator: 'customerType', // опционально }, }, }); ``` #### HasOne (one-to-one) Связь покупателя к заказу, как обратная сторона `belongsTo` ```js schema.defineModel({ // ... relations: { // ... order: { type: 'hasOne', model: 'order', foreignKey: 'customerId', // поле целевой модели }, }, }); ``` Обратная сторона полиморфной версии `belongsTo` ```js schema.defineModel({ // ... relations: { // ... order: { type: 'hasOne', model: 'order', polymorphic: 'customer', // имя связи целевой модели }, }, }); ``` Явное указание `foreignKey` и `discriminator` ```js schema.defineModel({ // ... relations: { // ... order: { type: 'hasOne', model: 'order', polymorphic: true, // true вместо имени модели foreignKey: 'customerId', // поле целевой модели discriminator: 'customerType', // поле целевой модели }, }, }); ``` #### HasMany (one-to-many) Связь покупателя к заказам, как обратная сторона `belongsTo` ```js schema.defineModel({ // ... relations: { // ... orders: { type: 'hasMany', model: 'order', foreignKey: 'customerId', // поле целевой модели }, }, }); ``` Обратная сторона полиморфной версии `belongsTo` ```js schema.defineModel({ // ... relations: { // ... orders: { type: 'hasMany', model: 'order', polymorphic: 'customer', // имя связи целевой модели }, }, }); ``` Явное указание `foreignKey` и `discriminator` ```js schema.defineModel({ // ... relations: { // ... orders: { type: 'hasMany', model: 'order', polymorphic: true, // true вместо имени модели foreignKey: 'customerId', // поле целевой модели discriminator: 'customerType', // поле целевой модели }, }, }); ``` #### ReferencesMany Связь покупателя к заказам через поле `orderIds` ```js schema.defineModel({ // ... relations: { // ... orders: { type: 'referencesMany', model: 'order', foreignKey: 'orderIds', // опционально }, }, }); ``` --> ## Тесты ```bash npm run test ``` ## Лицензия MIT