README.md 29 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)

create(data, filter = undefined)

Добавляет новый документ в коллекцию и возвращает его.

// вызываем метод `create` с передачей состава
// нового документа первым параметром
const person = await rep.create({
  name: 'Rick Sanchez',
  dimension: 'C-137',
  age: 67,
});

// выводим результат
console.log(person);
// {
//   id: 1,
//   name: 'Rick Sanchez',
//   dimension: 'C-137',
//   age: 67
// }

Использование параметра filter

// второй параметр принимает объект настроек
// возвращаемого результата (опционально)
const result = await rep.create(data, {
  // "fields" - если определено, то возвращаемый
  // документ будут включать только указанные поля
  fields: 'name',
  fields: ['name', 'age'],

  // "include" - включить в результат связанные
  // документы (см. Связанные документы)
  include: 'son',
  include: {son: 'hobbies'},
  include: ['son', 'daughter'],
});

replaceById(id, data, filter = undefined)

Заменяет существующий документ по идентификатору и возвращает его. Если идентификатор не найден, то выбрасывает исключение.

// документ с идентификатором 1 имеет
// следующую структуру
// {
//   id: 1,
//   name: 'Rick Sanchez',
//   dimension: 'C-137',
//   age: 67
// }

// вызываем метод `replaceById` с передачей
// идентификатора и нового состава
const person = await rep.replaceById(1, {
  name: 'Morty Smith',
  kind: 'a young teenage boy',
  age: 14,
});

// выводим результат
console.log(person);
// {
//   id: 1,
//   name: 'Morty Smith', <= значение обновлено
//   kind: 'a young teenage boy', <= добавлено новое поле
//   age: 14 <= значение обновлено
// }
// поле "dimension" удалено, так как
// не содержалось в новом составе

patchById(id, data, filter = undefined)

Частично обновляет существующий документ по идентификатору и возвращает его. Если идентификатор не найден, то выбрасывает исключение.

// идентификатор 24 имеет следующий
// состав документа
// {
//   "id": 24,
//   "type": "airport",
//   "name": "Domodedovo Airport",
//   "code": "DME"
// }

// вызываем метод `patchById` с передачей
// идентификатора и новых значений обновляемых
// полей
const result = await rep.patchById(24, {
  name: 'Sheremetyevo Airport',
  code: 'SVO',
  featured: true,
});

// выводим результат
console.log(result);
// {
//   "id": 24,
//   "type": "airport",
//   "name": "Sheremetyevo Airport", <= значение обновлено
//   "code": "SVO", <= значение обновлено
//   "featured": true <= добавлено новое поле
// }

patch(data, where = undefined)

Обновляет документы и возвращает их число. Используется для обновления нескольких документов согласно условиям выборки, или всей коллекции сразу.

// коллекция имеет два документа
// [
//   {
//     "id": 1,
//     "type": null,
//     "name": "Bangkok"
//   },
//   {
//     "id": 2,
//     "type": null,
//     "name": "Moscow"
//   }
// ]

// вызываем метод `patch` с передачей
// значений для обновляемых полей
const result = await rep.patch({
  type: 'city',
  updatedAt: new Date().toISOString(),
});

// выводим количество затронутых
// документов
console.log(result);
// 2

// методом `find` просматриваем коллекцию
// для проверки изменений
const docs = await rep.find();
console.log(docs);
// [
//   {
//     "id": 1,
//     "type": "city", <= значение обновлено
//     "name": "Bangkok",
//     "updatedAt": "2023-12-02T14:13:27.649Z" <= добавлено новое поле
//   },
//   {
//     "id": 2,
//     "type": "city", <= значение обновлено
//     "name": "Moscow",
//     "updatedAt": "2023-12-02T14:13:27.649Z" <= добавлено новое поле
//   }
// ]

Условия выборки обновляемых документов.

