Абстракция для работы с базами данных для Node.js
npm install @e22m4u/js-repository
Опционально устанавливаем адаптер. Например, если используется MongoDB, то для подключения потребуется установить адаптер mongodb как отдельную зависимость.
npm install @e22m4u/js-repository-mongodb-adapter
Список доступных адаптеров:
| адаптер | описание |
|---|---|
memory |
виртуальная база в памяти процесса (для разработки и тестирования) |
mongodb |
MongoDB - система управления NoSQL базами (требует установки) |
Модуль позволяет спроектировать систему связанных данных, доступ к которым осуществляется посредством репозиториев. Каждый репозиторий имеет собственную модель данных, которая описывает структуру определенной коллекции в базе, а так же определяет связи к другим коллекциям.
flowchart LR
A[Datasource]-->B[Data Model]-->С[Repository];
Определения источников и моделей хранятся в экземпляре класса
Schema, и первым шагом будет создание данного
экземпляра.
import {Schema} from '@e22m4u/js-repository';
const schema = new Schema();
Интерфейс Schema содержит три основных метода:
defineDatasource(datasourceDef: object): this -
добавить источникdefineModel(modelDef: object): this - добавить
модельgetRepository(modelName: string): Repository - получить
репозиторийИсточник описывает способ подключения к базе и используемый адаптер.
Если адаптер имеет настройки, то они передаются вместе с объектом
определения источника методом defineDatasource, как это
показано ниже.
schema.defineDatasource({
name: 'myMongo', // название нового источника
adapter: 'mongodb', // название выбранного адаптера
// настройки адаптера mongodb
host: '127.0.0.1',
port: 27017,
database: 'data'
});
Параметры источника:
name: string уникальное названиеadapter: string выбранный адаптерПри желании можно использовать встроенный адаптер
memory, который хранит данные в памяти процесса. У него нет
специальных настроек, и он отлично подходит для тестов и
прототипирования.
schema.defineDatasource({
name: 'myMemory', // название источника
adapter: 'memory', // выбранный адаптер
});
Когда источники определены, можно перейти к описанию моделей данных. Модель может определять как структуру какого-либо объекта, так и являться отражением реальной коллекции базы.
Представьте себе коллекцию торговых точек, у каждой из которых
имеются координаты lat и lng. Мы можем заранее
определить модель для объекта координат методом defineModel
и использовать ее в других коллекциях.
schema.defineModel({
name: 'latLng', // название новой модели
properties: { // поля модели
lat: DataType.NUMBER, // поле широты
lng: DataType.NUMBER, // поле долготы
},
});
Параметры модели:
name: string уникальное название (обязательно)datasource: string выбранный источник данныхproperties: object определения полей моделиrelations: object определения связанных коллекцийПараметр properties принимает объект, ключи которого
являются именами полей, а значением тип поля или объект с
дополнительными параметрами.
schema.defineModel({
name: 'latLng',
properties: {
lat: DataType.NUMBER, // краткое определение поля "lat"
lng: { // расширенное определение поля "lng"
type: DataType.NUMBER, // тип допустимого значения
required: true, // исключает null и undefined
},
},
});
Типы данных:
DataType.ANYDataType.STRINGDataType.NUMBERDataType.BOOLEANDataType.ARRAYDataType.OBJECTМодель latLng всего лишь описывает структуру объекта
координат, тогда как торговая точка должна иметь реальную таблицу в
базе. По аналогии с предыдущим примером, добавим модель
place, но дополнительно укажем источник данных в параметре
datasource
schema.defineModel({
name: 'place',
datasource: 'myMemory', // выбранный источник данных
properties: {
name: DataType.STRING, // поле для названия торговой точки
location: { // поле для объекта координат
type: DataType.OBJECT, // допускать только объекты
model: 'latLng', // определение модели объекта координат
},
},
});
В примере выше мы использовали модель latLng как
структуру допустимого значения поля location. Возможный
документ данной коллекции может выглядеть так:
{
"id": 1,
"name": "Burger King",
"location": {
"lat": 32.412891,
"lng": 34.7660061
}
}
Стоит обратить внимание, что мы могли бы не объявлять параметр
properties, при этом теряя возможность проверки данных
перед записью в базу.
schema.defineModel({
name: 'place',
adapter: 'myMemory',
// параметр "properties" не указан
});
Параметры поля:
type: string тип допустимого значения
(обязательно)itemType: string тип элемента массива (для
type: 'array')model: string модель объекта (для
type: 'object')primaryKey: boolean объявить поле первичным ключомcolumnName: string переопределение названия
колонкиcolumnType: string тип колонки (определяется
адаптером)required: boolean объявить поле обязательнымdefault: any значение по умолчаниюВ отличие от latLng, модель place имеет
источник данных с названием myMemory, который был объявлен
ранее. Наличие источника позволяет получить репозиторий по названию
модели.
const rep = schema.getRepository('place');
Методы:
create(data, filter = undefined)replaceById(id, data, filter = undefined)patchById(id, data, filter = undefined)patch(data, where = undefined)find(filter = undefined)findOne(filter = undefined)findById(id, filter = undefined)delete(where = undefined)deleteById(id)exists(id)count(where = undefined)Добавляет новый документ в коллекцию и возвращает его.
// вызов метода `create` с передачей состава
// нового документа первым параметром
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
// }
Использование параметра filter (опционально).
// подготовка состава для нового документа
const data = {
name: 'Rick Sanchez',
dimension: 'C-137',
age: 67,
pictureId: 345,
biographyId: 59
}
// подготовка параметра "filter"
const filter = {
// "fields" - если определено, то результат
// будут включать только указанные поля
fields: [
'name',
'pictureId',
'biographyId'
],
// "include" - включение в результат
// связанных документов (см. Связи)
include: [
'picture',
'biography',
],
}
// вызов метода `create` и вывод результата
const result = await rep.create(data, filter);
console.log(result);
// {
// "name": "Rick Sanchez",
// "pictureId": 345,
// "picture": { <= встроено в ответ
// "id": 345,
// "mime": "image/jpeg",
// "file": "/uploads/rick.jpg"
// },
// "biographyId": 59,
// "biography": { <= встроено в ответ
// "id": 59,
// "annotation": "This article is about Rick Sanchez",
// "body": "He is a genius scientist whose ..."
// }
// }
//
// поля "age" и "dimension"
// исключены опцией "fields"
//
// документы "picture" и "biography"
// встроены опцией "include" (см. Связи)
Заменяет существующий документ по идентификатору и возвращает его. Если идентификатор не найден, то выбрасывает исключение.
// документ с идентификатором 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 (опционально).
// подготовка состава для заменяемого документа
const data = {
name: 'Morty Smith',
kind: 'a young teenage boy',
age: 14,
pictureId: 347,
biographyId: 61
}
// подготовка параметра "filter"
const filter = {
// "fields" - если определено, то результат
// будут включать только указанные поля
fields: [
'name',
'pictureId',
'biographyId'
],
// "include" - включение в результат
// связанных документов (см. Связи)
include: [
'picture',
'biography',
],
}
// вызов метода `replaceById` и вывод результата
const result = await rep.replaceById(12, data, filter);
console.log(result);
// {
// "name": "Morty Smith",
// "pictureId": 347,
// "picture": { <= встроено в ответ
// "id": 347,
// "mime": "image/jpeg",
// "file": "/uploads/morty.jpg"
// },
// "biographyId": 61,
// "biography": { <= встроено в ответ
// "id": 61,
// "annotation": "This article is about Morty Smith",
// "body": "Currently, Morty is 14 years old ..."
// }
// }
//
// поля "age" и "dimension"
// исключены опцией "fields"
//
// документы "picture" и "biography"
// встроены опцией "include" (см. Связи)
Частично обновляет существующий документ по идентификатору и возвращает его. Если идентификатор не найден, то выбрасывает исключение.
// документ с идентификатором 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 (опционально).
// подготовка обновляемых данных
const data = {
name: 'Sheremetyevo Airport',
code: 'SVO',
featured: true,
cityId: 231,
companyIds: [513, 514],
}
// подготовка параметра `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" (см. Связи)
Обновляет документы и возвращает их число. Используется для обновления нескольких документов согласно условиям выборки, или всей коллекции сразу.
// коллекция имеет три документа
// [
// {
// "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" <= добавлено новое поле
// }
// ]
Условия выборки (опционально).
// вызов метода `patch` с передачей
// условий выборки
const result = await rep.patch(
// новые значения полей
{
hidden: false,
updatedAt: new Date().toISOString(),
},
// условия выборки
{
type: 'city',
hidden: true,
}
);
// вывод количества затронутых документов
console.log(result);
// 2
// по условиям выборки обновлено
// только 2 документа из 3-х
Возвращает все документы коллекции или согласно условию.
// вызов метода `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
// }
// ]
Фильтрация результата (опционально).
// первый параметр может принимать
// объект 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'],
});
Возвращает первый найденный документ или undefined
// коллекция имеет три документа
// [
// {
// "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
// }
Фильтрация результата (опционально).
// первый параметр метода `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'],
});
Поиск документа по идентификатору. Возвращает найденный документ или выбрасывает исключение.
// коллекция содержит три документа
// [
// {
// "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 (опционально).
// второй параметр метода `findById` принимает
// объект настроек возвращаемого результата
const result = await rep.findById(2, {
// "fields" - если определено, то результат
// будут включать только указанные поля
fields: 'title',
fields: ['title', 'featured'],
// "include" - включить в результат связанные
// документы (см. Связи)
include: 'author',
include: {author: 'role'},
include: ['author', 'categories'],
});
Удаляет все документы коллекции или согласно условию. Возвращает количество удаленных документов.
// коллекция имеет три документа
// [
// {
// "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);
// []
Условия выборки (опционально).
// первый параметр метода `delete`
// принимает условия выборки
const result = await rep.delete({
title: {
like: 'bellows', // оператор "like" проверяет поле "title"
}, // на содержание подстроки "bellows"
featured: false, // значение поля "featured" должно быть false
// см. Фильтрация
});
// вывод результата
console.log(result);
// 1
Удаляет документ по идентификатору. Возвращает true в
случае успеха или false если не найден.
// коллекция имеет три документа
// [
// {
// "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"
// }
// ]
Проверка существования документа по идентификатору. Возвращает
true если найден, в противном случае
false.
// коллекция имеет три документа
// [
// {
// "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
Подсчет количества документов и возврат их числа.
// коллекция имеет три документа
// [
// {
// "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
Условия выборки (опционально).
// первый параметр метода `count`
// принимает условия выборки
const result = await rep.count({
featured: { // оператор "neq" проверяет поле "featured"
neq: true, // на неравенство значению true
}
// см. Фильтрация
});
// вывод результата
console.log(result);
// 2
При использовании метода getRepository выполняется
проверка на наличие существующего экземпляра репозитория для нужной
модели. При первичном запросе создается новый экземпляр, который будет
сохранен для повторных обращений к методу.
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. После чего, все создаваемые репозитории
будут использовать уже пользовательский конструктор вместо
стандартного.
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
npm run test
MIT