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

e22m4u 4338735be1 chore: updates dependencies 2 years ago
.husky d2c55fccc0 chore: revert "builds as cjs" 2 years ago
src e90312d348 fix: tests of hasMany relation, relation typings and README.md 2 years 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 730af0ba6c chore: adds *.d.ts 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 b42927e5ff chore: update README.md 2 years ago
mocha.setup.js dbff18d833 chore: initial commit 2 years ago
package.json 4338735be1 chore: updates dependencies 2 years ago
tsconfig.json 771d9e379e chore: renames to @e22m4u/js-repository and fixes d.ts files 2 years ago

README.md

@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)
  • replaceOrCreate(data, filter = undefined)
  • patch(data, where = undefined)
  • patchById(id, data, filter = 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 place = await rep.create({
  "name": "Burger King",
  "location": {
    "lat": 32.412891,
    "lng": 34.7660061
  },
});

console.log(place);
// {
//   "id": 1,
//   "name": "Burger King",
//   "location": {
//     "lat": 32.412891,
//     "lng": 34.7660061
//   }
// }

replaceById(id, data, filter = undefined)

Добавленный в базу документ можно полностью заменить зная его идентификатор. Воспользуемся методом replaceById, который перезапишет данные по значению первичного ключа.

// {
//   "id": 1,
//   "name": "Burger King",
//   "location": {
//     "lat": 32.412891,
//     "lng": 34.7660061
//   }
// }
const result = rep.replaceById(place.id, {
  name: 'Starbucks',
  address: 'Sukhumvit Alley'
});

console.log(result);
// {
//   "id": 1,
//   "name": "Starbucks",
//   "address": "Sukhumvit Alley"
// }

replaceOrCreate(data, filter = undefined)

Если вы знакомы с методом PUT в архитектуре REST, когда документ добавляется, если его не существовало, или же обновляется существующий, то replaceOrCreate работает схожим образом. Если параметр data передаваемый первым аргументом будет содержать идентификатор, то метод будет вести себя как replaceById, в противном случае будет создан новый документ.

// {
//   "id": 1,
//   "name": "Starbucks",
//   "address": "Sukhumvit Alley"
// }
const result = rep.replaceOrCreate({
  id: 1,
  name: 'Airport',
  city: 'Antalya',
  code: 'AYT'
});

console.log(result);
// {
//   "id": 1,
//   "name": "Airport",
//   "city": "Antalya"
//   "code": "AYT"
// }

В примере выше был передан первичный ключ id для поиска и замены существующего документа. Теперь рассмотрим создание документа с новым идентификатором.

const result = rep.replaceOrCreate({
  name: 'Airport',
  city: 'Bangkok',
  code: 'BKK',
});

console.log(result);
// {
//   "id": 2,
//   "name": "Airport",
//   "city": "Bangkok",
//   "code": "BKK"
// }

patchById(id, data, filter = undefined)

В отличие от replaceById, данный метод не удаляет поля, которые не были переданы, что позволяет обновить только часть документа, не затрагивая другие данные.

// {
//   "id": 2,
//   "name": "Airport",
//   "city": "Bangkok",
//   "code": "BKK"
// }
const result = rep.patchById(place.id, {
  city: 'Moscow',
  code: 'SVO'
});

console.log(result);
// {
//   "id": 2,
//   "name": "Airport",
//   "city": "Moscow",
//   "code": "SVO"
// }

Расширение

При использовании метода 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