@e22m4u/js-repository
v0.8.5
Published
Реализация репозитория для работы с базами данных
Maintainers
Readme
@e22m4u/js-repository
Реализация паттерна «Репозиторий» для работы с базами данных.
Содержание
- Установка
- Импорт
- Описание
- Пример
- Схема
- Источник данных
- Модель
- Свойства
- Репозиторий
- Фильтрация
- Связи
- Расширение
- TypeScript
- Тесты
- Лицензия
Установка
npm install @e22m4u/js-repositoryОпционально устанавливается нужный адаптер.
| адаптер | описание | установка |
|-----------|-------------------------------------------------|----------------------------------------------------------------------------|
| memory | Виртуальная база в памяти процесса | встроенный |
| mongodb | MongoDB - документо-ориентированная база данных | npm |
Импорт
Модуль поддерживает ESM и CommonJS стандарты.
ESM
import {DatabaseSchema} from '@e22m4u/js-repository';CommonJS
const {DatabaseSchema} = require('@e22m4u/js-repository');Описание
Модуль позволяет абстрагироваться от различных интерфейсов баз данных, представляя их как именованные источники данных, подключаемые к моделям. Модель же описывает таблицу базы, колонки которой являются свойствами модели. Свойства модели могут иметь определенный тип допустимого значения. Кроме того, модель может определять классические связи «один к одному», «один ко многим» и другие типы отношений между моделями.
Непосредственно чтение и запись данных производится с помощью репозитория, который есть у каждой модели с объявленным источником данных. Репозиторий может фильтровать запрашиваемые документы, выполнять валидацию свойств согласно определению модели, и встраивать связанные данные в результат выборки.
- Источник данных - определяет способ подключения к базе;
- Модель - описывает структуру документа и связи к другим моделям;
- Репозиторий - выполняет операции чтения и записи документов модели;
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Пример
Пример демонстрирует создание экземпляра схемы, объявление источника данных
и модели country. После чего, с помощью репозитория данной модели, в коллекцию
добавляется новый документ (страна), который выводится в консоль.
Страна (country)
┌─────────────────────────┐
│ id: 1 │
│ name: "Russia" │
│ population: 143400000 │
└─────────────────────────┘import {DataType} from '@e22m4u/js-repository';
import {DatabaseSchema} from '@e22m4u/js-repository';
// создание экземпляра DatabaseSchema
const dbs = new DatabaseSchema();
// объявление источника "myDb"
dbs.defineDatasource({
name: 'myDb', // название нового источника
adapter: 'memory', // выбранный адаптер
});
// объявление модели "country"
dbs.defineModel({
name: 'country', // название новой модели
datasource: 'myDb', // выбранный источник
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,
// }В следующем блоке определяется модель city со связью belongsTo к модели
country из примера выше. Затем создается новый документ города, связанный
с ранее созданной страной. После создания нового документа, выполняется запрос
на извлечение данного города с включением связанной страны.
Страна (country) Город (city)
┌─────────────────────────┐ ┌─────────────────────────┐
│ id: 1 <───────────────│───┐ │ id: 1 │
│ name: "Russia" │ │ │ name: "Moscow" │
│ population: 143400000 │ └───│─ countryId: 1 │
└─────────────────────────┘ └─────────────────────────┘// объявление модели "city" со связью к "country"
dbs.defineModel({
name: 'city',
datasource: 'myDb',
properties: {
name: DataType.STRING,
countryId: DataType.NUMBER,
// внешний ключ "countryId" указывать не обязательно,
// но для проверки типа значения перед записью в базу
// рекомендуется, так как адаптер "memory" по умолчанию
// создает числовые идентификаторы
},
relations: {
// определение связи "country" позволит автоматически включать
// связанные документы с помощью опции "include" при запросах
// из данной коллекции через методы репозитория
country: {
type: RelationType.BELONGS_TO, // тип связи: принадлежит к...
model: 'country', // название целевой модели
foreignKey: 'countryId', // поле с внешним ключом (не обязательно)
// если внешний ключ соответствует `relationName` + `Id`,
// то указывать опцию `foreignKey` не обязательно
},
},
});
// получение репозитория для модели "city"
const cityRep = dbs.getRepository('city');
// создание нового города и его привязка к стране через country.id
const city = await cityRep.create({
name: 'Moscow',
countryId: country.id, // использование id созданной ранее страны
});
console.log(city);
// {
// id: 1,
// name: 'Moscow',
// countryId: 1,
// }
// извлечение города по идентификатору с включением связанной страны
const cityWithCountry = await cityRep.findById(city.id, {
include: 'country',
});
console.log(cityWithCountry);
// {
// id: 1,
// name: 'Moscow',
// countryId: 1,
// country: {
// id: 1,
// name: 'Russia',
// population: 143400000
// }
// }Схема
Экземпляр класса DatabaseSchema хранит определения источников данных и моделей.
Методы
defineDatasource(datasourceDef: object): this- добавить источник;defineModel(modelDef: object): this- добавить модель;getRepository(modelName: string): Repository- получить репозиторий;
Примеры
Импорт класса и создание экземпляра схемы.
import {DatabaseSchema} from '@e22m4u/js-repository';
const dbs = new DatabaseSchema();Определение нового источника.
dbs.defineDatasource({
name: 'myDb', // название нового источника
adapter: 'memory', // выбранный адаптер
});Определение новой модели.
dbs.defineModel({
name: 'product', // название новой модели
datasource: 'myDb', // выбранный источник
properties: { // свойства модели
name: DataType.STRING,
weight: DataType.NUMBER,
},
});Получение репозитория по названию модели.
const productRep = dbs.getRepository('product');Источник данных
Источник хранит название выбранного адаптера и его настройки. Определение
нового источника выполняется методом defineDatasource экземпляра
DatabaseSchema.
Параметры
name: stringуникальное название;adapter: stringвыбранный адаптер;- параметры адаптера (если имеются);
Примеры
Определение нового источника.
dbs.defineDatasource({
name: 'myDb', // название нового источника
adapter: 'memory', // выбранный адаптер
});Передача дополнительных параметров на примере MongoDB адаптера (установка).
dbs.defineDatasource({
name: 'myDb',
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определения связей (см. Связи);
Примеры
Определение модели со свойствами указанного типа.
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значение по умолчанию (заменяетundefinedиnull);unique: boolean | stringпроверять значение на уникальность;
Параметр unique
Если значением параметра unique является true или "strict", то выполняется
строгая проверка на уникальность. В этом режиме любое значение данного свойства
не может быть представлено более одного раза.
unique: true | 'strict'строгая проверка на уникальность;unique: 'sparse'исключить из проверки ложные значения;unique: false | 'nonUnique'не проверять на уникальность (по умолчанию);
Режим "sparse" исключает из проверки на уникальность ложные значения с точки
зрения JavaScript. Указанные ниже значения будут проигнорированы при проверке
в данном режиме.
""пустая строка;0число ноль;falseлогическое отрицание;undefinedиnull;
В качестве значений параметра unique можно использовать предопределенные
константы как эквивалент строковых значений strict, sparse и nonUnique.
PropertyUniqueness.STRICT;PropertyUniqueness.SPARSE;PropertyUniqueness.NON_UNIQUE;
Примеры
Краткое определение свойств модели.
dbs.defineModel({
name: 'city',
properties: { // свойства модели
name: DataType.STRING, // тип свойства "string"
population: DataType.NUMBER, // тип свойства "number"
},
});Расширенное определение свойств модели.
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, // проверять уникальность
},
},
});Фабричное значение по умолчанию. Возвращаемое значение функции будет определено в момент записи документа.
dbs.defineModel({
name: 'article',
properties: { // свойства модели
tags: {
type: DataType.ARRAY, // тип свойства "array" (обязательно)
itemType: DataType.STRING, // тип элемента "string"
default: () => [], // фабричное значение
},
createdAt: {
type: DataType.STRING, // тип свойства "string" (обязательно)
default: () => new Date().toISOString(), // фабричное значение
},
},
});Репозиторий
Репозиторий выполняет операции чтения и записи данных определенной модели. Он выступает в роли посредника между бизнес-логикой приложения и базой данных.
Методы
create(data, filter = undefined)создать новый документ;replaceById(id, data, filter = undefined)заменить документ полностью;replaceOrCreate(data, filter = undefined)заменить или создать новый;patchById(id, data, filter = undefined)обновить документ частично;patch(data, where = undefined)обновить все документы или по условию;find(filter = undefined)найти все документы или по условию;findOne(filter = undefined)найти первый документ или по условию;findById(id, filter = undefined)найти документ по идентификатору;delete(where = undefined)удалить все документы или по условию;deleteById(id)удалить документ по идентификатору;exists(id)проверить существование по идентификатору;count(where = undefined)подсчет всех документов или по условию;
Аргументы
id: number|stringидентификатор (первичный ключ);data: objectданные документа (используется при записи);where: objectусловия фильтрации (см. Фильтрация);filter: objectпараметры выборки (см. Фильтрация);
Получение репозитория
Получить репозиторий можно с помощью метода getRepository() экземпляра
DatabaseSchema. В качестве аргумента метод принимает название модели.
Обязательным условием является наличие у модели определенного источника данных (datasource), так как репозиторий напрямую взаимодействует
с базой данных через указанный в источнике адаптер.
// объявление источника
dbs.defineDatasource({
name: 'myDatasource',
adapter: 'memory', // адаптер
});
// объявление модели
dbs.defineModel({
name: 'myModel',
datasource: 'myDatasource',
// properties: { ... },
// relations: { ... }
});
// получение репозитория модели
const modelRep = dbs.getRepository('myModel');При первом вызове getRepository('myModel') будет создан и сохранен новый
экземпляр репозитория. Все последующие вызовы с тем же названием модели будут
возвращать уже существующий экземпляр.
repository.create
Создает новый документ в коллекции на основе переданных данных. Возвращает созданный документ с присвоенным идентификатором.
Сигнатура:
create(
data: WithOptionalId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;Примеры
Создание нового документа.
const newProduct = await productRep.create({
name: 'Laptop',
price: 1200,
});
console.log(newProduct);
// {
// id: 1,
// name: 'Laptop',
// price: 1200,
// }Создание документа с возвратом определенных полей.
const product = await productRep.create(
{name: 'Mouse', price: 25},
{fields: ['id', 'name']},
);
console.log(product);
// {
// id: 2,
// name: 'Mouse',
// }Создание документа с включением связанных данных в результат.
// предполагается, что модель 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, будут отсутствовать в итоговом документе (если для них
не задано значение по умолчанию).
Сигнатура:
replaceById(
id: IdType,
data: WithoutId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;Примеры
Замена документа по идентификатору.
// исходный документ
// {
// 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
Заменяет существующий документ, если в переданных данных присутствует идентификатор, который уже существует в коллекции. В противном случае, если идентификатор не указан или не найден, создает новый документ.
Сигнатура:
replaceOrCreate(
data: WithOptionalId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;Примеры
Создание нового документа, если id: 3 не существует.
const product = await productRep.replaceOrCreate({
id: 3,
name: 'Keyboard',
price: 75,
});
console.log(product);
// {
// id: 3,
// name: 'Keyboard',
// price: 75,
// }Замена существующего документа с id: 1.
const updatedProduct = await productRep.replaceOrCreate({
id: 1,
name: 'Laptop Pro',
price: 1500,
});
console.log(updatedProduct);
// {
// id: 1,
// name: 'Laptop Pro',
// price: 1500,
// }repository.patchById
Частично обновляет существующий документ по его идентификатору, изменяя только переданные поля. Остальные поля документа остаются без изменений.
Сигнатура:
patchById(
id: IdType,
data: PartialWithoutId<FlatData, IdName>,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;Примеры
Частичное обновление документа по идентификатору.
// исходный документ с 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 не указан,
обновляет все документы в коллекции.
Сигнатура:
patch(
data: PartialWithoutId<FlatData, IdName>,
where?: WhereClause<FlatData>,
): Promise<number>;Примеры
Обновление документов по условию.
// обновит все товары с ценой меньше 30
const updatedCount = await productRep.patch(
{inStock: false},
{price: {lt: 30}},
);Обновление всех документов.
// добавит или обновит поле updatedAt для всех документов
const totalCount = await productRep.patch({
updatedAt: new Date(),
});repository.find
Находит все документы, соответствующие условиям фильтрации, и возвращает их в виде массива. Если фильтр не указан, возвращает все документы коллекции.
Сигнатура:
find(filter?: FilterClause<FlatData>): Promise<FlatData[]>;Примеры
Поиск всех документов.
const allProducts = await productRep.find();Поиск документов по условию where.
const cheapProducts = await productRep.find({
where: {price: {lt: 100}},
});Поиск с сортировкой и ограничением выборки.
const latestProducts = await productRep.find({
order: 'createdAt DESC',
limit: 10,
});repository.findOne
Находит первый документ, соответствующий условиям фильтрации. Возвращает
undefined, если документы не найдены.
Сигнатура:
findOne(
filter?: FilterClause<FlatData>,
): Promise<FlatData | undefined>;Примеры
Поиск одного документа по условию.
const expensiveProduct = await productRep.findOne({
where: {price: {gt: 1000}},
order: 'price DESC',
});Обработка случая, когда документ не найден.
const product = await productRep.findOne({
where: {name: 'Non-existent Product'},
});
if (!product) {
console.log('Product not found.');
}repository.findById
Находит один документ по его уникальному идентификатору. Если документ не найден, выбрасывается ошибка.
Сигнатура:
findById(
id: IdType,
filter?: ItemFilterClause<FlatData>,
): Promise<FlatData>;Примеры
Поиск документа по id.
try {
const product = await productRep.findById(1);
console.log(product);
} catch (error) {
console.error('Product with id 1 is not found.');
}Поиск документа с включением связанных данных.
const product = await productRep.findById(1, {
include: 'category',
});repository.delete
Удаляет один или несколько документов, соответствующих условиям where.
Возвращает количество удаленных документов. Если where не указан, удаляет
все документы в коллекции.
Сигнатура:
delete(where?: WhereClause<FlatData>): Promise<number>;Примеры
Удаление документов по условию.
const deletedCount = await productRep.delete({
inStock: false,
});Удаление всех документов.
const totalCount = await productRep.delete();repository.deleteById
Удаляет один документ по его уникальному идентификатору. Возвращает true,
если документ был найден и удален, в противном случае false.
Сигнатура:
deleteById(id: IdType): Promise<boolean>;Примеры
Удаление документа по id.
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.
Сигнатура:
exists(id: IdType): Promise<boolean>;Примеры
Проверка существования документа по id.
const productExists = await productRep.exists(1);
if (productExists) {
console.log('A document with id 1 exists.');
}repository.count
Подсчитывает количество документов, соответствующих условиям where. Если
where не указан, возвращает общее количество документов в коллекции.
Сигнатура:
count(where?: WhereClause<FlatData>): Promise<number>;Примеры
Подсчет документов по условию.
const cheapCount = await productRep.count({
price: {lt: 100},
});Подсчет всех документов.
const totalCount = await productRep.count();Фильтрация
Некоторые методы репозитория принимают объект настроек, влияющий
на возвращаемый результат. Максимально широкий набор таких настроек
имеет первый параметр метода find, где ожидается объект содержащий
набор опций указанных ниже.
where: objectусловия фильтрации по свойствам документа;order: string|string[]сортировка по указанным свойствам;limit: numberограничение количества документов;skip: numberпропуск документов (пагинация);fields: string|string[]выбор необходимых свойств модели;include: objectвключение связанных данных в результат;
Пример
// для запроса используется метод репозитория "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строгое равенство;neqнеравенство;gtбольше чем;ltменьше чем;gteбольше или равно;lteменьше или равно;inqв списке;ninне в списке;betweenдиапазон;existsналичие свойства;likeSQL-подобный шаблон;nlikeисключающий шаблон;ilikeрегистронезависимый шаблон;nilikeрегистронезависимый шаблон исключения;regexpрегулярное выражение;
Условия можно объединять логическими операторами:
Поиск по значению (сокращенная форма)
Находит документы, у которых значение указанного свойства в точности равно
переданному значению. Это сокращенная запись для оператора {eq: ...}.
// найдет все документы, где age равен 21
const res = await rep.find({
where: {
age: 21,
},
});eq (строгое равенство)
Находит документы, у которых значение свойства равно указанному.
// найдет все документы, где age равен 21
const res = await rep.find({
where: {
age: {eq: 21},
},
});neq (неравенство)
Находит документы, у которых значение свойства не равно указанному.
// найдет все документы, где age не равен 21
const res = await rep.find({
where: {
age: {neq: 21},
},
});gt (больше чем)
Находит документы, у которых значение свойства строго больше указанного.
// найдет документы, где age больше 30
const res = await rep.find({
where: {
age: {gt: 30},
},
});lt (меньше чем)
Находит документы, у которых значение свойства строго меньше указанного.
// найдет документы, где age меньше 30
const res = await rep.find({
where: {
age: {lt: 30},
},
});gte (больше или равно)
Находит документы, у которых значение свойства больше или равно указанному.
// найдет документы, где age больше или равен 30
const res = await rep.find({
where: {
age: {gte: 30},
},
});lte (меньше или равно)
Находит документы, у которых значение свойства меньше или равно указанному.
// найдет документы, где age меньше или равен 30
const res = await rep.find({
where: {
age: {lte: 30},
},
});inq (в списке)
Находит документы, у которых значение свойства совпадает с одним из значений в предоставленном массиве.
// найдет документы, где name - 'John' или 'Mary'
const res = await rep.find({
where: {
name: {inq: ['John', 'Mary']},
},
});nin (не в списке)
Находит документы, у которых значение свойства отсутствует в предоставленном массиве.
// найдет все документы, кроме тех, где name - 'John' или 'Mary'
const res = await rep.find({
where: {
name: {nin: ['John', 'Mary']},
},
});between (диапазон)
Находит документы, у которых значение свойства находится в указанном диапазоне (включая границы).
// найдет документы, где age находится в диапазоне от 20 до 30 включительно
const res = await rep.find({
where: {
age: {between: [20, 30]},
},
});exists (наличие свойства)
Проверяет наличие или отсутствие свойства в документе. Не проверяет значение свойства.
trueсвойство должно существовать (даже если его значениеnull);falseсвойство должно отсутствовать;
// найдет документы, у которых есть свойство 'nickname'
const res1 = await rep.find({
where: {
nickname: {exists: true},
},
});
// найдет документы, у которых нет свойства 'nickname'
const res2 = await rep.find({
where: {
nickname: {exists: false},
},
});like (шаблон)
Выполняет сопоставление с шаблоном, с учетом регистра (см. подробнее).
// найдет {name: 'John Doe'}, но не {name: 'john doe'}
const res = await rep.find({
where: {
name: {like: 'John%'},
},
});nlike (исключающий шаблон)
Находит документы, которые не соответствуют шаблону, с учетом регистра (см. подробнее).
// найдет все, кроме тех, что начинаются на 'John'
const res = await rep.find({
where: {
name: {nlike: 'John%'},
},
});ilike (регистронезависимый шаблон)
Выполняет сопоставление с шаблоном без учета регистра (см. подробнее).
// найдет {name: 'John Doe'} и {name: 'john doe'}
const res = await rep.find({
where: {
name: {ilike: 'john%'},
},
});nilike (регистронезависимый шаблон исключения)
Находит строки, которые не соответствуют шаблону, без учета регистра (см. подробнее).
// найдет все, кроме тех, что начинаются на 'John' или 'john'
const res = await rep.find({
where: {
name: {nilike: 'john%'},
},
});regexp (регулярное выражение)
Находит документы, у которых значение строкового свойства соответствует
указанному регулярному выражению. Может быть передано в виде строки или
объекта RegExp.
// найдет документы, где 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 (логическое И)
Объединяет несколько условий в массив, требуя, чтобы каждое условие было выполнено.
// найдет документы, где surname равен 'Smith' И age равен 21
const res = await rep.find({
where: {
and: [
{surname: 'Smith'},
{age: 21}
],
},
});or (логическое ИЛИ)
Объединяет несколько условий в массив, требуя, чтобы хотя бы одно из них было выполнено.
// найдет документы, где 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
const res = await rep.find({
order: 'createdAt',
});Упорядочить по полю createdAt в обратном порядке.
const res = await rep.find({
order: 'createdAt DESC',
});Упорядочить по нескольким свойствам в разных направлениях.
const res = await rep.find({
order: [
'title',
'price ASC',
'featured DESC',
],
});i. Направление порядка ASC указывать необязательно.
include
Параметр включает связанные документы в результат вызываемого метода. Названия включаемых связей должны быть определены в текущей модели. (см. Связи)
Примеры
Включение связи по названию.
const res = await rep.find({
include: 'city',
});Включение вложенных связей.
const res = await rep.find({
include: {
city: 'country',
},
});Включение нескольких связей массивом.
const res = await rep.find({
include: [
'city',
'address',
'employees'
],
});Использование фильтрации включаемых документов.
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.
Роль (role)
┌────────────────────┐
│ id: 3 <──────────│────┐
│ name: 'Manager' │ │
└────────────────────┘ │
│
Пользователь (user) │
┌────────────────────────┐ │
│ id: 1 │ │
│ name: 'John Doe' │ │
│ roleId: 3 ──────────│────┘
│ cityId: 24 ──────────│────┐
└────────────────────────┘ │
│
Город (city) │
┌────────────────────┐ │
│ id: 24 <─────────│────┘
│ name: 'Moscow' │
└────────────────────┘// запрос документа коллекции "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 методах репозитория.
import {
DataType,
RelationType,
DatabaseSchema,
} 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. Полиморфный режим позволяет динамически определять целевую модель по ее названию, которое хранит документ в свойстве-дискриминаторе.
Типы связей
Belongs To
Текущая модель ссылается на целевую по идентификатору.type: "belongsTo"илиtype: RelationType.BELONGS_TOHas One
Обратная сторонаbelongsToпо принципу "один к одному".type: "hasOne"илиtype: RelationType.HAS_ONEHas Many
Обратная сторонаbelongsToпо принципу "один ко многим".type: "hasMany"илиtype: RelationType.HAS_MANYReferences Many
Текущая модель ссылается на целевую через массив идентификаторов.type: "referencesMany"илиtype: RelationType.REFERENCES_MANY
Полиморфные версии:
Параметр type в определении связи принимает строку с названием типа. Чтобы исключить опечатку, рекомендуется использовать константы объекта RelationType
указанные ниже.
RelationType.BELONGS_TORelationType.HAS_ONERelationType.HAS_MANYRelationType.REFERENCES_MANY
Belongs To
Текущая модель ссылается на целевую по идентификатору.
Текущая (user) Целевая (role)
┌─────────────────────────┐ ┌─────────────────────────┐
│ id: 1 │ ┌───│─> id: 5 │
│ roleId: 5 ───────────│───┤ │ ... │
│ ... │ │ └─────────────────────────┘
└─────────────────────────┘ │
┌─────────────────────────┐ │
│ id: 2 │ │
│ roleId: 5 ───────────│───┘
│ ... │
└─────────────────────────┘ Определение связи:
dbs.defineModel({
name: 'user',
relations: {
role: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
model: 'role', // название целевой модели
foreignKey: 'roleId', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Id"
},
},
});Пример:
import {
DataType,
RelationType,
DatabaseSchema,
} from '@e22m4u/js-repository';
const dbs = new DatabaseSchema();
// источник данных
dbs.defineDatasource({
name: 'myDb',
adapter: 'memory',
});
// модель роли
dbs.defineModel({
name: 'role',
datasource: 'myDb',
properties: {
name: DataType.STRING,
},
});
// модель пользователя
dbs.defineModel({
name: 'user',
datasource: 'myDb',
properties: {
name: DataType.STRING,
roleId: DataType.NUMBER, // не обязательно
},
relations: {
role: {
type: RelationType.BELONGS_TO,
model: 'role',
foreignKey: 'roleId', // не обязательно
},
},
});
// создание роли
const roleRep = dbs.getRepository('role');
const role = await roleRep.create({
id: 5,
name: 'Manager',
});
console.log(role);
// {
// id: 5,
// name: 'manager'
// }
// создание пользователя
const userRep = dbs.getRepository('user');
const user = await userRep.create({
id: 1,
name: 'John Doe',
roleId: role.id,
});
console.log(user);
// {
// id: 1,
// name: 'John Doe',
// roleId: 5
// }
// извлечение пользователя и связанной роли (опция "include")
const userWithRole = await userRep.findById(user.id, {include: 'role'});
console.log(userWithRole);
// {
// id: 1,
// name: 'John Doe',
// roleId: 5,
// role: {
// id: 5,
// name: 'Manager'
// }
// }Has One
Обратная сторона belongsTo по принципу "один к одному".
Текущая (profile) Целевая (user)
┌─────────────────────────┐ ┌─────────────────────────┐
│ id: 5 <──────────────│───┐ │ id: 1 │
│ ... │ └───│── profileId: 5 │
└─────────────────────────┘ │ ... │
└─────────────────────────┘Определение связи:
// dbs.defineModel({
// name: 'user',
// relations: {
// profile: {
// type: RelationType.BELONGS_TO,
// model: 'profile',
// },
// },
// });
dbs.defineModel({
name: 'profile',
relations: {
user: { // название связи
type: RelationType.HAS_ONE, // целевая модель ссылается на текущую
model: 'user', // название целевой модели
foreignKey: 'profileId', // внешний ключ из целевой модели на текущую
},
},
});Has Many
Обратная сторона belongsTo по принципу "один ко многим".
Текущая (role) Целевая (user)
┌─────────────────────────┐ ┌─────────────────────────┐
│ id: 5 <──────────────│───┐ │ id: 1 │
│ ... │ ├───│── roleId: 5 │
└─────────────────────────┘ │ │ ... │
│ └─────────────────────────┘
│ ┌─────────────────────────┐
│ │ id: 2 │
└───│── roleId: 5 │
│ ... │
└─────────────────────────┘Определение связи:
// dbs.defineModel({
// name: 'user',
// relations: {
// role: {
// type: RelationType.BELONGS_TO,
// model: 'role',
// },
// },
// });
dbs.defineModel({
name: 'role',
relations: {
users: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'user', // название целевой модели
foreignKey: 'roleId', // внешний ключ целевой модели
},
},
});References Many
Текущая модель ссылается на целевую через массив идентификаторов.
Текущая (article) Целевая (category)
┌─────────────────────────┐ ┌─────────────────────────┐
│ id: 1 │ ┌───│─> id: 5 │
│ categoryIds: [5, 6] ──│───┤ │ ... │
│ ... │ │ └─────────────────────────┘
└─────────────────────────┘ │ ┌─────────────────────────┐
└───│─> id: 6 │
│ ... │
└─────────────────────────┘Определение связи:
// dbs.defineModel({name: 'category', ...
dbs.defineModel({
name: 'article',
relations: {
categories: { // название связи
type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов
model: 'category', // название целевой модели
foreignKey: 'categoryIds', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Ids"
},
},
});Belongs To (полиморфная версия)
Текущая модель ссылается на целевую по идентификатору. Название целевой модели определяется свойством-дискриминатором.
Текущая (file) ┌──────> Целевая 1 (letter)
┌─────────────────────────────┐ │ ┌─────────────────────────┐
│ id: 1 │ │ ┌──│─> id: 10 │
│ referenceType: 'letter' ─│──┘ │ │ ... │
│ referenceId: 10 ─────────│────┘ └─────────────────────────┘
└─────────────────────────────┘
┌──────> Целевая 2 (user)
┌─────────────────────────────┐ │ ┌─────────────────────────┐
│ id: 2 │ │ ┌──│─> id: 5 │
│ referenceType: 'user' ───│──┘ │ │ ... │
│ referenceId: 5 ──────────│────┘ └─────────────────────────┘
└─────────────────────────────┘Определение связи:
dbs.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
// полиморфный режим позволяет хранить название целевой модели
// в свойстве-дискриминаторе, которое формируется согласно
// названию связи с постфиксом "Type", и в данном случае
// название целевой модели хранит "referenceType",
// а идентификатор документа "referenceId"
polymorphic: true,
},
},
});Определение связи с указанием свойств:
dbs.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
polymorphic: true, // название целевой модели хранит дискриминатор
foreignKey: 'referenceId', // свойство для идентификатора цели
discriminator: 'referenceType', // свойство для названия целевой модели
},
},
});Has One (полиморфная версия)
Обратная сторона полиморфная связи belongsTo по принципу "один к одному".
Текущая (company) <───────┐ Целевая (license)
┌─────────────────────────┐ │ ┌─────────────────────────┐
│ id: 10 <─────────────│──┐ │ │ id: 1 │
│ ... │ │ └──│── ownerType: 'company' │
└─────────────────────────┘ └────│── ownerId: 10 │
└─────────────────────────┘Определение связи с указанием названия связи целевой модели:
// dbs.defineModel({
// name: 'license',
// relations: {
// owner: {
// type: RelationType.BELONGS_TO,
// polymorphic: true,
// },
// },
// });
dbs.defineModel({
name: 'company',
relations: {
license: { // название связи
type: RelationType.HAS_ONE, // целевая модель ссылается на текущую
model: 'license', // название целевой модели
polymorphic: 'owner', // название полиморфной связи целевой модели
},
},
});Определение связи с указанием свойств целевой модели:
// dbs.defineModel({
// name: 'license',
// relations: {
// owner: {
// type: RelationType.BELONGS_TO,
// polymorphic: true,
// foreignKey: 'ownerId',
// discriminator: 'ownerType',
// },
// },
// });
dbs.defineModel({
name: 'company',
relations: {
license: { // название связи
type: RelationType.HAS_ONE, // целевая модель ссылается на текущую
model: 'license', // название целевой модели
polymorphic: true, // название текущей модели находится в дискриминаторе
foreignKey: 'ownerId', // свойство целевой модели для идентификатора
discriminator: 'ownerType', // свойство целевой модели для названия текущей
},
},
});Has Many (полиморфная версия)
Обратная сторона полиморфная связи belongsTo по принципу "один ко многим".
Текущая (letter) <─────────┐ Целевая (file)
┌──────────────────────────┐ │ ┌────────────────────────────┐
│ id: 10 <──────────────│──┐ │ │ id: 1 │
│ ... │ │ ├──│── referenceType: 'letter' │
└──────────────────────────┘ ├─│──│── referenceId: 10 │
│ │ └────────────────────────────┘
│ │ ┌────────────────────────────┐
│ │ │ id: 2 │
│ └──│── referenceType: 'letter' │
└────│── referenceId: 10 │
└────────────────────────────┘Определение связи с указанием названия связи целевой модели:
// dbs.defineModel({
// name: 'file',
// relations: {
// reference: {
// type: RelationType.BELONGS_TO,
// polymorphic: true,
// },
// },
// });
dbs.defineModel({
name: 'letter',
relations: {
attachments: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'file', // название целевой модели
polymorphic: 'reference', // название полиморфной связи целевой модели
},
},
});Определение связи с указанием свойств целевой модели:
// dbs.defineModel({
// name: 'file',
// relations: {
// reference: {
// type: RelationType.BELONGS_TO,
// polymorphic: true,
// foreignKey: 'referenceId',
// discriminator: 'referenceType',
// },
// },
// });
dbs.defineModel({
name: 'letter',
relations: {
attachments: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'file', // название целевой модели
polymorphic: true, // название текущей модели находится в дискриминаторе
foreignKey: 'referenceId', // свойство целевой модели для идентификатора
discriminator: 'referenceType', // свойство целевой модели для названия текущей
},
},
});Расширение
Метод getRepository экземпляра DatabaseSchema проверяет наличие
существующего репозитория для указанной модели и возвращает его.
В противном случае создается новый экземпляр, который будет сохранен
для последующих обращений к методу.
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. После чего все новые
репозитории будут создаваться указанным конструктором вместо стандартного.
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); // truei. Так как экземпляры репозитория кэшируется, то замену конструктора
следует выполнять до обращения к методу getRepository.
TypeScript
Получение типизированного репозитория с указанием интерфейса модели.
import {DataType} from '@e22m4u/js-repository';
import {RelationType} from '@e22m4u/js-repository';
import {DatabaseSchema} from '@e22m4u/js-repository';
// const dbs = new DatabaseSchema();
// dbs.defineDatasource ...
// определение модели "city"
dbs.defineModel({
name: 'city',
datasource: 'myDatasource',
properties: {
name: DataType.STRING,
timeZone: DataType.STRING,
},
});
// определение интерфейса "city"
interface City {
id: number;
name?: string;
timeZone?: string;
}
// при получении репозитория нужной модели
// можно указать тип документов
const cityRep = dbs.getRepository<City>('city');
// теперь, методы репозитория возвращают
// тип City вместо Record<string, unknown>
const city: City = await cityRep.create({
name: 'Moscow',
timeZone: 'Europe/Moscow',
});Тесты
npm run testЛицензия
MIT
