Реализация репозитория для работы с базами данных
|
|
1 year ago | |
|---|---|---|
| .husky | 1 year ago | |
| assets | 1 year ago | |
| src | 1 year ago | |
| .c8rc | 2 years ago | |
| .commitlintrc | 2 years ago | |
| .editorconfig | 2 years ago | |
| .eslintignore | 1 year ago | |
| .eslintrc.cjs | 1 year ago | |
| .gitignore | 2 years ago | |
| .mocharc.cjs | 1 year ago | |
| .prettierrc | 2 years ago | |
| LICENSE | 1 year ago | |
| README.md | 1 year ago | |
| mocha.setup.js | 1 year ago | |
| package.json | 1 year ago | |
| tsconfig.json | 1 year ago |
Модуль для работы с базами данных для Node.js
npm install @e22m4u/js-repository
Опционально устанавливаем адаптер.
| описание | |
|---|---|
memory |
виртуальная база в памяти процесса (не требует установки) |
mongodb |
MongoDB - система управления NoSQL базами (установка) |
Модуль позволяет абстрагироваться от различных интерфейсов баз данных, представляя их как именованные источники данных, подключаемые к моделям. Модель же описывает таблицу базы, колонки которой являются свойствами модели. Свойства модели могут иметь определенный тип допустимого значения, набор валидаторов и трансформеров, через которые проходят данные перед записью в базу. Кроме того, модель может определять классические связи «один к одному», «один ко многим» и другие типы отношений между моделями.
Непосредственно чтение и запись данных производится с помощью репозитория, который имеет каждая модель с объявленным источником данных. Репозиторий может фильтровать запрашиваемые документы, выполнять валидацию свойств согласно определению модели, и встраивать связанные данные в результат выборки.
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 {Schema} from '@e22m4u/js-repository';
import {DataType} from '@e22m4u/js-repository';
// создание экземпляра Schema
const schema = new Schema();
// определение источника "myMemory"
schema.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});
// определение модели "country"
schema.defineModel({
name: 'country', // название новой модели
datasource: 'myMemory', // выбранный источник
properties: { // свойства модели
name: DataType.STRING, // тип "string"
population: DataType.NUMBER, // тип "number"
},
})
// получение репозитория модели "country"
const countryRep = schema.getRepository('country');
// добавление нового документа в коллекцию "country"
const country = await countryRep.create({
name: 'Russia',
population: 143400000,
});
// вывод нового документа
console.log(country);
// {
// "id": 1,
// "name": "Russia",
// "population": 143400000,
// }
Экземпляр класса Schema хранит определения источников
данных и моделей.
Методы
defineDatasource(datasourceDef: object): this -
добавить источникdefineModel(modelDef: object): this - добавить
модельgetRepository(modelName: string): Repository - получить
репозиторийПримеры
Импорт класса и создание экземпляра схемы.
import {Schema} from '@e22m4u/js-repository';
const schema = new Schema();
Определение нового источника.
schema.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});
Определение новой модели.
schema.defineModel({
name: 'product', // название новой модели
datasource: 'myMemory', // выбранный источник
properties: { // свойства модели
name: DataType.STRING,
weight: DataType.NUMBER,
},
});
Получение репозитория по названию модели.
const productRep = schema.getRepository('product');
Источник хранит название выбранного адаптера и его настройки.
Определить новый источник можно методом defineDatasource
экземпляра схемы.
Параметры
name: string уникальное названиеadapter: string выбранный адаптерПримеры
Определение нового источника.
schema.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});
Передача дополнительных параметров адаптера.
schema.defineDatasource({
name: 'myMongodb',
adapter: 'mongodb',
// параметры адаптера "mongodb"
host: '127.0.0.1',
port: 27017,
database: 'myDatabase',
});
Описывает структуру документа коллекции и связи к другим моделям.
Определить новую модель можно методом defineModel
экземпляра схемы.
Параметры
name: string название модели (обязательно)base: string название наследуемой моделиtableName: string название коллекции в базеdatasource: string выбранный источник данныхproperties: object определения свойств (см. Свойства)relations: object определения связей (см. Связи)Примеры
Определение модели со свойствами указанного типа.
schema.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' не проверять на
уникальность (по умолчанию)Примеры
Краткое определение свойств модели.
schema.defineModel({
name: 'city',
properties: { // свойства модели
name: DataType.STRING, // тип свойства "string"
population: DataType.NUMBER, // тип свойства "number"
},
});
Расширенное определение свойств модели.
schema.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, // проверять уникальность
},
},
});
Фабричное значение по умолчанию. Возвращаемое значение функции будет определено в момент записи документа.
schema.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, который принимает объект с их названиями и
настройками.
schema.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"
schema
.get(PropertyValidatorRegistry)
.addValidator('numeric', numericValidator);
// использование валидатора в определении
// свойства "code" для новой модели
schema.defineModel({
name: 'document',
properties: {
code: {
type: DataType.STRING,
validate: 'numeric',
},
},
});
С помощью трансформеров производится модификация значений определенных полей перед записью в базу. Трансформеры позволяют указать какие изменения нужно производить с входящими данными. Исключением являются пустые значения, которые не подлежат трансформации.
trim удаление пробельных символов с начала и конца
строкиtoUpperCase перевод строки в верхний регистрtoLowerCase перевод строки в нижний регистрtoTitleCase перевод строки в регистр заголовкаПример
Трансформеры указываются в объявлении свойства модели параметром
transform, который принимает название трансформера. Если
требуется указать несколько названий, то используется массив. Если
трансформер имеет настройки, то используется объект, где ключом является
название трансформера, а значением его параметры.
schema.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 экземпляра
схемы.
Методы
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 = schema.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 включение связанных данных в
результатПараметр принимает объект с условиями выборки и поддерживает широкий набор операторов сравнения.
{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'}},
],
});
Параметр упорядочивает выборку по указанным свойствам модели.
Обратное направление порядка можно задать постфиксом 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 указывать
необязательно.
Параметр включает связанные документы в результат вызываемого метода. Названия включаемых связей должны быть определены в текущей модели. (см. Связи)
Примеры
Включение связи по названию.
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
schema.defineModel({
name: 'user',
relations: {
role: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
model: 'role', // название целевой модели
foreignKey: 'roleId', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Id"
},
},
});
Объявление связи hasMany
schema.defineModel({
name: 'role',
relations: {
users: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'user', // название целевой модели
foreignKey: 'roleId', // внешний ключ из целевой модели на текущую
},
},
});
Объявление связи referencesMany
schema.defineModel({
name: 'article',
relations: {
categories: { // название связи
type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов
model: 'category', // название целевой модели
foreignKey: 'categoryIds', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Ids"
},
},
});
Полиморфная версия belongsTo
schema.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
// полиморфный режим позволяет хранить название целевой модели
// в свойстве-дискриминаторе, которое формируется согласно
// названию связи с постфиксом "Type", и в данном случае
// название целевой модели хранит "referenceType",
// а идентификатор документа "referenceId"
polymorphic: true,
},
},
});
Полиморфная версия belongsTo с указанием свойств.
schema.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
polymorphic: true, // название целевой модели хранит дискриминатор
foreignKey: 'referenceId', // свойство для идентификатора цели
discriminator: 'referenceType', // свойство для названия целевой модели
},
},
})
Полиморфная версия hasMany с указанием названия связи
целевой модели.
schema.defineModel({
name: 'letter',
relations: {
attachments: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'file', // название целевой модели
polymorphic: 'reference', // название полиморфной связи целевой модели
},
},
})
Полиморфная версия hasMany с указанием свойств целевой
модели.
schema.defineModel({
name: 'letter',
relations: {
attachments: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'file', // название целевой модели
polymorphic: true, // название текущей модели находится в дискриминаторе
foreignKey: 'referenceId', // свойство целевой модели для идентификатора
discriminator: 'referenceType', // свойство целевой модели для названия текущей
},
},
})
Метод 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('model');
const rep2 = schema.getRepository('model');
console.log(rep1 === rep2); // true
Подмена стандартного конструктора репозитория выполняется методом
setRepositoryCtor сервиса RepositoryRegistry,
который находится в контейнере экземпляра схемы. После чего все новые
репозитории будут создаваться указанным конструктором вместо
стандартного.
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.defineDatasource ...
// schema.defineModel ...
schema.get(RepositoryRegistry).setRepositoryCtor(MyRepository);
const rep = schema.getRepository('model');
console.log(rep instanceof MyRepository); // true
i. Так как экземпляры репозитория кэшируется, то замену
конструктора следует выполнять до обращения к методу
getRepository.
Получение типизированного репозитория с указанием интерфейса модели.
import {Schema} from '@e22m4u/js-repository';
import {DataType} from '@e22m4u/js-repository';
import {RelationType} from '@e22m4u/js-repository';
// const schema = new Schema();
// schema.defineDatasource ...
// schema.defineModel ...
// определение модели "city"
schema.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;
// открыть свойства (опционально)
[property: string]: unknown;
}
// получаем репозиторий по названию модели
// указывая ее тип и тип идентификатора
const cityRep = schema.getRepository<City, number>('city');
npm run test
MIT