@e22m4u/js-repository v0.3.0
@e22m4u/js-repository
Реализация паттерна «Репозиторий» для работы с базами данных в Node.js
- Установка
- Импорт
- Описание
- Пример
- Схема баз данных
- Источник данных
- Модель
- Свойства
- Валидаторы
- Трансформеры
- Пустые значения
- Репозиторий
- Фильтрация
- Связи
- Расширение
- TypeScript
- Тесты
- Лицензия
Установка
npm install @e22m4u/js-repositoryОпционально устанавливаем адаптер.
| описание | |
|---|---|
memory | виртуальная база в памяти процесса (не требует установки) |
mongodb | MongoDB - система управления NoSQL базами (установка) |
Импорт
Модуль поддерживает 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Пример
Объявление источника данных, модели и добавление нового документа в коллекцию.
import {DataType} from '@e22m4u/js-repository';
import {DatabaseSchema} from '@e22m4u/js-repository';
// создание экземпляра DatabaseSchema
const dbs = new DatabaseSchema();
// объявление источника "myMemory"
dbs.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});
// объявление модели "country"
dbs.defineModel({
name: 'country', // название новой модели
datasource: 'myMemory', // выбранный источник
properties: { // свойства модели
name: DataType.STRING, // тип "string"
population: DataType.NUMBER, // тип "number"
},
})
// получение репозитория модели "country"
const countryRep = dbs.getRepository('country');
// добавление нового документа в коллекцию "country"
const country = await countryRep.create({
name: 'Russia',
population: 143400000,
});
// вывод нового документа
console.log(country);
// {
// "id": 1,
// "name": "Russia",
// "population": 143400000,
// }Схема баз данных
Экземпляр класса DatabaseSchema хранит определения источников данных и моделей.
Методы
defineDatasource(datasourceDef: object): this- добавить источникdefineModel(modelDef: object): this- добавить модельgetRepository(modelName: string): Repository- получить репозиторий
Примеры
Импорт класса и создание экземпляра схемы.
import {DatabaseSchema} from '@e22m4u/js-repository';
const dbs = new DatabaseSchema();Определение нового источника.
dbs.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});Определение новой модели.
dbs.defineModel({
name: 'product', // название новой модели
datasource: 'myMemory', // выбранный источник
properties: { // свойства модели
name: DataType.STRING,
weight: DataType.NUMBER,
},
});Получение репозитория по названию модели.
const productRep = dbs.getRepository('product');Источник данных
Источник хранит название выбранного адаптера и его настройки. Определение
нового источника выполняется методом defineDatasource экземпляра
DatabaseSchema.
Параметры
name: stringуникальное названиеadapter: stringвыбранный адаптер- параметры адаптера (если имеются)
Примеры
Определение нового источника.
dbs.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});Передача дополнительных параметров адаптера.
dbs.defineDatasource({
name: 'myMongodb',
adapter: 'mongodb',
// параметры адаптера "mongodb"
host: '127.0.0.1',
port: 27017,
database: 'myDatabase',
});Модель
Описывает структуру документа коллекции и связи к другим моделям. Определение
новой модели выполняется методом defineModel экземпляра DatabaseSchema.
Параметры
name: stringназвание модели (обязательно)base: stringназвание наследуемой моделиtableName: stringназвание коллекции в базеdatasource: stringвыбранный источник данныхproperties: objectопределения свойств (см. Свойства)relations: objectопределения связей (см. Связи)
Примеры
Определение модели со свойствами указанного типа.
dbs.defineModel({
name: 'user', // название новой модели
properties: { // свойства модели
name: DataType.STRING,
age: DataType.NUMBER,
},
});Свойства
Параметр properties находится в определении модели и принимает объект, ключи
которого являются свойствами этой модели, а значением тип свойства или объект
с дополнительными параметрами.
Тип данных
DataType.ANYразрешено любое значениеDataType.STRINGтолько значение типаstringDataType.NUMBERтолько значение типаnumberDataType.BOOLEANтолько значение типаbooleanDataType.ARRAYтолько значение типаarrayDataType.OBJECTтолько значение типаobject
Параметры
type: stringтип допустимого значения (обязательно)itemType: stringтип элемента массива (дляtype: 'array')model: stringмодель объекта (дляtype: 'object')primaryKey: booleanобъявить свойство первичным ключомcolumnName: stringпереопределение названия колонкиcolumnType: stringтип колонки (определяется адаптером)required: booleanобъявить свойство обязательнымdefault: anyзначение по умолчаниюvalidate: string | array | objectсм. Валидаторыunique: boolean | stringпроверять значение на уникальность
Параметр unique
Если значением параметра unique является true или 'strict', то выполняется
строгая проверка на уникальность. В этом режиме пустые значения
так же подлежат проверке, где null и undefined не могут повторяться более одного
раза.
Режим 'sparse' проверяет только значения с полезной нагрузкой, исключая
пустые значения, список которых отличается в зависимости
от типа свойства. Например, для типа string пустым значением будет undefined,
null и '' (пустая строка).
unique: true | 'strict'строгая проверка на уникальностьunique: 'sparse'исключить из проверки пустые значенияunique: false | 'nonUnique'не проверять на уникальность (по умолчанию)
В качестве значений параметра unique можно использовать предопределенные
константы как эквивалент строковых значений strict, sparse и nonUnique.
PropertyUniqueness.STRICTPropertyUniqueness.SPARSEPropertyUniqueness.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.UNIQUE, // проверять уникальность
},
},
});Фабричное значение по умолчанию. Возвращаемое значение функции будет определено в момент записи документа.
dbs.defineModel({
name: 'article',
properties: { // свойства модели
tags: {
type: DataType.ARRAY, // тип свойства "array" (обязательно)
itemType: DataType.STRING, // тип элемента "string"
default: () => [], // фабричное значение
},
createdAt: {
type: DataType.STRING, // тип свойства "string" (обязательно)
default: () => new Date().toISOString(), // фабричное значение
},
},
});Валидаторы
Кроме проверки типа, дополнительные условия можно задать с помощью валидаторов, через которые будет проходить значение свойства перед записью в базу. Исключением являются пустые значения, которые не подлежат проверке.
minLength: numberминимальная длинна строки или массиваmaxLength: numberмаксимальная длинна строки или массиваregexp: string | RegExpпроверка по регулярному выражению
Пример
Валидаторы указываются в объявлении свойства модели параметром
validate, который принимает объект с их названиями и настройками.
dbs.defineModel({
name: 'user',
properties: {
name: {
type: DataType.STRING,
validate: { // валидаторы свойства "name"
minLength: 2, // минимальная длинна строки
maxLength: 24, // максимальная длинна строки
},
},
},
});Пользовательские валидаторы
Валидатором является функция, в которую передается значение соответствующего
поля перед записью в базу. Если во время проверки функция возвращает false,
то выбрасывается стандартная ошибка. Подмена стандартной ошибки возможна
с помощью выброса пользовательской ошибки непосредственно внутри функции.
Регистрация пользовательского валидатора выполняется методом addValidator
сервиса PropertyValidatorRegistry, который принимает новое название
и функцию для проверки значения.
Пример
// создание валидатора для запрета
// всех символов кроме чисел
const numericValidator = (input) => {
return /^[0-9]+$/.test(String(input));
}
// регистрация валидатора "numeric"
dbs.get(PropertyValidatorRegistry).addValidator('numeric', numericValidator);
// использование валидатора в определении
// свойства "code" для новой модели
dbs.defineModel({
name: 'document',
properties: {
code: {
type: DataType.STRING,
validate: 'numeric',
},
},
});Трансформеры
С помощью трансформеров производится модификация значений определенных полей перед записью в базу. Трансформеры позволяют указать какие изменения нужно производить с входящими данными. Исключением являются пустые значения, которые не подлежат трансформации.
trimудаление пробельных символов с начала и конца строкиtoUpperCaseперевод строки в верхний регистрtoLowerCaseперевод строки в нижний регистрtoTitleCaseперевод строки в регистр заголовка
Пример
Трансформеры указываются в объявлении свойства модели параметром
transform, который принимает название трансформера. Если требуется
указать несколько названий, то используется массив. Если трансформер
имеет настройки, то используется объект, где ключом является название
трансформера, а значением его параметры.
dbs.defineModel({
name: 'user',
properties: {
name: {
type: DataType.STRING,
transform: [ // трансформеры свойства "name"
'trim', // удалить пробелы в начале и конце строки
'toTitleCase', // перевод строки в регистр заголовка
],
},
},
});Пустые значения
Разные типы свойств имеют свои наборы пустых значений. Эти наборы
используются для определения наличия полезной нагрузки в значении
свойства. Например, параметр default в определении свойства
устанавливает значение по умолчанию, только если входящее значение
является пустым. Параметр required исключает пустые значения
выбрасывая ошибку. А параметр unique в режиме sparse наоборот
допускает дублирование пустых значений уникального свойства.
| тип | пустые значения |
|---|---|
'any' | undefined, null |
'string' | undefined, null, '' |
'number' | undefined, null, 0 |
'boolean' | undefined, null |
'array' | undefined, null, [] |
'object' | undefined, null, {} |
Репозиторий
Выполняет операции чтения и записи документов определенной модели.
Получить репозиторий можно методом getRepository экземпляра DatabaseSchema.
Методы
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параметры возвращаемого результата (см. Фильтрация)
Примеры
Получение репозитория по названию модели.
const countryRep = dbs.getRepository('country');Добавление нового документа в коллекцию.
const res = await countryRep.create({
name: 'Russia',
population: 143400000,
});
console.log(res);
// {
// "id": 1,
// "name": "Russia",
// "population": 143400000,
// }Поиск документа по идентификатору.
const res = await countryRep.findById(1);
console.log(res);
// {
// "id": 1,
// "name": "Russia",
// "population": 143400000,
// }Удаление документа по идентификатору.
const res = await countryRep.deleteById(1);
console.log(res); // trueФильтрация
Некоторые методы репозитория принимают объект настроек влияющий
на возвращаемый результат. Максимально широкий набор таких настроек
имеет первый параметр метода find, где ожидается объект содержащий
набор опций указанных ниже.
where: objectобъект выборкиorder: string[]указание порядкаlimit: numberограничение количества документовskip: numberпропуск документовfields: string[]выбор необходимых свойств моделиinclude: objectвключение связанных данных в результат
where
Параметр принимает объект с условиями выборки и поддерживает широкий набор операторов сравнения.
{foo: 'bar'} поиск по значению свойства foo{foo: {eq: 'bar'}} оператор равенства eq{foo: {neq: 'bar'}} оператор неравенства neq{foo: {gt: 5}} оператор "больше" gt{foo: {lt: 10}} оператор "меньше" lt{foo: {gte: 5}} оператор "больше или равно" gte{foo: {lte: 10}} оператор "меньше или равно" lte{foo: {inq: ['bar', 'baz']}} равенство одного из значений inq{foo: {nin: ['bar', 'baz']}} исключение значений массива nin{foo: {between: [5, 10]}} оператор диапазона between{foo: {exists: true}} оператор наличия значения exists{foo: {like: 'bar'}} оператор поиска подстроки like{foo: {ilike: 'BaR'}} регистронезависимая версия ilike{foo: {nlike: 'bar'}} оператор исключения подстроки nlike{foo: {nilike: 'BaR'}} регистронезависимая версия nilike{foo: {regexp: 'ba.+'}} оператор регулярного выражения regexp{foo: {regexp: 'ba.+', flags: 'i'}} флаги регулярного выражения
i. Условия можно объединять операторами and, or и nor.
Примеры
Применение условий выборки при подсчете документов.
const res = await rep.count({
authorId: 251,
publishedAt: {
lte: '2023-12-02T14:00:00.000Z',
},
});Применение оператора or при удалении документов.
const res = await rep.delete({
or: [
{draft: true},
{title: {like: 'draft'}},
],
});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"
},
},
});Связи
Параметр relations находится в определении модели и принимает
объект, ключ которого является названием связи, а значением объект
с параметрами.
Параметры
type: stringтип связиmodel: stringназвание целевой моделиforeignKey: stringсвойство текущей модели для идентификатора целиpolymorphic: boolean|stringобъявить связь полиморфной*discriminator: stringсвойство текущей модели для названия целевой*
i. Полиморфный режим позволяет динамически определять целевую модель по ее названию, которое хранит документ в свойстве-дискриминаторе.
Тип связи
belongsTo- текущая модель содержит свойство для идентификатора целиhasOne- обратная сторонаbelongsToпо принципу "один к одному"hasMany- обратная сторонаbelongsToпо принципу "один ко многим"referencesMany- документ содержит массив с идентификаторами целевой модели
Примеры
Объявление связи belongsTo
dbs.defineModel({
name: 'user',
relations: {
role: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
model: 'role', // название целевой модели
foreignKey: 'roleId', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Id"
},
},
});Объявление связи hasMany
dbs.defineModel({
name: 'role',
relations: {
users: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'user', // название целевой модели
foreignKey: 'roleId', // внешний ключ из целевой модели на текущую
},
},
});Объявление связи referencesMany
dbs.defineModel({
name: 'article',
relations: {
categories: { // название связи
type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов
model: 'category', // название целевой модели
foreignKey: 'categoryIds', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Ids"
},
},
});Полиморфная версия belongsTo
dbs.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
// полиморфный режим позволяет хранить название целевой модели
// в свойстве-дискриминаторе, которое формируется согласно
// названию связи с постфиксом "Type", и в данном случае
// название целевой модели хранит "referenceType",
// а идентификатор документа "referenceId"
polymorphic: true,
},
},
});Полиморфная версия belongsTo с указанием свойств.
dbs.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
polymorphic: true, // название целевой модели хранит дискриминатор
foreignKey: 'referenceId', // свойство для идентификатора цели
discriminator: 'referenceType', // свойство для названия целевой модели
},
},
});Полиморфная версия hasMany с указанием названия связи целевой модели.
dbs.defineModel({
name: 'letter',
relations: {
attachments: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'file', // название целевой модели
polymorphic: 'reference', // название полиморфной связи целевой модели
},
},
});Полиморфная версия hasMany с указанием свойств целевой модели.
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.get(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 ...
// dbs.defineModel ...
// определение модели "city"
dbs.defineModel({
name: 'city',
datasource: 'myDatasource',
properties: {
title: DataType.STRING,
timeZone: DataType.STRING,
},
relations: {
country: {
type: RelationType.BELONGS_TO,
model: 'country',
},
},
});
// определение интерфейса "city"
interface City {
id: number;
title?: string;
timeZone?: string;
countryId?: number;
country?: Country;
}
// получаем репозиторий по названию модели
// указывая ее тип и тип идентификатора
const cityRep = dbs.getRepository<City, number>('city');Тесты
npm run testЛицензия
MIT
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
6 months ago
1 year ago
7 months ago
7 months ago
1 year ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago