README.md 47 KB

@e22m4u/js-repository

Абстракция для работы с базами данных для Node.js

Установка

npm install @e22m4u/js-repository

Опционально устанавливаем адаптер. Например, если используется MongoDB, то для подключения потребуется установить адаптер mongodb как отдельную зависимость.

npm install @e22m4u/js-repository-mongodb-adapter

Список доступных адаптеров:

адаптер описание
memory виртуальная база в памяти процесса (для разработки и тестирования)
mongodb MongoDB - система управления NoSQL базами (требует установки)

Концепция

Модуль позволяет спроектировать систему связанных данных, доступ к которым осуществляется посредством репозиториев. Каждый репозиторий имеет собственную модель данных, которая описывает структуру документа коллекции, а так же определяет связи к другим моделям.

flowchart LR

A[Datasource]-->B[Data Model]-->С[Repository];

Точка входа

Определения источников и моделей хранятся в экземпляре класса Schema, и первым шагом будет создание данного экземпляра.

import {Schema} from '@e22m4u/js-repository';

const schema = new Schema();

Интерфейс Schema содержит три основных метода:

  • defineDatasource(datasourceDef: object): this - добавить источник
  • defineModel(modelDef: object): this - добавить модель
  • getRepository(modelName: string): Repository - получить репозиторий

Источник данных

Источник описывает способ подключения к базе и используемый адаптер. Если адаптер имеет настройки, то они передаются вместе с объектом определения источника методом defineDatasource, как это показано ниже.

schema.defineDatasource({
  name: 'myMongo', // название нового источника
  adapter: 'mongodb', // название выбранного адаптера
  // настройки адаптера mongodb
  host: '127.0.0.1',
  port: 27017,
  database: 'data'
});

Параметры источника:

  • name: string уникальное название
  • adapter: string выбранный адаптер

При желании можно использовать встроенный адаптер memory, который хранит данные в памяти процесса. У него нет специальных настроек, и он отлично подходит для тестов и прототипирования.

schema.defineDatasource({
  name: 'myMemory', // название источника
  adapter: 'memory', // выбранный адаптер
});

Модель данных

Когда источники определены, можно перейти к описанию моделей данных. Модель может определять как структуру какого-либо объекта, так и являться отражением реальной коллекции базы.

Представьте себе коллекцию торговых точек, у каждой из которых имеются координаты lat и lng. Мы можем заранее определить модель для объекта координат методом defineModel и использовать ее в других коллекциях.

schema.defineModel({
  name: 'latLng', // название новой модели
  properties: { // поля модели
    lat: DataType.NUMBER, // поле широты
    lng: DataType.NUMBER, // поле долготы
  },
});

Параметры модели:

  • name: string уникальное название (обязательно)
  • datasource: string выбранный источник данных
  • properties: object определения полей модели
  • relations: object определения связей

Параметр properties принимает объект, ключи которого являются именами полей, а значением тип поля или объект с дополнительными параметрами.

schema.defineModel({
  name: 'latLng',
  properties: {
    lat: DataType.NUMBER, // краткое определение поля "lat"
    lng: { // расширенное определение поля "lng"
      type: DataType.NUMBER, // тип допустимого значения
      required: true, // исключает null и undefined
    },
  },
});

Типы данных:

  • DataType.ANY
  • DataType.STRING
  • DataType.NUMBER
  • DataType.BOOLEAN
  • DataType.ARRAY
  • DataType.OBJECT

Модель latLng всего лишь описывает структуру объекта координат, тогда как торговая точка должна иметь реальную таблицу в базе. По аналогии с предыдущим примером, добавим модель place, но дополнительно укажем источник данных в параметре datasource

schema.defineModel({
  name: 'place',
  datasource: 'myMemory', // выбранный источник данных
  properties: {
    name: DataType.STRING, // поле для названия торговой точки
    location: { // поле для объекта координат
      type: DataType.OBJECT, // допускать только объекты
      model: 'latLng', // модель для объекта координат
    },
  },
});

