@e22m4u/js-repository 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README-ru.md ADDED
@@ -0,0 +1,962 @@
1
+ ## @e22m4u/js-repository
2
+
3
+ *[English](README.md) | Русский*
4
+
5
+ Реализация паттерна «Репозиторий» для Node.js
6
+
7
+ - [Установка](#Установка)
8
+ - [Импорт](#Импорт)
9
+ - [Описание](#Описание)
10
+ - [Пример](#Пример)
11
+ - [Схема](#Схема)
12
+ - [Источник данных](#Источник-данных)
13
+ - [Модель](#Модель)
14
+ - [Свойства](#Свойства)
15
+ - [Валидаторы](#Валидаторы)
16
+ - [Трансформеры](#Трансформеры)
17
+ - [Пустые значения](#Пустые-значения)
18
+ - [Репозиторий](#Репозиторий)
19
+ - [Фильтрация](#Фильтрация)
20
+ - [Связи](#Связи)
21
+ - [Расширение](#Расширение)
22
+ - [TypeScript](#TypeScript)
23
+ - [Тесты](#Тесты)
24
+ - [Лицензия](#Лицензия)
25
+
26
+ ## Установка
27
+
28
+ ```bash
29
+ npm install @e22m4u/js-repository
30
+ ```
31
+
32
+ Опционально устанавливаем адаптер.
33
+
34
+ | | описание |
35
+ |-----------|--------------------------------------------------------------------------------------------------------------------------------|
36
+ | `memory` | виртуальная база в памяти процесса (не требует установки) |
37
+ | `mongodb` | MongoDB - система управления NoSQL базами (*[установка](https://www.npmjs.com/package/@e22m4u/js-repository-mongodb-adapter))* |
38
+
39
+ ## Импорт
40
+
41
+ Модуль поддерживает ESM и CommonJS стандарты.
42
+
43
+ *ESM*
44
+
45
+ ```js
46
+ import {Schema} from '@e22m4u/js-repository';
47
+ ```
48
+
49
+ *CommonJS*
50
+
51
+ ```js
52
+ const {Schema} = require('@e22m4u/js-repository');
53
+ ```
54
+
55
+ ## Описание
56
+
57
+ Модуль позволяет абстрагироваться от различных интерфейсов баз данных,
58
+ представляя их как именованные *источники данных*, подключаемые к *моделям*.
59
+ *Модель* же описывает таблицу базы, колонки которой являются свойствами
60
+ модели. Свойства модели могут иметь определенный *тип* допустимого значения,
61
+ набор *валидаторов* и *трансформеров*, через которые проходят данные перед
62
+ записью в базу. Кроме того, *модель* может определять классические связи
63
+ «один к одному», «один ко многим» и другие типы отношений между моделями.
64
+
65
+ Непосредственно чтение и запись данных производится с помощью *репозитория*,
66
+ который имеет каждая модель с объявленным *источником данных*. Репозиторий
67
+ может фильтровать запрашиваемые документы, выполнять валидацию свойств
68
+ согласно определению модели, и встраивать связанные данные в результат
69
+ выборки.
70
+
71
+ - *Источник данных* - определяет способ подключения к базе
72
+ - *Модель* - описывает структуру документа и связи к другим моделям
73
+ - *Репозиторий* - выполняет операции чтения и записи документов модели
74
+
75
+ ```mermaid
76
+ flowchart TD
77
+
78
+ A[Схема]
79
+ subgraph Базы данных
80
+ B[Источник данных 1]
81
+ C[Источник данных 2]
82
+ end
83
+ A-->B
84
+ A-->C
85
+
86
+ subgraph Коллекции
87
+ D[Модель A]
88
+ E[Модель Б]
89
+ F[Модель В]
90
+ G[Модель Г]
91
+ end
92
+ B-->D
93
+ B-->E
94
+ C-->F
95
+ C-->G
96
+
97
+ H[Репозиторий A]
98
+ I[Репозиторий Б]
99
+ J[Репозиторий В]
100
+ K[Репозиторий Г]
101
+ D-->H
102
+ E-->I
103
+ F-->J
104
+ G-->K
105
+ ```
106
+
107
+ ## Пример
108
+
109
+ Объявление источника данных, модели и добавление нового документа в коллекцию.
110
+
111
+ ```js
112
+ import {Schema} from '@e22m4u/js-repository';
113
+ import {DataType} from '@e22m4u/js-repository';
114
+
115
+ // создание экземпляра Schema
116
+ const schema = new Schema();
117
+
118
+ // объявление источника "myMemory"
119
+ schema.defineDatasource({
120
+ name: 'myMemory', // название нового источника
121
+ adapter: 'memory', // выбранный адаптер
122
+ });
123
+
124
+ // объявление модели "country"
125
+ schema.defineModel({
126
+ name: 'country', // название новой модели
127
+ datasource: 'myMemory', // выбранный источник
128
+ properties: { // свойства модели
129
+ name: DataType.STRING, // тип "string"
130
+ population: DataType.NUMBER, // тип "number"
131
+ },
132
+ })
133
+
134
+ // получение репозитория модели "country"
135
+ const countryRep = schema.getRepository('country');
136
+
137
+ // добавление нового документа в коллекцию "country"
138
+ const country = await countryRep.create({
139
+ name: 'Russia',
140
+ population: 143400000,
141
+ });
142
+
143
+ // вывод нового документа
144
+ console.log(country);
145
+ // {
146
+ // "id": 1,
147
+ // "name": "Russia",
148
+ // "population": 143400000,
149
+ // }
150
+ ```
151
+
152
+ ## Схема
153
+
154
+ Экземпляр класса `Schema` хранит определения источников данных и моделей.
155
+
156
+ **Методы**
157
+
158
+ - `defineDatasource(datasourceDef: object): this` - добавить источник
159
+ - `defineModel(modelDef: object): this` - добавить модель
160
+ - `getRepository(modelName: string): Repository` - получить репозиторий
161
+
162
+ **Примеры**
163
+
164
+ Импорт класса и создание экземпляра схемы.
165
+
166
+ ```js
167
+ import {Schema} from '@e22m4u/js-repository';
168
+
169
+ const schema = new Schema();
170
+ ```
171
+
172
+ Определение нового источника.
173
+
174
+ ```js
175
+ schema.defineDatasource({
176
+ name: 'myMemory', // название нового источника
177
+ adapter: 'memory', // выбранный адаптер
178
+ });
179
+ ```
180
+
181
+ Определение новой модели.
182
+
183
+ ```js
184
+ schema.defineModel({
185
+ name: 'product', // название новой модели
186
+ datasource: 'myMemory', // выбранный источник
187
+ properties: { // свойства модели
188
+ name: DataType.STRING,
189
+ weight: DataType.NUMBER,
190
+ },
191
+ });
192
+ ```
193
+
194
+ Получение репозитория по названию модели.
195
+
196
+ ```js
197
+ const productRep = schema.getRepository('product');
198
+ ```
199
+
200
+ ## Источник данных
201
+
202
+ Источник хранит название выбранного адаптера и его настройки. Определение
203
+ нового источника выполняется методом `defineDatasource` экземпляра схемы.
204
+
205
+ **Параметры**
206
+
207
+ - `name: string` уникальное название
208
+ - `adapter: string` выбранный адаптер
209
+ - параметры адаптера (если имеются)
210
+
211
+ **Примеры**
212
+
213
+ Определение нового источника.
214
+
215
+ ```js
216
+ schema.defineDatasource({
217
+ name: 'myMemory', // название нового источника
218
+ adapter: 'memory', // выбранный адаптер
219
+ });
220
+ ```
221
+
222
+ Передача дополнительных параметров адаптера.
223
+
224
+ ```js
225
+ schema.defineDatasource({
226
+ name: 'myMongodb',
227
+ adapter: 'mongodb',
228
+ // параметры адаптера "mongodb"
229
+ host: '127.0.0.1',
230
+ port: 27017,
231
+ database: 'myDatabase',
232
+ });
233
+ ```
234
+
235
+ ## Модель
236
+
237
+ Описывает структуру документа коллекции и связи к другим моделям. Определение
238
+ новой модели выполняется методом `defineModel` экземпляра схемы.
239
+
240
+ **Параметры**
241
+
242
+ - `name: string` название модели (обязательно)
243
+ - `base: string` название наследуемой модели
244
+ - `tableName: string` название коллекции в базе
245
+ - `datasource: string` выбранный источник данных
246
+ - `properties: object` определения свойств (см. [Свойства](#Свойства))
247
+ - `relations: object` определения связей (см. [Связи](#Связи))
248
+
249
+ **Примеры**
250
+
251
+ Определение модели со свойствами указанного типа.
252
+
253
+ ```js
254
+ schema.defineModel({
255
+ name: 'user', // название новой модели
256
+ properties: { // свойства модели
257
+ name: DataType.STRING,
258
+ age: DataType.NUMBER,
259
+ },
260
+ });
261
+ ```
262
+
263
+ ## Свойства
264
+
265
+ Параметр `properties` находится в определении модели и принимает объект, ключи
266
+ которого являются свойствами этой модели, а значением тип свойства или объект
267
+ с дополнительными параметрами.
268
+
269
+ **Тип данных**
270
+
271
+ - `DataType.ANY` разрешено любое значение
272
+ - `DataType.STRING` только значение типа `string`
273
+ - `DataType.NUMBER` только значение типа `number`
274
+ - `DataType.BOOLEAN` только значение типа `boolean`
275
+ - `DataType.ARRAY` только значение типа `array`
276
+ - `DataType.OBJECT` только значение типа `object`
277
+
278
+ **Параметры**
279
+
280
+ - `type: string` тип допустимого значения (обязательно)
281
+ - `itemType: string` тип элемента массива (для `type: 'array'`)
282
+ - `model: string` модель объекта (для `type: 'object'`)
283
+ - `primaryKey: boolean` объявить свойство первичным ключом
284
+ - `columnName: string` переопределение названия колонки
285
+ - `columnType: string` тип колонки (определяется адаптером)
286
+ - `required: boolean` объявить свойство обязательным
287
+ - `default: any` значение по умолчанию
288
+ - `validate: string | array | object` см. [Валидаторы](#Валидаторы)
289
+ - `unique: boolean | string` проверять значение на уникальность
290
+
291
+ **Параметр `unique`**
292
+
293
+ Если значением параметра `unique` является `true` или `'strict'`, то выполняется
294
+ строгая проверка на уникальность. В этом режиме [пустые значения](#Пустые-значения)
295
+ так же подлежат проверке, где `null` и `undefined` не могут повторяться более одного
296
+ раза.
297
+
298
+ Режим `'sparse'` проверяет только значения с полезной нагрузкой, исключая
299
+ [пустые значения](#Пустые-значения), список которых отличается в зависимости
300
+ от типа свойства. Например, для типа `string` пустым значением будет `undefined`,
301
+ `null` и `''` (пустая строка).
302
+
303
+ - `unique: true | 'strict'` строгая проверка на уникальность
304
+ - `unique: 'sparse'` исключить из проверки [пустые значения](#Пустые-значения)
305
+ - `unique: false | 'nonUnique'` не проверять на уникальность (по умолчанию)
306
+
307
+ В качестве значений параметра `unique` можно использовать предопределенные
308
+ константы как эквивалент строковых значений `strict`, `sparse` и `nonUnique`.
309
+
310
+ - `PropertyUniqueness.STRICT`
311
+ - `PropertyUniqueness.SPARSE`
312
+ - `PropertyUniqueness.NON_UNIQUE`
313
+
314
+ **Примеры**
315
+
316
+ Краткое определение свойств модели.
317
+
318
+ ```js
319
+ schema.defineModel({
320
+ name: 'city',
321
+ properties: { // свойства модели
322
+ name: DataType.STRING, // тип свойства "string"
323
+ population: DataType.NUMBER, // тип свойства "number"
324
+ },
325
+ });
326
+ ```
327
+
328
+ Расширенное определение свойств модели.
329
+
330
+ ```js
331
+ schema.defineModel({
332
+ name: 'city',
333
+ properties: { // свойства модели
334
+ name: {
335
+ type: DataType.STRING, // тип свойства "string" (обязательно)
336
+ required: true, // исключение значений undefined и null
337
+ },
338
+ population: {
339
+ type: DataType.NUMBER, // тип свойства "number" (обязательно)
340
+ default: 0, // значение по умолчанию
341
+ },
342
+ code: {
343
+ type: DataType.NUMBER, // тип свойства "number" (обязательно)
344
+ unique: PropertyUniqueness.UNIQUE, // проверять уникальность
345
+ },
346
+ },
347
+ });
348
+ ```
349
+
350
+ Фабричное значение по умолчанию. Возвращаемое значение функции будет
351
+ определено в момент записи документа.
352
+
353
+ ```js
354
+ schema.defineModel({
355
+ name: 'article',
356
+ properties: { // свойства модели
357
+ tags: {
358
+ type: DataType.ARRAY, // тип свойства "array" (обязательно)
359
+ itemType: DataType.STRING, // тип элемента "string"
360
+ default: () => [], // фабричное значение
361
+ },
362
+ createdAt: {
363
+ type: DataType.STRING, // тип свойства "string" (обязательно)
364
+ default: () => new Date().toISOString(), // фабричное значение
365
+ },
366
+ },
367
+ });
368
+ ```
369
+
370
+ ## Валидаторы
371
+
372
+ Кроме проверки типа, дополнительные условия можно задать с помощью
373
+ валидаторов, через которые будет проходить значение свойства перед
374
+ записью в базу. Исключением являются [пустые значения](#Пустые-значения),
375
+ которые не подлежат проверке.
376
+
377
+ - `minLength: number` минимальная длинна строки или массива
378
+ - `maxLength: number` максимальная длинна строки или массива
379
+ - `regexp: string | RegExp` проверка по регулярному выражению
380
+
381
+ **Пример**
382
+
383
+ Валидаторы указываются в объявлении свойства модели параметром
384
+ `validate`, который принимает объект с их названиями и настройками.
385
+
386
+ ```js
387
+ schema.defineModel({
388
+ name: 'user',
389
+ properties: {
390
+ name: {
391
+ type: DataType.STRING,
392
+ validate: { // валидаторы свойства "name"
393
+ minLength: 2, // минимальная длинна строки
394
+ maxLength: 24, // максимальная длинна строки
395
+ },
396
+ },
397
+ },
398
+ });
399
+ ```
400
+
401
+ ### Пользовательские валидаторы
402
+
403
+ Валидатором является функция, в которую передается значение соответствующего
404
+ поля перед записью в базу. Если во время проверки функция возвращает `false`,
405
+ то выбрасывается стандартная ошибка. Подмена стандартной ошибки возможна
406
+ с помощью выброса пользовательской ошибки непосредственно внутри функции.
407
+
408
+ Регистрация пользовательского валидатора выполняется методом `addValidator`
409
+ сервиса `PropertyValidatorRegistry`, который принимает новое название
410
+ и функцию для проверки значения.
411
+
412
+ **Пример**
413
+
414
+ ```js
415
+ // создание валидатора для запрета
416
+ // всех символов кроме чисел
417
+ const numericValidator = (input) => {
418
+ return /^[0-9]+$/.test(String(input));
419
+ }
420
+
421
+ // регистрация валидатора "numeric"
422
+ schema
423
+ .get(PropertyValidatorRegistry)
424
+ .addValidator('numeric', numericValidator);
425
+
426
+ // использование валидатора в определении
427
+ // свойства "code" для новой модели
428
+ schema.defineModel({
429
+ name: 'document',
430
+ properties: {
431
+ code: {
432
+ type: DataType.STRING,
433
+ validate: 'numeric',
434
+ },
435
+ },
436
+ });
437
+ ```
438
+
439
+ ## Трансформеры
440
+
441
+ С помощью трансформеров производится модификация значений определенных
442
+ полей перед записью в базу. Трансформеры позволяют указать какие изменения
443
+ нужно производить с входящими данными. Исключением являются
444
+ [пустые значения](#Пустые-значения), которые не подлежат трансформации.
445
+
446
+ - `trim` удаление пробельных символов с начала и конца строки
447
+ - `toUpperCase` перевод строки в верхний регистр
448
+ - `toLowerCase` перевод строки в нижний регистр
449
+ - `toTitleCase` перевод строки в регистр заголовка
450
+
451
+ **Пример**
452
+
453
+ Трансформеры указываются в объявлении свойства модели параметром
454
+ `transform`, который принимает название трансформера. Если требуется
455
+ указать несколько названий, то используется массив. Если трансформер
456
+ имеет настройки, то используется объект, где ключом является название
457
+ трансформера, а значением его параметры.
458
+
459
+ ```js
460
+ schema.defineModel({
461
+ name: 'user',
462
+ properties: {
463
+ name: {
464
+ type: DataType.STRING,
465
+ transform: [ // трансформеры свойства "name"
466
+ 'trim', // удалить пробелы в начале и конце строки
467
+ 'toTitleCase', // перевод строки в регистр заголовка
468
+ ],
469
+ },
470
+ },
471
+ });
472
+ ```
473
+
474
+ ## Пустые значения
475
+
476
+ Разные типы свойств имеют свои наборы пустых значений. Эти наборы
477
+ используются для определения наличия полезной нагрузки в значении
478
+ свойства. Например, параметр `default` в определении свойства
479
+ устанавливает значение по умолчанию, только если входящее значение
480
+ является пустым. Параметр `required` исключает пустые значения
481
+ выбрасывая ошибку. А параметр `unique` в режиме `sparse` наоборот
482
+ допускает дублирование пустых значений уникального свойства.
483
+
484
+ | тип | пустые значения |
485
+ |-------------|---------------------------|
486
+ | `'any'` | `undefined`, `null` |
487
+ | `'string'` | `undefined`, `null`, `''` |
488
+ | `'number'` | `undefined`, `null`, `0` |
489
+ | `'boolean'` | `undefined`, `null` |
490
+ | `'array'` | `undefined`, `null`, `[]` |
491
+ | `'object'` | `undefined`, `null`, `{}` |
492
+
493
+ ## Репозиторий
494
+
495
+ Выполняет операции чтения и записи документов определенной модели.
496
+ Получить репозиторий можно методом `getRepository` экземпляра схемы.
497
+
498
+ **Методы**
499
+
500
+ - `create(data, filter = undefined)` добавить новый документ
501
+ - `replaceById(id, data, filter = undefined)` заменить весь документ
502
+ - `replaceOrCreate(data, filter = undefined)` заменить или создать новый
503
+ - `patchById(id, data, filter = undefined)` частично обновить документ
504
+ - `patch(data, where = undefined)` обновить все документы или по условию
505
+ - `find(filter = undefined)` найти все документы или по условию
506
+ - `findOne(filter = undefined)` найти первый документ или по условию
507
+ - `findById(id, filter = undefined)` найти документ по идентификатору
508
+ - `delete(where = undefined)` удалить все документы или по условию
509
+ - `deleteById(id)` удалить документ по идентификатору
510
+ - `exists(id)` проверить существование по идентификатору
511
+ - `count(where = undefined)` подсчет всех документов или по условию
512
+
513
+ **Аргументы**
514
+
515
+ - `id: number|string` идентификатор (первичный ключ)
516
+ - `data: object` объект отражающий состав документа
517
+ - `where: object` параметры выборки (см. [Фильтрация](#Фильтрация))
518
+ - `filter: object` параметры возвращаемого результата (см. [Фильтрация](#Фильтрация))
519
+
520
+ **Примеры**
521
+
522
+ Получение репозитория по названию модели.
523
+
524
+ ```js
525
+ const countryRep = schema.getRepository('country');
526
+ ```
527
+
528
+ Добавление нового документа в коллекцию.
529
+
530
+ ```js
531
+ const res = await countryRep.create({
532
+ name: 'Russia',
533
+ population: 143400000,
534
+ });
535
+
536
+ console.log(res);
537
+ // {
538
+ // "id": 1,
539
+ // "name": "Russia",
540
+ // "population": 143400000,
541
+ // }
542
+ ```
543
+
544
+ Поиск документа по идентификатору.
545
+
546
+ ```js
547
+ const res = await countryRep.findById(1);
548
+
549
+ console.log(res);
550
+ // {
551
+ // "id": 1,
552
+ // "name": "Russia",
553
+ // "population": 143400000,
554
+ // }
555
+ ```
556
+
557
+ Удаление документа по идентификатору.
558
+
559
+ ```js
560
+ const res = await countryRep.deleteById(1);
561
+
562
+ console.log(res); // true
563
+ ```
564
+
565
+ ## Фильтрация
566
+
567
+ Некоторые методы репозитория принимают объект настроек влияющий
568
+ на возвращаемый результат. Максимально широкий набор таких настроек
569
+ имеет первый параметр метода `find`, где ожидается объект содержащий
570
+ набор опций указанных ниже.
571
+
572
+ - `where: object` объект выборки
573
+ - `order: string[]` указание порядка
574
+ - `limit: number` ограничение количества документов
575
+ - `skip: number` пропуск документов
576
+ - `fields: string[]` выбор необходимых свойств модели
577
+ - `include: object` включение связанных данных в результат
578
+
579
+ ### where
580
+
581
+ Параметр принимает объект с условиями выборки и поддерживает широкий
582
+ набор операторов сравнения.
583
+
584
+ `{foo: 'bar'}` поиск по значению свойства `foo`
585
+ `{foo: {eq: 'bar'}}` оператор равенства `eq`
586
+ `{foo: {neq: 'bar'}}` оператор неравенства `neq`
587
+ `{foo: {gt: 5}}` оператор "больше" `gt`
588
+ `{foo: {lt: 10}}` оператор "меньше" `lt`
589
+ `{foo: {gte: 5}}` оператор "больше или равно" `gte`
590
+ `{foo: {lte: 10}}` оператор "меньше или равно" `lte`
591
+ `{foo: {inq: ['bar', 'baz']}}` равенство одного из значений `inq`
592
+ `{foo: {nin: ['bar', 'baz']}}` исключение значений массива `nin`
593
+ `{foo: {between: [5, 10]}}` оператор диапазона `between`
594
+ `{foo: {exists: true}}` оператор наличия значения `exists`
595
+ `{foo: {like: 'bar'}}` оператор поиска подстроки `like`
596
+ `{foo: {ilike: 'BaR'}}` регистронезависимая версия `ilike`
597
+ `{foo: {nlike: 'bar'}}` оператор исключения подстроки `nlike`
598
+ `{foo: {nilike: 'BaR'}}` регистронезависимая версия `nilike`
599
+ `{foo: {regexp: 'ba.+'}}` оператор регулярного выражения `regexp`
600
+ `{foo: {regexp: 'ba.+', flags: 'i'}}` флаги регулярного выражения
601
+
602
+ *i. Условия можно объединять операторами `and`, `or` и `nor`.*
603
+
604
+ **Примеры**
605
+
606
+ Применение условий выборки при подсчете документов.
607
+
608
+ ```js
609
+ const res = await rep.count({
610
+ authorId: 251,
611
+ publishedAt: {
612
+ lte: '2023-12-02T14:00:00.000Z',
613
+ },
614
+ });
615
+ ```
616
+
617
+ Применение оператора `or` при удалении документов.
618
+
619
+ ```js
620
+ const res = await rep.delete({
621
+ or: [
622
+ {draft: true},
623
+ {title: {like: 'draft'}},
624
+ ],
625
+ });
626
+ ```
627
+
628
+ ### order
629
+
630
+ Параметр упорядочивает выборку по указанным свойствам модели. Обратное
631
+ направление порядка можно задать постфиксом `DESC` в названии свойства.
632
+
633
+ **Примеры**
634
+
635
+ Упорядочить по полю `createdAt`
636
+
637
+ ```js
638
+ const res = await rep.find({
639
+ order: 'createdAt',
640
+ });
641
+ ```
642
+
643
+ Упорядочить по полю `createdAt` в обратном порядке.
644
+
645
+ ```js
646
+ const res = await rep.find({
647
+ order: 'createdAt DESC',
648
+ });
649
+ ```
650
+
651
+ Упорядочить по нескольким свойствам в разных направлениях.
652
+
653
+ ```js
654
+ const res = await rep.find({
655
+ order: [
656
+ 'title',
657
+ 'price ASC',
658
+ 'featured DESC',
659
+ ],
660
+ });
661
+ ```
662
+
663
+ *i. Направление порядка `ASC` указывать необязательно.*
664
+
665
+ ### include
666
+
667
+ Параметр включает связанные документы в результат вызываемого метода.
668
+ Названия включаемых связей должны быть определены в текущей модели.
669
+ (см. [Связи](#Связи))
670
+
671
+ **Примеры**
672
+
673
+ Включение связи по названию.
674
+
675
+ ```js
676
+ const res = await rep.find({
677
+ include: 'city',
678
+ });
679
+ ```
680
+
681
+ Включение вложенных связей.
682
+
683
+ ```js
684
+ const res = await rep.find({
685
+ include: {
686
+ city: 'country',
687
+ },
688
+ });
689
+ ```
690
+
691
+ Включение нескольких связей массивом.
692
+
693
+ ```js
694
+ const res = await rep.find({
695
+ include: [
696
+ 'city',
697
+ 'address',
698
+ 'employees'
699
+ ],
700
+ });
701
+ ```
702
+
703
+ Использование фильтрации включаемых документов.
704
+
705
+ ```js
706
+ const res = await rep.find({
707
+ include: {
708
+ relation: 'employees', // название связи
709
+ scope: { // фильтрация документов "employees"
710
+ where: {hidden: false}, // условия выборки
711
+ order: 'id', // порядок документов
712
+ limit: 10, // ограничение количества
713
+ skip: 5, // пропуск документов
714
+ fields: ['name', 'surname'], // только указанные поля
715
+ include: 'city', // включение связей для "employees"
716
+ },
717
+ },
718
+ });
719
+ ```
720
+
721
+ ## Связи
722
+
723
+ Параметр `relations` находится в определении модели и принимает
724
+ объект, ключ которого является названием связи, а значением объект
725
+ с параметрами.
726
+
727
+ **Параметры**
728
+
729
+ - `type: string` тип связи
730
+ - `model: string` название целевой модели
731
+ - `foreignKey: string` свойство текущей модели для идентификатора цели
732
+ - `polymorphic: boolean|string` объявить связь полиморфной*
733
+ - `discriminator: string` свойство текущей модели для названия целевой*
734
+
735
+ *i. Полиморфный режим позволяет динамически определять целевую модель
736
+ по ее названию, которое хранит документ в свойстве-дискриминаторе.*
737
+
738
+ **Тип связи**
739
+
740
+ - `belongsTo` - текущая модель содержит свойство для идентификатора цели
741
+ - `hasOne` - обратная сторона `belongsTo` по принципу "один к одному"
742
+ - `hasMany` - обратная сторона `belongsTo` по принципу "один ко многим"
743
+ - `referencesMany` - документ содержит массив с идентификаторами целевой модели
744
+
745
+ **Примеры**
746
+
747
+ Объявление связи `belongsTo`
748
+
749
+ ```js
750
+ schema.defineModel({
751
+ name: 'user',
752
+ relations: {
753
+ role: { // название связи
754
+ type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
755
+ model: 'role', // название целевой модели
756
+ foreignKey: 'roleId', // внешний ключ (необязательно)
757
+ // если "foreignKey" не указан, то свойство внешнего
758
+ // ключа формируется согласно названию связи
759
+ // с добавлением постфикса "Id"
760
+ },
761
+ },
762
+ });
763
+ ```
764
+
765
+ Объявление связи `hasMany`
766
+
767
+ ```js
768
+ schema.defineModel({
769
+ name: 'role',
770
+ relations: {
771
+ users: { // название связи
772
+ type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
773
+ model: 'user', // название целевой модели
774
+ foreignKey: 'roleId', // внешний ключ из целевой модели на текущую
775
+ },
776
+ },
777
+ });
778
+ ```
779
+
780
+ Объявление связи `referencesMany`
781
+
782
+ ```js
783
+ schema.defineModel({
784
+ name: 'article',
785
+ relations: {
786
+ categories: { // название связи
787
+ type: RelationType.REFERENCES_MANY, // связь через массив идентификаторов
788
+ model: 'category', // название целевой модели
789
+ foreignKey: 'categoryIds', // внешний ключ (необязательно)
790
+ // если "foreignKey" не указан, то свойство внешнего
791
+ // ключа формируется согласно названию связи
792
+ // с добавлением постфикса "Ids"
793
+ },
794
+ },
795
+ });
796
+ ```
797
+
798
+ Полиморфная версия `belongsTo`
799
+
800
+ ```js
801
+ schema.defineModel({
802
+ name: 'file',
803
+ relations: {
804
+ reference: { // название связи
805
+ type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
806
+ // полиморфный режим позволяет хранить название целевой модели
807
+ // в свойстве-дискриминаторе, которое формируется согласно
808
+ // названию связи с постфиксом "Type", и в данном случае
809
+ // название целевой модели хранит "referenceType",
810
+ // а идентификатор документа "referenceId"
811
+ polymorphic: true,
812
+ },
813
+ },
814
+ });
815
+ ```
816
+
817
+ Полиморфная версия `belongsTo` с указанием свойств.
818
+
819
+ ```js
820
+ schema.defineModel({
821
+ name: 'file',
822
+ relations: {
823
+ reference: { // название связи
824
+ type: RelationType.BELONGS_TO, // текущая модель ссылается на целевую
825
+ polymorphic: true, // название целевой модели хранит дискриминатор
826
+ foreignKey: 'referenceId', // свойство для идентификатора цели
827
+ discriminator: 'referenceType', // свойство для названия целевой модели
828
+ },
829
+ },
830
+ });
831
+ ```
832
+
833
+ Полиморфная версия `hasMany` с указанием названия связи целевой модели.
834
+
835
+ ```js
836
+ schema.defineModel({
837
+ name: 'letter',
838
+ relations: {
839
+ attachments: { // название связи
840
+ type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
841
+ model: 'file', // название целевой модели
842
+ polymorphic: 'reference', // название полиморфной связи целевой модели
843
+ },
844
+ },
845
+ });
846
+ ```
847
+
848
+ Полиморфная версия `hasMany` с указанием свойств целевой модели.
849
+
850
+ ```js
851
+ schema.defineModel({
852
+ name: 'letter',
853
+ relations: {
854
+ attachments: { // название связи
855
+ type: RelationType.HAS_MANY, // целевая модель ссылается на текущую
856
+ model: 'file', // название целевой модели
857
+ polymorphic: true, // название текущей модели находится в дискриминаторе
858
+ foreignKey: 'referenceId', // свойство целевой модели для идентификатора
859
+ discriminator: 'referenceType', // свойство целевой модели для названия текущей
860
+ },
861
+ },
862
+ });
863
+ ```
864
+
865
+ ## Расширение
866
+
867
+ Метод `getRepository` экземпляра схемы проверяет наличие существующего
868
+ репозитория для указанной модели и возвращает его. В противном случае
869
+ создается новый экземпляр, который будет сохранен для последующих
870
+ обращений к методу.
871
+
872
+ ```js
873
+ import {Schema} from '@e22m4u/js-repository';
874
+ import {Repository} from '@e22m4u/js-repository';
875
+
876
+ // const schema = new Schema();
877
+ // schema.defineDatasource ...
878
+ // schema.defineModel ...
879
+
880
+ const rep1 = schema.getRepository('model');
881
+ const rep2 = schema.getRepository('model');
882
+ console.log(rep1 === rep2); // true
883
+ ```
884
+
885
+ Подмена стандартного конструктора репозитория выполняется методом
886
+ `setRepositoryCtor` сервиса `RepositoryRegistry`, который находится
887
+ в контейнере экземпляра схемы. После чего все новые репозитории будут
888
+ создаваться указанным конструктором вместо стандартного.
889
+
890
+ ```js
891
+ import {Schema} from '@e22m4u/js-repository';
892
+ import {Repository} from '@e22m4u/js-repository';
893
+ import {RepositoryRegistry} from '@e22m4u/js-repository';
894
+
895
+ class MyRepository extends Repository {
896
+ /*...*/
897
+ }
898
+
899
+ // const schema = new Schema();
900
+ // schema.defineDatasource ...
901
+ // schema.defineModel ...
902
+
903
+ schema.get(RepositoryRegistry).setRepositoryCtor(MyRepository);
904
+ const rep = schema.getRepository('model');
905
+ console.log(rep instanceof MyRepository); // true
906
+ ```
907
+
908
+ *i. Так как экземпляры репозитория кэшируется, то замену конструктора
909
+ следует выполнять до обращения к методу `getRepository`.*
910
+
911
+ ## TypeScript
912
+
913
+ Получение типизированного репозитория с указанием интерфейса модели.
914
+
915
+ ```ts
916
+ import {Schema} from '@e22m4u/js-repository';
917
+ import {DataType} from '@e22m4u/js-repository';
918
+ import {RelationType} from '@e22m4u/js-repository';
919
+
920
+ // const schema = new Schema();
921
+ // schema.defineDatasource ...
922
+ // schema.defineModel ...
923
+
924
+ // определение модели "city"
925
+ schema.defineModel({
926
+ name: 'city',
927
+ datasource: 'myDatasource',
928
+ properties: {
929
+ title: DataType.STRING,
930
+ timeZone: DataType.STRING,
931
+ },
932
+ relations: {
933
+ country: {
934
+ type: RelationType.BELONGS_TO,
935
+ model: 'country',
936
+ },
937
+ },
938
+ });
939
+
940
+ // определение интерфейса "city"
941
+ interface City {
942
+ id: number;
943
+ title?: string;
944
+ timeZone?: string;
945
+ countryId?: number;
946
+ country?: Country;
947
+ }
948
+
949
+ // получаем репозиторий по названию модели
950
+ // указывая ее тип и тип идентификатора
951
+ const cityRep = schema.getRepository<City, number>('city');
952
+ ```
953
+
954
+ ## Тесты
955
+
956
+ ```bash
957
+ npm run test
958
+ ```
959
+
960
+ ## Лицензия
961
+
962
+ MIT