@e22m4u/js-repository
TypeScript icon, indicating that this package has built-in type declarations

0.1.16 • Public • Published

@e22m4u/js-repository

Модуль для работы с базами данных для 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 только значение типа 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 значение по умолчанию
  • 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 включение связанных данных в результат

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

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.

TypeScript

Получение типизированного репозитория с указанием интерфейса модели.

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

Package Sidebar

Install

npm i @e22m4u/js-repository

Weekly Downloads

59

Version

0.1.16

License

MIT

Unpacked Size

1.48 MB

Total Files

255

Last publish

Collaborators

  • e22m4u