Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/e22m4u/js-repository
Модуль для работы с базами данных для Node.js
https://github.com/e22m4u/js-repository
database datasource db mongodb nodejs odm orm relations repository russian
Last synced: 3 days ago
JSON representation
Модуль для работы с базами данных для Node.js
- Host: GitHub
- URL: https://github.com/e22m4u/js-repository
- Owner: e22m4u
- License: mit
- Created: 2024-03-16T14:29:24.000Z (8 months ago)
- Default Branch: master
- Last Pushed: 2024-11-09T11:07:34.000Z (6 days ago)
- Last Synced: 2024-11-09T11:28:47.477Z (6 days ago)
- Topics: database, datasource, db, mongodb, nodejs, odm, orm, relations, repository, russian
- Language: JavaScript
- Homepage:
- Size: 1.81 MB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
## @e22m4u/js-repository
ES-модуль для работы с базами данных для Node.js
- [Установка](#Установка)
- [Импорт](#Импорт)
- [Описание](#Описание)
- [Пример](#Пример)
- [Схема](#Схема)
- [Источник данных](#Источник-данных)
- [Модель](#Модель)
- [Свойства](#Свойства)
- [Валидаторы](#Валидаторы)
- [Трансформеры](#Трансформеры)
- [Пустые значения](#Пустые-значения)
- [Репозиторий](#Репозиторий)
- [Фильтрация](#Фильтрация)
- [Связи](#Связи)
- [Расширение](#Расширение)
- [TypeScript](#TypeScript)
- [Тесты](#Тесты)
- [Лицензия](#Лицензия)## Установка
```bash
npm install @e22m4u/js-repository
```Опционально устанавливаем адаптер.
| | описание |
|-----------|--------------------------------------------------------------------------------------------------------------------------------|
| `memory` | виртуальная база в памяти процесса (не требует установки) |
| `mongodb` | MongoDB - система управления NoSQL базами (*[установка](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter))* |## Импорт
Модуль поддерживает ESM и CommonJS стандарты.
*ESM*
```js
import {Schema} from '@e22m4u/js-repository';
```*CommonJS*
```js
const {Schema} = require('@e22m4u/js-repository');
```## Описание
Модуль позволяет абстрагироваться от различных интерфейсов баз данных,
представляя их как именованные *источники данных*, подключаемые к *моделям*.
*Модель* же описывает таблицу базы, колонки которой являются свойствами
модели. Свойства модели могут иметь определенный *тип* допустимого значения,
набор *валидаторов* и *трансформеров*, через которые проходят данные перед
записью в базу. Кроме того, *модель* может определять классические связи
«один к одному», «один ко многим» и другие типы отношений между моделями.Непосредственно чтение и запись данных производится с помощью *репозитория*,
который имеет каждая модель с объявленным *источником данных*. Репозиторий
может фильтровать запрашиваемые документы, выполнять валидацию свойств
согласно определению модели, и встраивать связанные данные в результат
выборки.- *Источник данных* - определяет способ подключения к базе
- *Модель* - описывает структуру документа и связи к другим моделям
- *Репозиторий* - выполняет операции чтения и записи документов модели```mermaid
flowchart TDA[Схема]
subgraph Базы данных
B[Источник данных 1]
C[Источник данных 2]
end
A-->B
A-->Csubgraph Коллекции
D[Модель A]
E[Модель Б]
F[Модель В]
G[Модель Г]
end
B-->D
B-->E
C-->F
C-->GH[Репозиторий A]
I[Репозиторий Б]
J[Репозиторий В]
K[Репозиторий Г]
D-->H
E-->I
F-->J
G-->K
```## Пример
Объявление источника данных, модели и добавление нового документа в коллекцию.
```js
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` - получить репозиторий**Примеры**
Импорт класса и создание экземпляра схемы.
```js
import {Schema} from '@e22m4u/js-repository';const schema = new Schema();
```Определение нового источника.
```js
schema.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});
```Определение новой модели.
```js
schema.defineModel({
name: 'product', // название новой модели
datasource: 'myMemory', // выбранный источник
properties: { // свойства модели
name: DataType.STRING,
weight: DataType.NUMBER,
},
});
```Получение репозитория по названию модели.
```js
const productRep = schema.getRepository('product');
```## Источник данных
Источник хранит название выбранного адаптера и его настройки. Определение
нового источника выполняется методом `defineDatasource` экземпляра схемы.**Параметры**
- `name: string` уникальное название
- `adapter: string` выбранный адаптер
- параметры адаптера (если имеются)**Примеры**
Определение нового источника.
```js
schema.defineDatasource({
name: 'myMemory', // название нового источника
adapter: 'memory', // выбранный адаптер
});
```Передача дополнительных параметров адаптера.
```js
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` определения связей (см. [Связи](#Связи))**Примеры**
Определение модели со свойствами указанного типа.
```js
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` значение по умолчанию
- `validate: string | array | object` см. [Валидаторы](#Валидаторы)
- `unique: boolean | string` проверять значение на уникальность**Параметр `unique`**
Если значением параметра `unique` является `true` или `'strict'`, то выполняется
строгая проверка на уникальность. В этом режиме [пустые значения](#Пустые-значения)
так же подлежат проверке, где `null` и `undefined` не могут повторяться более одного
раза.Режим `'sparse'` проверяет только значения с полезной нагрузкой, исключая
[пустые значения](#Пустые-значения), список которых отличается в зависимости
от типа свойства. Например, для типа `string` пустым значением будет `undefined`,
`null` и `''` (пустая строка).- `unique: true | 'strict'` строгая проверка на уникальность
- `unique: 'sparse'` исключить из проверки [пустые значения](#Пустые-значения)
- `unique: false | 'nonUnique'` не проверять на уникальность (по умолчанию)В качестве значений параметра `unique` можно использовать предопределенные
константы как эквивалент строковых значений `strict`, `sparse` и `nonUnique`.- `PropertyUniqueness.STRICT`
- `PropertyUniqueness.SPARSE`
- `PropertyUniqueness.NON_UNIQUE`**Примеры**
Краткое определение свойств модели.
```js
schema.defineModel({
name: 'city',
properties: { // свойства модели
name: DataType.STRING, // тип свойства "string"
population: DataType.NUMBER, // тип свойства "number"
},
});
```Расширенное определение свойств модели.
```js
schema.defineModel({
name: 'city',
properties: { // свойства модели
name: {
type: DataType.STRING, // тип свойства "string" (обязательно)
required: true, // исключение значений undefined и null
},
population: {
type: DataType.NUMBER, // тип свойства "number" (обязательно)
default: 0, // значение по умолчанию
},
code: {
type: DataType.NUMBER, // тип свойства "number" (обязательно)
unique: PropertyUniqueness.UNIQUE, // проверять уникальность
},
},
});
```Фабричное значение по умолчанию. Возвращаемое значение функции будет
определено в момент записи документа.```js
schema.defineModel({
name: 'article',
properties: { // свойства модели
tags: {
type: DataType.ARRAY, // тип свойства "array" (обязательно)
itemType: DataType.STRING, // тип элемента "string"
default: () => [], // фабричное значение
},
createdAt: {
type: DataType.STRING, // тип свойства "string" (обязательно)
default: () => new Date().toISOString(), // фабричное значение
},
},
});
```## Валидаторы
Кроме проверки типа, дополнительные условия можно задать с помощью
валидаторов, через которые будет проходить значение свойства перед
записью в базу. Исключением являются [пустые значения](#Пустые-значения),
которые не подлежат проверке.- `minLength: number` минимальная длинна строки или массива
- `maxLength: number` максимальная длинна строки или массива
- `regexp: string | RegExp` проверка по регулярному выражению**Пример**
Валидаторы указываются в объявлении свойства модели параметром
`validate`, который принимает объект с их названиями и настройками.```js
schema.defineModel({
name: 'user',
properties: {
name: {
type: DataType.STRING,
validate: { // валидаторы свойства "name"
minLength: 2, // минимальная длинна строки
maxLength: 24, // максимальная длинна строки
},
},
},
});
```### Пользовательские валидаторы
Валидатором является функция, в которую передается значение соответствующего
поля перед записью в базу. Если во время проверки функция возвращает `false`,
то выбрасывается стандартная ошибка. Подмена стандартной ошибки возможна
с помощью выброса пользовательской ошибки непосредственно внутри функции.Регистрация пользовательского валидатора выполняется методом `addValidator`
сервиса `PropertyValidatorRegistry`, который принимает новое название
и функцию для проверки значения.**Пример**
```js
// создание валидатора для запрета
// всех символов кроме чисел
const numericValidator = (input) => {
return /^[0-9]+$/.test(String(input));
}// регистрация валидатора "numeric"
schema
.get(PropertyValidatorRegistry)
.addValidator('numeric', numericValidator);// использование валидатора в определении
// свойства "code" для новой модели
schema.defineModel({
name: 'document',
properties: {
code: {
type: DataType.STRING,
validate: 'numeric',
},
},
});
```## Трансформеры
С помощью трансформеров производится модификация значений определенных
полей перед записью в базу. Трансформеры позволяют указать какие изменения
нужно производить с входящими данными. Исключением являются
[пустые значения](#Пустые-значения), которые не подлежат трансформации.- `trim` удаление пробельных символов с начала и конца строки
- `toUpperCase` перевод строки в верхний регистр
- `toLowerCase` перевод строки в нижний регистр
- `toTitleCase` перевод строки в регистр заголовка**Пример**
Трансформеры указываются в объявлении свойства модели параметром
`transform`, который принимает название трансформера. Если требуется
указать несколько названий, то используется массив. Если трансформер
имеет настройки, то используется объект, где ключом является название
трансформера, а значением его параметры.```js
schema.defineModel({
name: 'user',
properties: {
name: {
type: DataType.STRING,
transform: [ // трансформеры свойства "name"
'trim', // удалить пробелы в начале и конце строки
'toTitleCase', // перевод строки в регистр заголовка
],
},
},
});
```## Пустые значения
Разные типы свойств имеют свои наборы пустых значений. Эти наборы
используются для определения наличия полезной нагрузки в значении
свойства. Например, параметр `default` в определении свойства
устанавливает значение по умолчанию, только если входящее значение
является пустым. Параметр `required` исключает пустые значения
выбрасывая ошибку. А параметр `unique` в режиме `sparse` наоборот
допускает дублирование пустых значений уникального свойства.| тип | пустые значения |
|-------------|---------------------------|
| `'any'` | `undefined`, `null` |
| `'string'` | `undefined`, `null`, `''` |
| `'number'` | `undefined`, `null`, `0` |
| `'boolean'` | `undefined`, `null` |
| `'array'` | `undefined`, `null`, `[]` |
| `'object'` | `undefined`, `null`, `{}` |## Репозиторий
Выполняет операции чтения и записи документов определенной модели.
Получить репозиторий можно методом `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` параметры возвращаемого результата (см. [Фильтрация](#Фильтрация))**Примеры**
Получение репозитория по названию модели.
```js
const countryRep = schema.getRepository('country');
```Добавление нового документа в коллекцию.
```js
const res = await countryRep.create({
name: 'Russia',
population: 143400000,
});console.log(res);
// {
// "id": 1,
// "name": "Russia",
// "population": 143400000,
// }
```Поиск документа по идентификатору.
```js
const res = await countryRep.findById(1);console.log(res);
// {
// "id": 1,
// "name": "Russia",
// "population": 143400000,
// }
```Удаление документа по идентификатору.
```js
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`.*
**Примеры**
Применение условий выборки при подсчете документов.
```js
const res = await rep.count({
authorId: 251,
publishedAt: {
lte: '2023-12-02T14:00:00.000Z',
},
});
```Применение оператора `or` при удалении документов.
```js
const res = await rep.delete({
or: [
{draft: true},
{title: {like: 'draft'}},
],
});
```### order
Параметр упорядочивает выборку по указанным свойствам модели. Обратное
направление порядка можно задать постфиксом `DESC` в названии свойства.**Примеры**
Упорядочить по полю `createdAt`
```js
const res = await rep.find({
order: 'createdAt',
});
```Упорядочить по полю `createdAt` в обратном порядке.
```js
const res = await rep.find({
order: 'createdAt DESC',
});
```Упорядочить по нескольким свойствам в разных направлениях.
```js
const res = await rep.find({
order: [
'title',
'price ASC',
'featured DESC',
],
});
```*i. Направление порядка `ASC` указывать необязательно.*
### include
Параметр включает связанные документы в результат вызываемого метода.
Названия включаемых связей должны быть определены в текущей модели.
(см. [Связи](#Связи))**Примеры**
Включение связи по названию.
```js
const res = await rep.find({
include: 'city',
});
```Включение вложенных связей.
```js
const res = await rep.find({
include: {
city: 'country',
},
});
```Включение нескольких связей массивом.
```js
const res = await rep.find({
include: [
'city',
'address',
'employees'
],
});
```Использование фильтрации включаемых документов.
```js
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`
```js
schema.defineModel({
name: 'user',
relations: {
role: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
model: 'role', // название целевой модели
foreignKey: 'roleId', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Id"
},
},
});
```Объявление связи `hasMany`
```js
schema.defineModel({
name: 'role',
relations: {
users: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'user', // название целевой модели
foreignKey: 'roleId', // внешний ключ из целевой модели на текущую
},
},
});
```Объявление связи `referencesMany`
```js
schema.defineModel({
name: 'article',
relations: {
categories: { // название связи
type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов
model: 'category', // название целевой модели
foreignKey: 'categoryIds', // внешний ключ (необязательно)
// если "foreignKey" не указан, то свойство внешнего
// ключа формируется согласно названию связи
// с добавлением постфикса "Ids"
},
},
});
```Полиморфная версия `belongsTo`
```js
schema.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
// полиморфный режим позволяет хранить название целевой модели
// в свойстве-дискриминаторе, которое формируется согласно
// названию связи с постфиксом "Type", и в данном случае
// название целевой модели хранит "referenceType",
// а идентификатор документа "referenceId"
polymorphic: true,
},
},
});
```Полиморфная версия `belongsTo` с указанием свойств.
```js
schema.defineModel({
name: 'file',
relations: {
reference: { // название связи
type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
polymorphic: true, // название целевой модели хранит дискриминатор
foreignKey: 'referenceId', // свойство для идентификатора цели
discriminator: 'referenceType', // свойство для названия целевой модели
},
},
})
```Полиморфная версия `hasMany` с указанием названия связи целевой модели.
```js
schema.defineModel({
name: 'letter',
relations: {
attachments: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'file', // название целевой модели
polymorphic: 'reference', // название полиморфной связи целевой модели
},
},
})
```Полиморфная версия `hasMany` с указанием свойств целевой модели.
```js
schema.defineModel({
name: 'letter',
relations: {
attachments: { // название связи
type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
model: 'file', // название целевой модели
polymorphic: true, // название текущей модели находится в дискриминаторе
foreignKey: 'referenceId', // свойство целевой модели для идентификатора
discriminator: 'referenceType', // свойство целевой модели для названия текущей
},
},
})
```## Расширение
Метод `getRepository` экземпляра схемы проверяет наличие существующего
репозитория для указанной модели и возвращает его. В противном случае
создается новый экземпляр, который будет сохранен для последующих
обращений к методу.```js
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`, который находится
в контейнере экземпляра схемы. После чего все новые репозитории будут
создаваться указанным конструктором вместо стандартного.```js
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
Получение типизированного репозитория с указанием интерфейса модели.
```ts
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');
```## Тесты
```bash
npm run test
```## Лицензия
MIT