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

e22m4u 4c21cc4b47 chore: bumps version to 0.0.42 2 лет назад
.husky d2c55fccc0 chore: revert "builds as cjs" 2 лет назад
src e90312d348 fix: tests of hasMany relation, relation typings and README.md 2 лет назад
.c8rc dbff18d833 chore: initial commit 2 лет назад
.commitlintrc dbff18d833 chore: initial commit 2 лет назад
.editorconfig dbff18d833 chore: initial commit 2 лет назад
.eslintignore 730af0ba6c chore: adds *.d.ts 2 лет назад
.eslintrc.cjs 85139bcc41 chore: adds *.d.ts 2 лет назад
.gitignore dbff18d833 chore: initial commit 2 лет назад
.mocharc.cjs dbff18d833 chore: initial commit 2 лет назад
.prettierrc dbff18d833 chore: initial commit 2 лет назад
LICENSE dbff18d833 chore: initial commit 2 лет назад
README.md e90312d348 fix: tests of hasMany relation, relation typings and README.md 2 лет назад
mocha.setup.js dbff18d833 chore: initial commit 2 лет назад
package.json 4c21cc4b47 chore: bumps version to 0.0.42 2 лет назад
tsconfig.json 771d9e379e chore: renames to @e22m4u/js-repository and fixes d.ts files 2 лет назад

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 at Avenue Mall",
  "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 at Avenue Mall",
  "location": {
    "lat": 32.412891,
    "lng": 34.7660061
  },
});

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

replaceById(id, data, filter = undefined)

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

// {
//   "id": 1,
//   "name": "Burger King at Avenue Mall",
//   "location": {
//     "lat": 32.412891,
//     "lng": 34.7660061
//   }
// }
const result = rep.replaceById(place.id, {
  name: 'Terminal 21 Shopping Mall',
  address: 'Sukhumvit 19 Alley'
});

console.log(result);
// {
//   "id": 1,
//   "name": "Terminal 21 Shopping Mall",
//   "address": "Sukhumvit 19 Alley"
// }

patchById(id, data, filter = undefined)

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

// {
//   "id": 1,
//   "name": "Terminal 21 Shopping Mall",
//   "address": "Sukhumvit 19 Alley"
// }
const result = rep.patchById(place.id, {
  address: 'Moo 6',
  city: 'Pattaya',
});

console.log(result);
// {
//   "id": 1,
//   "name": "Terminal 21 Shopping Mall",
//   "address": "Moo 6",
//   "city": "Pattaya"
// }

Пример

Создаем модель user

schema.defineModel({
  name: 'user', // название модели
  adapter: 'myMemory', // выбранный источник
  properties: { // поля модели
    name: 'string',
    age: 'number',
  },
});

Получаем репозиторий модели user

const userRep = schema.getRepository('user');

Добавляем новую запись методом create

const fedor = await userRep.create({
  name: 'Fedor',
  age: 24,
});

console.log(fedor);
// {
//   id: 1,
//   name: 'Fedor',
//   age: 24,
// }

Изменяем данные методом patchById

const result = await userRep.patchById(
  fedor.id,
  {age: 30},
);

console.log(result);
// {
//   id: 1,
//   name: 'Fedor',
//   age: 30,
// }

Удаляем по идентификатору методом deleteById

await userRep.deleteById(fedor.id); // true

Репозиторий

Выполняет операции чтения и записи определенной коллекции.

Методы:

  • 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)

Получение репозитория модели:

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

const schema = new Schema();
// создаем источник
schema.defineDatasource({name: 'myDatasource', adapter: 'memory'});
// создаем модель
schema.defineModel({name: 'myModel', datasource: 'myDatasource'});
// получаем репозиторий по названию модели
const repositorty = schema.getRepository('myModel');

Переопределение конструктора:

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

Filter

Большинство методов репозитория принимают объект filter для фильтрации возвращаемого ответа. Описание параметров объекта:

  • where (условия выборки данных из базы)
    примеры:
    where: {foo: 'bar'} поиск по значению поля foo
    where: {foo: {eq: 'bar'}} оператор равенства eq
    where: {foo: {neq: 'bar'}} оператор неравенства neq
    where: {foo: {gt: 5}} оператор "больше" gt
    where: {foo: {lt: 10}} оператор "меньше" lt
    where: {foo: {gte: 5}} оператор "больше или равно" gte
    where: {foo: {lte: 10}} оператор "меньше или равно" lte
    where: {foo: {inq: ['bar', 'baz']}} равенство одного из значений inq
    where: {foo: {nin: ['bar', 'baz']}} исключение значений массива nin
    where: {foo: {between: [5, 10]}} оператор диапазона between
    where: {foo: {exists: true}} оператор наличия значения exists
    where: {foo: {like: 'bar'}} оператор поиска подстроки like
    where: {foo: {ilike: 'BaR'}} регистронезависимая версия ilike
    where: {foo: {nlike: 'bar'}} оператор исключения подстроки nlike
    where: {foo: {nilike: 'BaR'}} регистронезависимая версия nilike
    where: {foo: {regexp: 'ba.+'}} оператор регулярного выражения regexp
    where: {foo: {regexp: 'ba.+', flags: 'i'}} флаги регулярного выражения

  • order (упорядочить записи по полю)
    примеры:
    order: 'foo' порядок по полю foo
    order: 'foo ASC' явное указание порядка
    order: 'foo DESC' инвертировать порядок
    order: ['foo', 'bar ASC', 'baz DESC'] по нескольким полям

  • limit (не более N записей)
    примеры:
    limit: 0 не ограничивать
    limit: 14 не более 14-и

  • skip (пропуск первых N записей)
    примеры:
    skip: 0 выборка без пропуска
    skip: 10 пропустить 10 объектов выборки

  • include (включение связанных данных в результат)
    примеры:
    include: 'foo' включение связи foo
    include: ['foo', 'bar'] включение foo и bar
    include: {foo: 'bar'} включение вложенной связи foo

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