// второй параметр метода `patch` принимает
// условия выборки (опционально)
const result = await rep.patch(data, {
  type: 'city',          // поле "type" должно иметь значение "city"
  description: {         // оператор "like" проверяет поле "description"
    like: 'the capital', // на наличие подстроки "the capital"
  },
  updatedAt: {                      // оператор "lt" проверяет значение поля
    lt: '2023-12-02T21:00:00.000Z', // "updatedAt" на наличие более ранней даты,
  },                                // чем указана в условии
  // см. раздел "фильтрация" и "операторы"
});

find(filter = undefined)

Возвращает все документы коллекции, или использует условия выборки.

// вызов метода `find` без аргументов
// запрашивает все документы коллекции
const result = await rep.find();
console.log(result);
// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 2,
//     "title": "A Giant Bellows"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]

Фильтрация результата в параметре filter

// первый параметр принимает объект
// c настройками выборки (опционально)
const result = await rep.find({
  // "where" - фильтрация выборки по условию, где
  // указанные поля должны содержать определенные
  // значения (см. Фильтрация)
  where: {type: 'article', published: true},
  where: {description: {like: 'breaking news'}},
  where: {publishedAt: {lte: '2023-12-02T21:00:00.000Z'}},

  // "order" - сортировка по указанному полю может
  // принимать постфикс DESC для обратного порядка
  order: 'foo',
  order: 'bar DESC',
  order: ['foo', 'bar DESC'],

  // "limit" - ограничение результата
  // "skip" - пропуск документов
  limit: 10,
  skip: 10,

  // "fields" - если определено, то документы выборки
  // будут включать только указанные поля
  fields: 'title',
  fields: ['title', 'published'],

  // "include" - включить в результат связанные
  // документы (см. Связанные документы)
  include: 'author',
  include: {author: 'city'},
  include: ['author', 'categories'],
});

findOne(filter = undefined)

Возвращает первый найденный документ или undefined

// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 2,
//     "title": "A Giant Bellows"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]
const result = await rep.findOne();
console.log(result);
// {
//   "id": 1,
//   "title": "The Forgotten Ship"
// }

findById(id, filter = undefined)

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

// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 2,
//     "title": "A Giant Bellows"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]
const result = await rep.findById(2);
console.log(result);
// {
//   "id": 2,
//   "title": "A Giant Bellows"
// }

delete(where = undefined)

Удаляет документы коллекции.
Возвращает количество удаленных документов.

// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 2,
//     "title": "A Giant Bellows"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]
const result = await rep.delete();
console.log(result);
// 3

const docs = await rep.find();
console.log(docs);
// []

deleteById(id)

Удаляет документ по идентификатору.
Возвращает true в случае успеха или false если не найден.

// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 2,
//     "title": "A Giant Bellows"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]
const result = await rep.deleteById(2);
console.log(result);
// true

const docs = await rep.find();
console.log(docs);
// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]

exists(id)

Проверка существования документа по идентификатору.
Возвращает true если найден, в противном случае false.

// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 2,
//     "title": "A Giant Bellows"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]

const result1 = await rep.exists(2);
console.log(result1);
// true

const result2 = await rep.exists(10);
console.log(result2);
// false

count(where = undefined)

Подсчет количества документов.
Возвращает число найденных документов.

// [
//   {
//     "id": 1,
//     "title": "The Forgotten Ship"
//   },
//   {
//     "id": 2,
//     "title": "A Giant Bellows"
//   },
//   {
//     "id": 3,
//     "title": "Hundreds of bottles"
//   }
// ]
const result = await rep.count();
console.log(result);
// 3

Расширение

При использовании метода 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('myModel');
const rep2 = schema.getRepository('myModel');
console.log(rep1 === rep2); // true

Если требуется изменить или расширить поведение репозитория, то перед его созданием можно переопределить его конструктор методом setRepositoryCtor. После чего, все создаваемые репозитории будут использовать уже пользовательский конструктор вместо стандартного.

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.get(RepositoryRegistry).setRepositoryCtor(MyRepository);
// теперь schema.getRepository(modelName) будет возвращать
// экземпляр класса MyRepository

Тесты

npm run test

Лицензия

MIT