Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

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 TD

A[Схема]
subgraph Базы данных
B[Источник данных 1]
C[Источник данных 2]
end
A-->B
A-->C

subgraph Коллекции
D[Модель A]
E[Модель Б]
F[Модель В]
G[Модель Г]
end
B-->D
B-->E
C-->F
C-->G

H[Репозиторий 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