Определяет настройки и способ подключения к базе.

Параметры:

  • name: string название нового источника
  • adapter: string выбранный адаптер базы данных

Пример:

schema.defineDatasource({
  name: 'myDatasource',
  adapter: 'memory',
});

Адаптер может иметь параметры, которые передаются при определении источника.

Пример:

schema.defineDatasource({
  name: 'myDatasource',
  adapter: 'mongodb',
  // параметры адаптера:
  host: '127.0.0.1',
  port: 27017,
});

Модель

Описывает набор полей и связей к другим моделям.

Параметры:

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

Пример:

schema.defineModel({
  name: 'myModel',
  datasource: 'myDatasource',
  properties: {...}, // см. ниже
  relations: {...}, // см. ниже
});

Поля

Параметр properties описывает набор полей и их настройки.

Типы:

  • string
  • number
  • boolean
  • array
  • object
  • any

Пример:

schema.defineModel({
  // ...
  properties: {
    prop1: 'string',
    prop2: 'number',
    prop3: 'boolean',
    prop4: 'array',
    prop5: 'object',
    prop6: 'any',
  },
});

Расширенные параметры:

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

Пример:

schema.defineModel({
  // ...
  properties: {
    prop1: {
      type: 'string',
      primaryKey: true,
    },
    prop2: {
      type: 'boolean',
      required: true,
    },
    prop3: {
      type: 'number',
      default: 100,
    },
    prop3: {
      type: 'string',
      // фабричное значение
      default: () => new Date().toISOString(),
    },
    prop4: {
      type: 'array',
      itemType: 'string',
    },
    prop5: {
      type: 'object',
      model: 'objectModel',
    },
  },
});

Связи

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

Понятия:

  • источник связи
    - модель в которой определена данная связь
  • целевая модель
    - модель на которую ссылается источник связи

Типы:

  • belongsTo - ссылка на целевую модель находится в источнике
  • hasOne - ссылка на источник находится в целевой модели (one-to-one)
  • hasMany - ссылка на источник находится в целевой модели (one-to-many)
  • referencesMany - массив ссылок на целевую модель находится в источнике

Параметры:

  • type: string тип связи
  • model: string целевая модель
  • foreignKey: string поле для идентификатора цели
  • polymorphic: boolean|string объявить связь полиморфной*
  • discriminator: string поле для названия целевой модели (для polymorphic: true)

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

BelongsTo

Связь заказа к покупателю через поле customerId

schema.defineModel({
  // ...
  relations: {
    // ...
    customer: {
      type: 'belongsTo',
      model: 'customer',
      foreignKey: 'customerId', // опционально
    },
  },
});

Полиморфная версия

schema.defineModel({
  // ...
  relations: {
    // ...
    customer: {
      type: 'belongsTo',
      polymorphic: true,
      foreignKey: 'customerId', // опционально
      discriminator: 'customerType', // опционально
    },
  },
});

HasOne (one-to-one)

Связь покупателя к заказу, как обратная сторона belongsTo

schema.defineModel({
  // ...
  relations: {
    // ...
    order: {
      type: 'hasOne',
      model: 'order',
      foreignKey: 'customerId', // поле целевой модели
    },
  },
});

Обратная сторона полиморфной версии belongsTo

schema.defineModel({
  // ...
  relations: {
    // ...
    order: {
      type: 'hasOne',
      model: 'order',
      polymorphic: 'customer', // имя связи целевой модели
    },
  },
});

Явное указание foreignKey и discriminator

schema.defineModel({
  // ...
  relations: {
    // ...
    order: {
      type: 'hasOne',
      model: 'order',
      polymorphic: true, // true вместо имени модели
      foreignKey: 'customerId', // поле целевой модели 
      discriminator: 'customerType', // поле целевой модели
    },
  },
});

HasMany (one-to-many)

Связь покупателя к заказам, как обратная сторона belongsTo

schema.defineModel({
  // ...
  relations: {
    // ...
    orders: {
      type: 'hasMany',
      model: 'order',
      foreignKey: 'customerId', // поле целевой модели
    },
  },
});

Обратная сторона полиморфной версии belongsTo

schema.defineModel({
  // ...
  relations: {
    // ...
    orders: {
      type: 'hasMany',
      model: 'order',
      polymorphic: 'customer', // имя связи целевой модели
    },
  },
});

Явное указание foreignKey и discriminator

schema.defineModel({
  // ...
  relations: {
    // ...
    orders: {
      type: 'hasMany',
      model: 'order',
      polymorphic: true, // true вместо имени модели
      foreignKey: 'customerId', // поле целевой модели 
      discriminator: 'customerType', // поле целевой модели
    },
  },
});

ReferencesMany

Связь покупателя к заказам через поле orderIds

schema.defineModel({
  // ...
  relations: {
    // ...
    orders: {
      type: 'referencesMany',
      model: 'order',
      foreignKey: 'orderIds', // опционально
    },
  },
});

Тесты

npm run test

Лицензия

MIT