Реализация репозитория для работы с базами данных

e22m4u 386e4d46d2 chore: updates README.md 1 year ago
.husky 711475680a chore: adds api documentation 2 years ago
docs 386e4d46d2 chore: updates README.md 1 year ago
src 23fd7a3d34 fix: replaceOrCreate method should check document existence before create 1 year ago
.c8rc dbff18d833 chore: initial commit 2 years ago
.commitlintrc dbff18d833 chore: initial commit 2 years ago
.editorconfig dbff18d833 chore: initial commit 2 years ago
.eslintignore 711475680a chore: adds api documentation 2 years ago
.eslintrc.cjs 85139bcc41 chore: adds *.d.ts 2 years ago
.gitignore dbff18d833 chore: initial commit 2 years ago
.mocharc.cjs dbff18d833 chore: initial commit 2 years ago
.prettierrc dbff18d833 chore: initial commit 2 years ago
LICENSE dbff18d833 chore: initial commit 2 years ago
README.md 386e4d46d2 chore: updates README.md 1 year ago
mocha.setup.js dbff18d833 chore: initial commit 2 years ago
package.json 06f6f3082d chore: upgrades dev dependencies 1 year ago
tsconfig.json 771d9e379e chore: renames to @e22m4u/js-repository and fixes d.ts files 2 years ago
typedoc.json 711475680a chore: adds api documentation 2 years ago

README.md

@e22m4u/js-repository

Модуль для работы с базами данных для Node.js

API

Установка

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 значение по умолчанию

Примеры

Краткое определение свойств модели.

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, // значение по умолчанию
    },
  },
});

Фабричное значение по умолчанию. Возвращаемое значение функции будет определено в момент записи документа.

schema.defineModel({
  name: 'article',
  properties: { // свойства модели
    tags: {
      type: DataType.ARRAY, // тип свойства "array" (обязательно)
      itemType: DataType.STRING, // тип элемента "string"
      default: () => [], // фабричное значение
    },
    createdAt: {
      type: DataType.STRING, // тип свойства "string" (обязательно)
      default: () => new Date().toISOString(), // фабричное значение
    },
  },
});

Репозиторий

Выполняет операции чтения и записи документов определенной модели. Получить репозиторий можно методом 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