В примере выше мы использовали модель latLng как структуру допустимого значения поля location. Возможный документ данной коллекции может выглядеть так:

{
  "id": 1,
  "name": "Burger King",
  "location": {
    "lat": 32.412891,
    "lng": 34.7660061
  }
}

Стоит обратить внимание, что мы могли бы не объявлять параметр properties, при этом теряя возможность проверки данных перед записью в базу.

schema.defineModel({
  name: 'place',
  adapter: 'myMemory',
  // параметр "properties" не указан
});

Параметры поля:

  • type: string тип допустимого значения (обязательно)
  • itemType: string тип элемента массива (для type: 'array')
  • model: string модель объекта (для type: 'object')
  • primaryKey: boolean объявить поле первичным ключом
  • columnName: string переопределение названия колонки
  • columnType: string тип колонки (определяется адаптером)
  • required: boolean объявить поле обязательным
  • default: any значение по умолчанию

Репозиторий

В отличие от latLng, модель place имеет источник данных с названием myMemory, который был объявлен ранее. Наличие источника позволяет получить репозиторий по названию модели.

const rep = schema.getRepository('place');

Методы:

  • create(data, filter = undefined) добавить новый документ
  • replaceById(id, 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 идентификатор (первичный ключ)
  • data объект отражающий состав документа
  • where параметры выборки (см. Фильтрация)
  • filter параметры возвращаемого результата (см. Фильтрация)

Фильтрация

Некоторые методы репозитория опционально принимают параметр where для условий выборки и/или параметр filter управляющий возвращаемым значением.

where

Рассмотрим применение параметра where на примере метода patch, который выполняет поиск и обновление документов по определенному условию.

Сигнатура метода patch(data, where = undefined) указывает на два параметра, где первый принимает объект данных, а второй является опциональным объектом выборки.

// вызов метода `patch`
await rep.patch(
  // обновить значения полей
  // "hidden" и "updatedAt"
  {
    hidden: true,
    updatedAt: new Date().toISOString(),
  },
  // только в тех документах, где поле "type"
  // равно значению "article", а "publishedAt"
  // содержит дату более раннюю, чем указана
  // в операторе "lte"
  {
    type: 'article',
    publishedAt: {
      lte: '2023-12-02T14:00:00.000Z',
    }
  },
);

Второй аргумент метода patch предыдущего листинга содержит условия выборки по которым будет выполнен поиск и обновление документов. Полный список возможных операторов сравнения приводится ниже.

{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'}} флаги регулярного выражения

filter

Методы репозитория асинхронны и возвращают Promise с некоторым значением. Параметр filter позволяет влиять на это значение более широким образом, чем просто ограничение для затрагиваемых данных. Ниже представлен список опциональных полей объекта filter, одним из которых является where (рассматривался ранее).

  • where объект выборки
  • order указание порядка
  • limit ограничение количества документов
  • skip пропуск документов
  • fields выбор необходимых полей документа
  • include включение связанных данных в результат (см. Связи)

При использовании метода find может потребоваться комбинация параметров limit и skip для механизма пагинации.

// первый параметр метода `find` принимает
// объект настроек возвращаемого результата
const result = await rep.find({
  limit: 12,
  skip: 24,
});

Документы могут быть отсортированы по указанным полям и в нужном направлении, где ASC - по умолчанию, или DESC - в обратном порядке.

const result = await rep.find({
  // по полю "featured"
  order: 'featured',
  // по полю "publishedAt" в обратном порядке
  order: 'publishedAt DESC',
  // по нескольким полям в разных направлениях
  order: ['publishedAt DESC', 'featured ASC', 'id'],
});

Сократить объем документа можно параметром fields указав необходимый набор полей.

const result = await rep.find({
  // включить в результат только поле "title"
  fields: 'title',
  // или поле "title", "createdAt" и "featured" 
  fields: ['title', 'createdAt', 'featured'],
  // первичный ключ документа
  // указывать не обязательно
});

Тесты

npm run test

Лицензия

MIT